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

Viewing files at commit 6e4e24f0d92fCommit detailsView latest

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