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

Viewing files at commit ce192002f08aCommit detailsView latest

tests/api.test.ts 904 lines · 30.7 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
    let storedProfile: any = {
24
      agentId: 'agent-a',
25
      projectId: 'project-a',
26
      name: 'Test agent',
27
      description: '',
28
      framework: 'Botpress',
29
      agentVersion: '1.0.0',
30
      agentMode: 'persistent_runtime',
31
      transport: 'direct_a2a',
32
      autoPublish: false,
33
      autoAcceptPolicy: 'off',
34
      autoAcceptTaskCategories: [],
35
      capabilities: [],
36
      limitations: [],
37
      identityMode: 'owner_managed',
38
      status: 'active',
39
      createdAt: '2026-01-01T00:00:00.000Z',
40
      updatedAt: '2026-01-01T00:00:00.000Z',
41
    };
42
    const repository = {
43
      dashboard: async (operatorId: string) => {
44
        calls.push(`dashboard:${operatorId}`);
45
        return {
46
          agents: [],
47
          projects: [],
48
          installations: [],
49
          setupRequests: [],
50
          publications: [],
51
          interactions: [],
52
          federatedInteractions: [],
53
          liveSessions: [],
54
          hostedThreads: [],
55
          events: [],
56
          conflicts: [],
57
          receipts: [],
58
          profiles: [],
59
          counterpartyBriefs: [],
60
          completionReports: [],
61
          feedbackRequests: [],
62
          interactionFeedback: [],
63
          interactionConclusions: [],
64
          learningEligibility: [],
65
          profileDeltas: [],
66
          runtimes: [],
67
          accessTokens: [],
68
        };
69
      },
70
      getSettings: async (operatorId: string) => {
71
        calls.push(`settings:${operatorId}`);
72
        return {
73
          displayName: '',
74
          contributionEnabled: false,
75
          retentionDays: 30,
76
          evidenceSharing: 'ask' as const,
77
          rawConversationsStored: false as const,
78
        };
79
      },
80
      saveSettings: async (operatorId: string, value: any) => {
81
        calls.push(`save:${operatorId}`);
82
        return { ...value, rawConversationsStored: false as const };
83
      },
84
      upsert: async (_operatorId: string, kind: string, _recordId: string, payload: any) => {
85
        if (kind === 'agent_profile') storedProfile = payload;
86
      },
87
      list: async () => [
88
        { kind: 'agent_profile' as const, recordId: 'agent-a', payload: storedProfile },
89
        {
90
          kind: 'publication' as const,
91
          recordId: 'agent-a',
92
          payload: { agentId: 'agent-a', published: true },
93
        },
94
      ],
95
      publishAgent: async (_operatorId: string, card: any) => card,
96
      unpublishAgent: async () => true,
97
      getPublishedAgent: async () => undefined,
98
      resolveAgentReference: async () => undefined,
99
      searchPublishedAgents: async () => [],
100
      listContextualIntelligence: async () => [
101
        {
102
          agentId: 'agent-peer',
103
          agentVersion: '1.0.0',
104
          taskCategory: 'research',
105
          score: 0.8,
106
          confidence: {
107
            level: 'medium' as const,
108
            value: 0.5,
109
            evidenceCount: 4,
110
            effectiveSampleSize: 3,
111
          },
112
          trend: { direction: 'stable' as const, delta: 0 },
113
          strengths: [],
114
          risks: [],
115
          versionStatus: {
116
            currentVersion: '1.0.0',
117
            evidenceVersion: '1.0.0',
118
            status: 'current' as const,
119
          },
120
          source: 'private_verified_history' as const,
121
          updatedAt: '2026-08-30T00:00:00.000Z',
122
        },
123
      ],
124
      searchPersonalizedMarketplace: async () => [
125
        {
126
          card: { agentId: 'agent-peer', name: 'Peer' },
127
          taskCategory: 'research',
128
          match: { score: 0.7, label: 'possible', reasons: [] },
129
        },
130
      ],
131
      registerAgentRuntime: async (operatorId: string, agentId: string, endpoint: string) => {
132
        calls.push(`runtime:${operatorId}:${agentId}:${endpoint}`);
133
        return {
134
          agentId,
135
          endpoint,
136
          a2aEndpoint: endpoint,
137
          status: 'verified' as const,
138
          verifiedAt: new Date().toISOString(),
139
          verificationKey: 'public-key',
140
        };
141
      },
142
      touchAgentPresence: async (operatorId: string, agentId: string) => {
143
        calls.push(`heartbeat:${operatorId}:${agentId}`);
144
        return {
145
          status: 'online' as const,
146
          checkedAt: '2026-01-01T00:00:00.000Z',
147
        };
148
      },
149
      disableAgentRuntime: async (operatorId: string, agentId: string) => {
150
        calls.push(`disable-runtime:${operatorId}:${agentId}`);
151
        return { agentId, status: 'disabled' as const };
152
      },
153
      deleteAgent: async (operatorId: string, agentId: string) => {
154
        calls.push(`delete-agent:${operatorId}:${agentId}`);
155
        return { agentId, deleted: true as const, historyRetained: true as const };
156
      },
157
      listAgentAccessTokens: async (operatorId: string, agentId?: string) => {
158
        calls.push(`list-tokens:${operatorId}:${agentId ?? '*'}`);
159
        return [];
160
      },
161
      issueAgentAccessToken: async (
162
        operatorId: string,
163
        agentId: string,
164
        value: { name: string; expiresInDays: number },
165
      ) => {
166
        calls.push(`issue-token:${operatorId}:${agentId}:${value.name}:${value.expiresInDays}`);
167
        return {
168
          tokenId: 'abcdefghijklmnop',
169
          token: 'oc_at_abcdefghijklmnop.abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',
170
          agentId,
171
          name: value.name,
172
          scopes: ['mcp:access', 'runtime:connect'],
173
          createdAt: '2026-01-01T00:00:00.000Z',
174
          expiresAt: '2027-01-01T00:00:00.000Z',
175
        };
176
      },
177
      revokeAgentAccessToken: async (operatorId: string, agentId: string, tokenId: string) => {
178
        calls.push(`revoke-token:${operatorId}:${agentId}:${tokenId}`);
179
        return { tokenId, agentId, revokedAt: '2026-01-02T00:00:00.000Z' };
180
      },
181
      getCounterpartyBrief: async (operatorId: string, interactionId: string, agentId: string) => {
182
        calls.push(`brief:${operatorId}:${interactionId}:${agentId}`);
183
        return {
184
          briefId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
185
          interactionId,
186
          contractHash: 'contract-hash',
187
          recipientAgentId: agentId,
188
          subjectAgentId: 'agent-b',
189
          taskCategory: 'recruiting',
190
          decision: 'CHALLENGE' as const,
191
          requirements: [],
192
          insights: [],
193
          relevantSampleSize: 0,
194
          historyConfidence: 0,
195
          subjectAgentVersion: '1.0.0',
196
          recommendedContractChanges: [],
197
          generatedAt: '2026-08-29T00:00:00.000Z',
198
          expiresAt: '2026-08-29T01:00:00.000Z',
199
        };
200
      },
201
      submitCompletionReport: async (
202
        operatorId: string,
203
        agentId: string,
204
        report: any,
205
        submissionMethod: 'oauth_installation' | 'agent_access_token' | 'runtime_session',
206
      ) => {
207
        calls.push(
208
          `completion:${operatorId}:${agentId}:${report.interactionId}:${submissionMethod}`,
209
        );
210
        return report;
211
      },
212
      recordSessionCompletionReport: async (token: string, report: any) => {
213
        calls.push(`session-completion:${token}:${report.interactionId}`);
214
        return report;
215
      },
216
      listFeedbackRequests: async (operatorId: string, agentId: string) => {
217
        calls.push(`feedback-requests:${operatorId}:${agentId}`);
218
        return [];
219
      },
220
      submitInteractionFeedback: async (
221
        operatorId: string,
222
        agentId: string,
223
        feedback: any,
224
        submissionMethod: 'oauth_installation' | 'agent_access_token' | 'runtime_session',
225
      ) => {
226
        calls.push(
227
          `feedback:${operatorId}:${agentId}:${feedback.interactionId}:${submissionMethod}`,
228
        );
229
        return { feedbackId: feedback.feedbackId, status: 'submitted' as const, revealed: false };
230
      },
231
      recordSessionFeedback: async (token: string, feedback: any) => {
232
        calls.push(`session-feedback:${token}:${feedback.interactionId}`);
233
        return { feedbackId: feedback.feedbackId, status: 'submitted' as const, revealed: false };
234
      },
235
      receiveTemporaryMessage: async (
236
        token: string,
237
        agentId: string,
238
        requestKey: string,
239
        content: string,
240
      ) => {
241
        calls.push(`temporary:${token}:${agentId}:${requestKey}:${content}`);
242
        return {
243
          message: {
244
            messageId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
245
            threadId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
246
            interactionId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
247
            senderAgentId: 'agent-peer',
248
            recipientAgentId: agentId,
249
            contentType: 'text/plain' as const,
250
            content,
251
            contentHash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
252
            delivery: 'delivered' as const,
253
            createdAt: new Date().toISOString(),
254
          },
255
          deduplicated: false,
256
        };
257
      },
258
    };
259
    const app = buildApi(undefined, undefined, repository);
260
    await app.ready();
261
    expect((await app.inject({ method: 'GET', url: '/v0.1/dashboard' })).statusCode).toBe(401);
262
    expect(
263
      (
264
        await app.inject({
265
          method: 'GET',
266
          url: '/v0.1/dashboard',
267
          headers: { 'x-openclasp-operator': 'user-a' },
268
        })
269
      ).statusCode,
270
    ).toBe(200);
271
    expect(
272
      (
273
        await app.inject({
274
          method: 'GET',
275
          url: '/v0.1/intelligence?agentId=agent-peer&taskCategory=research',
276
          headers: { 'x-openclasp-operator': 'user-a' },
277
        })
278
      ).json()[0],
279
    ).toMatchObject({ agentId: 'agent-peer', score: 0.8 });
280
    expect(
281
      (
282
        await app.inject({
283
          method: 'GET',
284
          url: '/v0.1/marketplace?agentId=agent-a&taskCategory=research',
285
          headers: { 'x-openclasp-operator': 'user-a' },
286
        })
287
      ).json()[0],
288
    ).toMatchObject({ match: { label: 'possible' } });
289
    expect(
290
      (
291
        await app.inject({
292
          method: 'GET',
293
          url: '/v0.1/settings',
294
          headers: { 'x-openclasp-operator': 'user-b' },
295
        })
296
      ).statusCode,
297
    ).toBe(200);
298
    expect(
299
      (
300
        await app.inject({
301
          method: 'PUT',
302
          url: '/v0.1/agents/agent-a/runtime',
303
          headers: { 'x-openclasp-operator': 'user-a' },
304
          payload: { endpoint: 'https://agent.example/openclasp' },
305
        })
306
      ).statusCode,
307
    ).toBe(200);
308
    expect(calls).toEqual([
309
      'dashboard:user-a',
310
      'settings:user-b',
311
      'runtime:user-a:agent-a:https://agent.example/openclasp',
312
    ]);
313
    expect(
314
      (
315
        await app.inject({
316
          method: 'GET',
317
          url: '/v0.1/runtime/bootstrap',
318
          headers: {
319
            'x-openclasp-operator': 'user-a',
320
            'x-openclasp-bound-agent': 'agent-a',
321
            host: 'openclasp.example',
322
            'x-forwarded-proto': 'https',
323
          },
324
        })
325
      ).json(),
326
    ).toMatchObject({
327
      agentId: 'agent-a',
328
      openClaspUrl: 'https://openclasp.example',
329
      protocol: 'A2A/1.0',
330
    });
331
    expect(
332
      (
333
        await app.inject({
334
          method: 'PUT',
335
          url: '/v0.1/runtime',
336
          headers: {
337
            'x-openclasp-operator': 'user-a',
338
            'x-openclasp-bound-agent': 'agent-a',
339
          },
340
          payload: { endpoint: 'https://runtime.example/a2a' },
341
        })
342
      ).statusCode,
343
    ).toBe(200);
344
    expect(calls.at(-1)).toBe('runtime:user-a:agent-a:https://runtime.example/a2a');
345
    expect(
346
      (
347
        await app.inject({
348
          method: 'DELETE',
349
          url: '/v0.1/runtime',
350
          headers: {
351
            'x-openclasp-operator': 'user-a',
352
            'x-openclasp-bound-agent': 'agent-a',
353
          },
354
        })
355
      ).statusCode,
356
    ).toBe(200);
357
    expect(calls.at(-1)).toBe('disable-runtime:user-a:agent-a');
358
    expect(
359
      (
360
        await app.inject({
361
          method: 'POST',
362
          url: '/v0.1/runtime/heartbeat',
363
          headers: {
364
            'x-openclasp-operator': 'user-a',
365
            'x-openclasp-bound-agent': 'agent-a',
366
          },
367
        })
368
      ).statusCode,
369
    ).toBe(200);
370
    expect(calls.at(-1)).toBe('heartbeat:user-a:agent-a');
371
    const runtimeProfile = await app.inject({
372
      method: 'PUT',
373
      url: '/v0.1/runtime/profile',
374
      headers: {
375
        'x-openclasp-operator': 'user-a',
376
        'x-openclasp-bound-agent': 'agent-a',
377
        host: 'openclasp.example',
378
        'x-forwarded-proto': 'https',
379
      },
380
      payload: {
381
        description: 'Finds suitable candidates for open roles',
382
        capabilities: ['recruiting', 'candidate-screening', 'recruiting'],
383
        limitations: ['no-live-job-database'],
384
      },
385
    });
386
    expect(runtimeProfile.statusCode).toBe(200);
387
    expect(runtimeProfile.json()).toMatchObject({
388
      profile: {
389
        capabilities: ['recruiting', 'candidate-screening'],
390
        limitations: ['no-live-job-database'],
391
      },
392
      card: { capabilities: ['recruiting', 'candidate-screening'] },
393
    });
394
    expect(storedProfile.description).toBe('Finds suitable candidates for open roles');
395
    expect(
396
      (
397
        await app.inject({
398
          method: 'DELETE',
399
          url: '/v0.1/agents/agent-a',
400
          headers: { 'x-openclasp-operator': 'user-a' },
401
        })
402
      ).json(),
403
    ).toMatchObject({ agentId: 'agent-a', deleted: true, historyRetained: true });
404
    expect(calls.at(-1)).toBe('delete-agent:user-a:agent-a');
405
    const issuedToken = await app.inject({
406
      method: 'POST',
407
      url: '/v0.1/agents/agent-a/access-tokens',
408
      headers: { 'x-openclasp-operator': 'user-a' },
409
      payload: { name: 'Botpress', expiresInDays: 365 },
410
    });
411
    expect(issuedToken.statusCode).toBe(200);
412
    expect(issuedToken.json()).toMatchObject({
413
      agentId: 'agent-a',
414
      name: 'Botpress',
415
      scopes: ['mcp:access', 'runtime:connect'],
416
    });
417
    expect(calls.at(-1)).toBe('issue-token:user-a:agent-a:Botpress:365');
418
    expect(
419
      (
420
        await app.inject({
421
          method: 'DELETE',
422
          url: '/v0.1/agents/agent-a/access-tokens/abcdefghijklmnop',
423
          headers: { 'x-openclasp-operator': 'user-a' },
424
        })
425
      ).statusCode,
426
    ).toBe(200);
427
    expect(calls.at(-1)).toBe('revoke-token:user-a:agent-a:abcdefghijklmnop');
428
    const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
429
    expect(
430
      (
431
        await app.inject({
432
          method: 'GET',
433
          url: `/v0.1/federated-interactions/${interactionId}/brief?agentId=agent-a`,
434
          headers: { 'x-openclasp-operator': 'user-a' },
435
        })
436
      ).json(),
437
    ).toMatchObject({ recipientAgentId: 'agent-a', decision: 'CHALLENGE' });
438
    expect(
439
      (
440
        await app.inject({
441
          method: 'GET',
442
          url: `/v0.1/federated-interactions/${interactionId}/brief?agentId=agent-b`,
443
          headers: {
444
            'x-openclasp-operator': 'user-a',
445
            'x-openclasp-bound-agent': 'agent-a',
446
          },
447
        })
448
      ).statusCode,
449
    ).toBe(403);
450
    const completionReport = {
451
      reportId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
452
      interactionId,
453
      contractHash: 'contract-hash',
454
      reportingAgentId: 'agent-a',
455
      counterpartyAgentId: 'agent-b',
456
      agentVersion: '1.0.0',
457
      outcome: 'partial',
458
      summary: 'The counterparty answered but no matching role was available.',
459
      requestedOutcome: 'Find a backend role',
460
      criteria: [],
461
      completedAt: '2026-08-29T00:00:00.000Z',
462
      confidence: 0.9,
463
      dataSharingMode: 'structured_only',
464
    };
465
    expect(
466
      (
467
        await app.inject({
468
          method: 'POST',
469
          url: `/v0.1/federated-interactions/${interactionId}/completion-reports`,
470
          headers: {
471
            'x-openclasp-operator': 'user-a',
472
            'x-openclasp-bound-agent': 'agent-a',
473
          },
474
          payload: completionReport,
475
        })
476
      ).statusCode,
477
    ).toBe(200);
478
    expect(calls.at(-1)).toBe(`completion:user-a:agent-a:${interactionId}:oauth_installation`);
479
    expect(
480
      (
481
        await app.inject({
482
          method: 'POST',
483
          url: `/v0.1/federated-interactions/${interactionId}/completion-reports`,
484
          headers: {
485
            'x-openclasp-operator': 'user-a',
486
            'x-openclasp-bound-agent': 'agent-a',
487
          },
488
          payload: { ...completionReport, rawTranscript: 'must never be accepted' },
489
        })
490
      ).statusCode,
491
    ).toBe(400);
492
    expect(
493
      (
494
        await app.inject({
495
          method: 'POST',
496
          url: `/sessions/${interactionId}/completion-reports`,
497
          payload: completionReport,
498
        })
499
      ).statusCode,
500
    ).toBe(401);
501
    expect(
502
      (
503
        await app.inject({
504
          method: 'POST',
505
          url: `/sessions/${interactionId}/completion-reports`,
506
          headers: { authorization: 'Bearer scoped-session-token' },
507
          payload: completionReport,
508
        })
509
      ).statusCode,
510
    ).toBe(200);
511
    expect(calls.at(-1)).toBe(`session-completion:scoped-session-token:${interactionId}`);
512
    expect(
513
      (
514
        await app.inject({
515
          method: 'GET',
516
          url: '/v0.1/feedback-requests?agentId=agent-a',
517
          headers: {
518
            'x-openclasp-operator': 'user-a',
519
            'x-openclasp-bound-agent': 'agent-a',
520
          },
521
        })
522
      ).statusCode,
523
    ).toBe(200);
524
    const interactionFeedback = {
525
      feedbackId: 'dddddddd-dddd-4ddd-8ddd-dddddddddddd',
526
      requestId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
527
      interactionId,
528
      reviewerAgentId: 'agent-a',
529
      subjectAgentId: 'agent-b',
530
      reviewerAgentVersion: '1.0.0',
531
      ratings: {
532
        overall_satisfaction: 0.7,
533
        outcome_satisfaction: 0.6,
534
        communication: 0.9,
535
        timeliness: 0.8,
536
        scope_adherence: 0.6,
537
        evidence_quality: 0.7,
538
        correction_handling: 0.8,
539
        reliability: 0.75,
540
      },
541
      wouldWorkAgain: 'yes',
542
      confidence: 0.9,
543
      submittedAt: '2026-08-29T00:10:00.000Z',
544
    };
545
    expect(
546
      (
547
        await app.inject({
548
          method: 'POST',
549
          url: `/v0.1/federated-interactions/${interactionId}/feedback`,
550
          headers: {
551
            'x-openclasp-operator': 'user-a',
552
            'x-openclasp-bound-agent': 'agent-a',
553
          },
554
          payload: interactionFeedback,
555
        })
556
      ).statusCode,
557
    ).toBe(200);
558
    expect(calls.at(-1)).toBe(`feedback:user-a:agent-a:${interactionId}:oauth_installation`);
559
    expect(
560
      (
561
        await app.inject({
562
          method: 'POST',
563
          url: `/sessions/${interactionId}/feedback`,
564
          headers: { authorization: 'Bearer scoped-session-token' },
565
          payload: interactionFeedback,
566
        })
567
      ).statusCode,
568
    ).toBe(200);
569
    expect(calls.at(-1)).toBe(`session-feedback:scoped-session-token:${interactionId}`);
570
    const providerConnection = await app.inject({
571
      method: 'POST',
572
      url: '/v0.1/provider-connections',
573
      headers: { 'x-openclasp-operator': 'user-a' },
574
      payload: {
575
        provider: 'botpress',
576
        agentName: 'Recruiting agent',
577
        projectName: 'Recruiting',
578
        description: 'Matches candidates with open roles',
579
        capabilities: ['candidate matching'],
580
        limitations: ['no final hiring decisions'],
581
        expiresInDays: 365,
582
      },
583
    });
584
    expect(providerConnection.statusCode).toBe(200);
585
    expect(providerConnection.json()).toMatchObject({
586
      agent: {
587
        name: 'Recruiting agent',
588
        framework: 'Botpress',
589
        identityMode: 'owner_managed',
590
      },
591
      accessToken: { name: 'Botpress', scopes: ['mcp:access', 'runtime:connect'] },
592
    });
593
    expect(calls.at(-1)).toMatch(/^issue-token:user-a:agent_.*:Botpress:365$/);
594
    expect(
595
      (
596
        await app.inject({
597
          method: 'POST',
598
          url: '/a2a/temporary/agent-a',
599
          payload: {
600
            jsonrpc: '2.0',
601
            id: 'request-1',
602
            method: 'message/send',
603
            params: { message: { parts: [{ kind: 'text', text: 'Hello engineer' }] } },
604
          },
605
        })
606
      ).statusCode,
607
    ).toBe(401);
608
    const temporaryDelivery = await app.inject({
609
      method: 'POST',
610
      url: '/a2a/temporary/agent-a',
611
      headers: { authorization: 'Bearer scoped-token' },
612
      payload: {
613
        jsonrpc: '2.0',
614
        id: 'request-1',
615
        method: 'message/send',
616
        params: { message: { parts: [{ kind: 'text', text: 'Hello engineer' }] } },
617
      },
618
    });
619
    expect(temporaryDelivery.statusCode).toBe(200);
620
    expect(temporaryDelivery.json()).toMatchObject({
621
      result: {
622
        task: { id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', state: 'submitted' },
623
        privacyMode: 'openclasp_hosted_temporary',
624
      },
625
    });
626
    expect(calls.at(-1)).toBe('temporary:scoped-token:agent-a:request-1:Hello engineer');
627
    await app.close();
628
  });
629
630
  it('publishes only an owned agent public card', async () => {
631
    const published: any[] = [];
632
    const repository = {
633
      dashboard: async () => ({}) as any,
634
      getSettings: async () => ({}) as any,
635
      saveSettings: async () => ({}) as any,
636
      upsert: async () => undefined,
637
      list: async () => [
638
        {
639
          kind: 'agent_profile' as const,
640
          recordId: 'agent-one',
641
          payload: {
642
            agentId: 'agent-one',
643
            projectId: 'secret-project',
644
            name: 'Research agent',
645
            description: 'Finds primary sources',
646
            framework: 'Codex',
647
            agentVersion: '1.0.0',
648
            a2aEndpoint: 'https://agent.example/a2a',
649
            capabilities: ['research'],
650
            limitations: ['no purchases'],
651
            identityMode: 'oauth_installation' as const,
652
            status: 'active' as const,
653
            createdAt: '2026-01-01T00:00:00.000Z',
654
            updatedAt: '2026-01-01T00:00:00.000Z',
655
          },
656
        },
657
      ],
658
      publishAgent: async (_operatorId: string, card: any) => {
659
        published.push(card);
660
        return card;
661
      },
662
      unpublishAgent: async () => true,
663
      getPublishedAgent: async () => published[0],
664
      resolveAgentReference: async (reference: string) =>
665
        published[0]
666
          ? {
667
              reference,
668
              matchedBy: 'slug' as const,
669
              verified: true as const,
670
              card: published[0],
671
              resolvedAt: '2026-08-30T00:00:00.000Z',
672
            }
673
          : undefined,
674
      searchPublishedAgents: async () => published,
675
    };
676
    const app = buildApi(undefined, undefined, repository);
677
    await app.ready();
678
    const response = await app.inject({
679
      method: 'POST',
680
      url: '/v0.1/agents/agent-one/publication',
681
      headers: { 'x-openclasp-operator': 'owner-one' },
682
      payload: { published: true },
683
    });
684
    expect(response.statusCode).toBe(200);
685
    expect(published[0]).toMatchObject({
686
      agentId: 'agent-one',
687
      capabilities: ['research'],
688
      assurance: 'oauth_authenticated',
689
    });
690
    expect(published[0]).not.toHaveProperty('projectId');
691
    expect(published[0]).not.toHaveProperty('operatorId');
692
    const publicCard = await app.inject({
693
      method: 'GET',
694
      url: '/agents/agent-one/card.json',
695
    });
696
    expect(publicCard.statusCode).toBe(200);
697
    expect(publicCard.json()).not.toHaveProperty('projectId');
698
    const a2aCard = await app.inject({
699
      method: 'GET',
700
      url: '/agents/agent-one/a2a-agent-card.json',
701
    });
702
    expect(a2aCard.statusCode).toBe(200);
703
    expect(a2aCard.json().supportedInterfaces[0]).toMatchObject({
704
      url: 'https://agent.example/a2a',
705
      protocolVersion: '1.0',
706
    });
707
    const resolution = await app.inject({
708
      method: 'GET',
709
      url: `/directory/resolve?reference=${encodeURIComponent(published[0].profileUrl)}`,
710
    });
711
    expect(resolution.statusCode).toBe(200);
712
    expect(resolution.json()).toMatchObject({ verified: true, card: { agentId: 'agent-one' } });
713
    const profile = await app.inject({ method: 'GET', url: `/a/${published[0].slug}` });
714
    expect(profile.statusCode).toBe(200);
715
    expect(profile.headers['content-type']).toContain('text/html');
716
    expect(profile.body).toContain('Account and agent ownership verified');
717
    const automation = await app.inject({
718
      method: 'PUT',
719
      url: '/v0.1/agents/agent-one/automation',
720
      headers: { 'x-openclasp-operator': 'owner-one' },
721
      payload: {
722
        a2aEndpoint: 'https://agent.example/a2a',
723
        autoPublish: true,
724
        autoAcceptPolicy: 'safe_matching',
725
        autoAcceptTaskCategories: ['research'],
726
      },
727
    });
728
    expect(automation.statusCode).toBe(200);
729
    expect(automation.json()).toMatchObject({
730
      autoPublish: true,
731
      autoAcceptPolicy: 'safe_matching',
732
    });
733
    await app.close();
734
  });
735
736
  it('accepts only structured live-session events at the reporting endpoint', async () => {
737
    const events: any[] = [];
738
    const repository = {
739
      dashboard: async () => ({}) as any,
740
      getSettings: async () => ({}) as any,
741
      saveSettings: async () => ({}) as any,
742
      upsert: async () => undefined,
743
      list: async () => [],
744
      publishAgent: async (_operatorId: string, card: any) => card,
745
      unpublishAgent: async () => true,
746
      getPublishedAgent: async () => undefined,
747
      resolveAgentReference: async () => undefined,
748
      searchPublishedAgents: async () => [],
749
      recordLiveSessionEvent: async (token: string, value: any) => {
750
        events.push({ token, value });
751
        return {
752
          recorded: true,
753
          deduplicated: false,
754
          eventId: value.eventId,
755
          attestation: {
756
            algorithm: 'Ed25519' as const,
757
            keyId: 'openclasp:test',
758
            value: 'signature',
759
            digest: 'digest',
760
          },
761
        };
762
      },
763
    };
764
    const app = buildApi(undefined, undefined, repository);
765
    await app.ready();
766
    const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
767
    expect(
768
      (await app.inject({ method: 'POST', url: `/sessions/${interactionId}/events` })).statusCode,
769
    ).toBe(401);
770
    const response = await app.inject({
771
      method: 'POST',
772
      url: `/sessions/${interactionId}/events`,
773
      headers: { authorization: 'Bearer live-session-token' },
774
      payload: {
775
        eventId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
776
        interactionId,
777
        agentId: 'agent-a',
778
        sequence: 1,
779
        type: 'message_sent',
780
        occurredAt: new Date().toISOString(),
781
        messageHash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
782
        evidenceReferences: [],
783
        details: {},
784
      },
785
    });
786
    expect(response.statusCode).toBe(200);
787
    expect(events[0]).toMatchObject({
788
      token: 'live-session-token',
789
      value: { interactionId, agentId: 'agent-a', type: 'message_sent' },
790
    });
791
    const rawMessageAttempt = await app.inject({
792
      method: 'POST',
793
      url: `/sessions/${interactionId}/events`,
794
      headers: { authorization: 'Bearer live-session-token' },
795
      payload: {
796
        eventId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
797
        interactionId,
798
        agentId: 'agent-a',
799
        sequence: 2,
800
        type: 'message_sent',
801
        occurredAt: new Date().toISOString(),
802
        details: { message: 'raw conversation text' },
803
      },
804
    });
805
    expect(rawMessageAttempt.statusCode).toBe(400);
806
    expect(events).toHaveLength(1);
807
    await app.close();
808
  });
809
810
  it('binds contract proposals and responses to the authenticated participating agent', async () => {
811
    const calls: any[] = [];
812
    const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
813
    const revisionId = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
814
    const contract = {
815
      protocolVersion: '0.1',
816
      interactionId,
817
      purpose: 'Buy paper',
818
      parties: ['agent-a', 'agent-b'],
819
      taskCategory: 'procurement',
820
      requestedOutcome: 'Five tonnes delivered',
821
      successCriteria: ['80 GSM verified', 'Delivered on time'],
822
      allowedActions: ['negotiate'],
823
      prohibitedActions: ['exceed_budget'],
824
      allowedData: [],
825
      prohibitedData: ['credentials'],
826
      evidenceRequirements: ['invoice'],
827
      delegationRules: ['explicit_contract_scope'],
828
      humanApprovalRequirements: [],
829
      factCheckingPolicy: 'important_claims',
830
      mediationPolicy: 'mutual_consent',
831
      retentionDays: 30,
832
      completionConditions: ['delivery accepted'],
833
      cancellationConditions: ['either party before acceptance'],
834
      signatures: {},
835
    };
836
    const repository: any = {
837
      dashboard: async () => ({}),
838
      getSettings: async () => ({}),
839
      saveSettings: async () => ({}),
840
      upsert: async () => undefined,
841
      list: async () => [],
842
      publishAgent: async (_operatorId: string, card: any) => card,
843
      unpublishAgent: async () => true,
844
      getPublishedAgent: async () => undefined,
845
      resolveAgentReference: async () => undefined,
846
      searchPublishedAgents: async () => [],
847
      proposeContractRevision: async (...args: any[]) => {
848
        calls.push(['propose', ...args]);
849
        return { interactionId, status: 'pending' };
850
      },
851
      respondToContractRevision: async (...args: any[]) => {
852
        calls.push(['respond', ...args]);
853
        return { interactionId, status: 'active' };
854
      },
855
    };
856
    const app = buildApi(undefined, undefined, repository);
857
    await app.ready();
858
    const headers = {
859
      'x-openclasp-operator': 'owner-a',
860
      'x-openclasp-bound-agent': 'agent-a',
861
      'x-openclasp-credential-type': 'agent_access_token',
862
    };
863
    const proposed = await app.inject({
864
      method: 'POST',
865
      url: `/v0.1/federated-interactions/${interactionId}/contract-proposals`,
866
      headers,
867
      payload: { agentId: 'agent-a', expectedTermsHash: 'old-hash', contract },
868
    });
869
    expect(proposed.statusCode).toBe(200);
870
    expect(calls[0]).toMatchObject([
871
      'propose',
872
      'owner-a',
873
      interactionId,
874
      'agent-a',
875
      contract,
876
      'old-hash',
877
      'oauth_installation',
878
    ]);
879
    const accepted = await app.inject({
880
      method: 'POST',
881
      url: `/v0.1/federated-interactions/${interactionId}/contract-proposals/${revisionId}/respond`,
882
      headers,
883
      payload: { agentId: 'agent-a', decision: 'accept' },
884
    });
885
    expect(accepted.statusCode).toBe(200);
886
    expect(calls[1]).toEqual([
887
      'respond',
888
      'owner-a',
889
      interactionId,
890
      'agent-a',
891
      revisionId,
892
      'accept',
893
      'oauth_installation',
894
    ]);
895
    const impersonation = await app.inject({
896
      method: 'POST',
897
      url: `/v0.1/federated-interactions/${interactionId}/contract-proposals`,
898
      headers,
899
      payload: { agentId: 'agent-b', contract },
900
    });
901
    expect(impersonation.statusCode).toBe(403);
902
    await app.close();
903
  });
904
});