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

Viewing files at commit 6b91d3cf5293Commit detailsView latest

tests/api.test.ts 980 lines · 34.0 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(200);
331
    expect(calls).toEqual([
332
      'dashboard:user-a',
333
      'settings:user-b',
334
      'runtime:user-a:agent-a:https://agent.example/openclasp',
335
    ]);
336
    expect(
337
      (
338
        await app.inject({
339
          method: 'GET',
340
          url: '/v0.1/runtime/bootstrap',
341
          headers: {
342
            'x-openclasp-operator': 'user-a',
343
            'x-openclasp-bound-agent': 'agent-a',
344
            host: 'openclasp.example',
345
            'x-forwarded-proto': 'https',
346
          },
347
        })
348
      ).json(),
349
    ).toMatchObject({
350
      agentId: 'agent-a',
351
      openClaspUrl: 'https://openclasp.example',
352
      protocol: 'A2A/1.0',
353
    });
354
    expect(
355
      (
356
        await app.inject({
357
          method: 'PUT',
358
          url: '/v0.1/runtime',
359
          headers: {
360
            'x-openclasp-operator': 'user-a',
361
            'x-openclasp-bound-agent': 'agent-a',
362
          },
363
          payload: { endpoint: 'https://runtime.example/a2a' },
364
        })
365
      ).statusCode,
366
    ).toBe(200);
367
    expect(calls.at(-1)).toBe('runtime:user-a:agent-a:https://runtime.example/a2a');
368
    expect(
369
      (
370
        await app.inject({
371
          method: 'DELETE',
372
          url: '/v0.1/runtime',
373
          headers: {
374
            'x-openclasp-operator': 'user-a',
375
            'x-openclasp-bound-agent': 'agent-a',
376
          },
377
        })
378
      ).statusCode,
379
    ).toBe(200);
380
    expect(calls.at(-1)).toBe('disable-runtime:user-a:agent-a');
381
    expect(
382
      (
383
        await app.inject({
384
          method: 'POST',
385
          url: '/v0.1/runtime/heartbeat',
386
          headers: {
387
            'x-openclasp-operator': 'user-a',
388
            'x-openclasp-bound-agent': 'agent-a',
389
          },
390
        })
391
      ).statusCode,
392
    ).toBe(200);
393
    expect(calls.at(-1)).toBe('heartbeat:user-a:agent-a');
394
    const runtimeProfile = await app.inject({
395
      method: 'PUT',
396
      url: '/v0.1/runtime/profile',
397
      headers: {
398
        'x-openclasp-operator': 'user-a',
399
        'x-openclasp-bound-agent': 'agent-a',
400
        host: 'openclasp.example',
401
        'x-forwarded-proto': 'https',
402
      },
403
      payload: {
404
        description: 'Finds suitable candidates for open roles',
405
        capabilities: ['recruiting', 'candidate-screening', 'recruiting'],
406
        limitations: ['no-live-job-database'],
407
      },
408
    });
409
    expect(runtimeProfile.statusCode).toBe(200);
410
    expect(runtimeProfile.json()).toMatchObject({
411
      profile: {
412
        capabilities: ['recruiting', 'candidate-screening'],
413
        limitations: ['no-live-job-database'],
414
      },
415
      card: { capabilities: ['recruiting', 'candidate-screening'] },
416
    });
417
    expect(storedProfile.description).toBe('Finds suitable candidates for open roles');
418
    expect(
419
      (
420
        await app.inject({
421
          method: 'DELETE',
422
          url: '/v0.1/agents/agent-a',
423
          headers: { 'x-openclasp-operator': 'user-a' },
424
        })
425
      ).json(),
426
    ).toMatchObject({ agentId: 'agent-a', deleted: true, historyRetained: true });
427
    expect(calls.at(-1)).toBe('delete-agent:user-a:agent-a');
428
    const issuedToken = await app.inject({
429
      method: 'POST',
430
      url: '/v0.1/agents/agent-a/access-tokens',
431
      headers: { 'x-openclasp-operator': 'user-a' },
432
      payload: { name: 'Botpress', expiresInDays: 365 },
433
    });
434
    expect(issuedToken.statusCode).toBe(200);
435
    expect(issuedToken.json()).toMatchObject({
436
      agentId: 'agent-a',
437
      name: 'Botpress',
438
      scopes: ['mcp:access', 'runtime:connect'],
439
    });
440
    expect(calls.at(-1)).toBe('issue-token:user-a:agent-a:Botpress:365');
441
    expect(
442
      (
443
        await app.inject({
444
          method: 'DELETE',
445
          url: '/v0.1/agents/agent-a/access-tokens/abcdefghijklmnop',
446
          headers: { 'x-openclasp-operator': 'user-a' },
447
        })
448
      ).statusCode,
449
    ).toBe(200);
450
    expect(calls.at(-1)).toBe('revoke-token:user-a:agent-a:abcdefghijklmnop');
451
    const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
452
    expect(
453
      (
454
        await app.inject({
455
          method: 'GET',
456
          url: `/v0.1/federated-interactions/${interactionId}/brief?agentId=agent-a`,
457
          headers: { 'x-openclasp-operator': 'user-a' },
458
        })
459
      ).json(),
460
    ).toMatchObject({ recipientAgentId: 'agent-a', decision: 'CHALLENGE' });
461
    expect(
462
      (
463
        await app.inject({
464
          method: 'GET',
465
          url: `/v0.1/federated-interactions/${interactionId}/brief?agentId=agent-b`,
466
          headers: {
467
            'x-openclasp-operator': 'user-a',
468
            'x-openclasp-bound-agent': 'agent-a',
469
          },
470
        })
471
      ).statusCode,
472
    ).toBe(403);
473
    const completionReport = {
474
      reportId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
475
      interactionId,
476
      contractHash: 'contract-hash',
477
      reportingAgentId: 'agent-a',
478
      counterpartyAgentId: 'agent-b',
479
      agentVersion: '1.0.0',
480
      outcome: 'partial',
481
      summary: 'The counterparty answered but no matching role was available.',
482
      requestedOutcome: 'Find a backend role',
483
      criteria: [],
484
      completedAt: '2026-08-29T00:00:00.000Z',
485
      confidence: 0.9,
486
      dataSharingMode: 'structured_only',
487
    };
488
    expect(
489
      (
490
        await app.inject({
491
          method: 'POST',
492
          url: `/v0.1/federated-interactions/${interactionId}/completion-reports`,
493
          headers: {
494
            'x-openclasp-operator': 'user-a',
495
            'x-openclasp-bound-agent': 'agent-a',
496
          },
497
          payload: completionReport,
498
        })
499
      ).statusCode,
500
    ).toBe(200);
501
    expect(calls.at(-1)).toBe(`completion:user-a:agent-a:${interactionId}:oauth_installation`);
502
    expect(
503
      (
504
        await app.inject({
505
          method: 'POST',
506
          url: `/v0.1/federated-interactions/${interactionId}/completion-reports`,
507
          headers: {
508
            'x-openclasp-operator': 'user-a',
509
            'x-openclasp-bound-agent': 'agent-a',
510
          },
511
          payload: { ...completionReport, rawTranscript: 'must never be accepted' },
512
        })
513
      ).statusCode,
514
    ).toBe(400);
515
    expect(
516
      (
517
        await app.inject({
518
          method: 'POST',
519
          url: `/sessions/${interactionId}/completion-reports`,
520
          payload: completionReport,
521
        })
522
      ).statusCode,
523
    ).toBe(401);
524
    expect(
525
      (
526
        await app.inject({
527
          method: 'POST',
528
          url: `/sessions/${interactionId}/completion-reports`,
529
          headers: { authorization: 'Bearer scoped-session-token' },
530
          payload: completionReport,
531
        })
532
      ).statusCode,
533
    ).toBe(200);
534
    expect(calls.at(-1)).toBe(`session-completion:scoped-session-token:${interactionId}`);
535
    expect(
536
      (
537
        await app.inject({
538
          method: 'GET',
539
          url: '/v0.1/feedback-requests?agentId=agent-a',
540
          headers: {
541
            'x-openclasp-operator': 'user-a',
542
            'x-openclasp-bound-agent': 'agent-a',
543
          },
544
        })
545
      ).statusCode,
546
    ).toBe(200);
547
    const interactionFeedback = {
548
      feedbackId: 'dddddddd-dddd-4ddd-8ddd-dddddddddddd',
549
      requestId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
550
      interactionId,
551
      reviewerAgentId: 'agent-a',
552
      subjectAgentId: 'agent-b',
553
      reviewerAgentVersion: '1.0.0',
554
      ratings: {
555
        overall_satisfaction: 0.7,
556
        outcome_satisfaction: 0.6,
557
        communication: 0.9,
558
        timeliness: 0.8,
559
        scope_adherence: 0.6,
560
        evidence_quality: 0.7,
561
        correction_handling: 0.8,
562
        reliability: 0.75,
563
      },
564
      wouldWorkAgain: 'yes',
565
      confidence: 0.9,
566
      submittedAt: '2026-08-29T00:10:00.000Z',
567
    };
568
    expect(
569
      (
570
        await app.inject({
571
          method: 'POST',
572
          url: `/v0.1/federated-interactions/${interactionId}/feedback`,
573
          headers: {
574
            'x-openclasp-operator': 'user-a',
575
            'x-openclasp-bound-agent': 'agent-a',
576
          },
577
          payload: interactionFeedback,
578
        })
579
      ).statusCode,
580
    ).toBe(200);
581
    expect(calls.at(-1)).toBe(`feedback:user-a:agent-a:${interactionId}:oauth_installation`);
582
    expect(
583
      (
584
        await app.inject({
585
          method: 'POST',
586
          url: `/sessions/${interactionId}/feedback`,
587
          headers: { authorization: 'Bearer scoped-session-token' },
588
          payload: interactionFeedback,
589
        })
590
      ).statusCode,
591
    ).toBe(200);
592
    expect(calls.at(-1)).toBe(`session-feedback:scoped-session-token:${interactionId}`);
593
    const assuranceResponse = {
594
      protocolVersion: '0.1',
595
      responseId: crypto.randomUUID(),
596
      planId: crypto.randomUUID(),
597
      interactionId,
598
      contractHash: 'contract-hash',
599
      phase: 'pre_task',
600
      agentId: 'agent-a',
601
      agentVersion: '1.0.0',
602
      answers: [
603
        {
604
          probeId: crypto.randomUUID(),
605
          questionCode: 'deadline_risk',
606
          responseType: 'enum',
607
          answer: 'yes',
608
          confidence: 0.9,
609
        },
610
      ],
611
      respondedAt: '2026-08-29T00:05:00.000Z',
612
    };
613
    expect(
614
      (
615
        await app.inject({
616
          method: 'POST',
617
          url: `/sessions/${interactionId}/assurance-responses`,
618
          payload: assuranceResponse,
619
        })
620
      ).statusCode,
621
    ).toBe(401);
622
    expect(
623
      (
624
        await app.inject({
625
          method: 'POST',
626
          url: `/sessions/${interactionId}/assurance-responses`,
627
          headers: { authorization: 'Bearer scoped-session-token' },
628
          payload: assuranceResponse,
629
        })
630
      ).statusCode,
631
    ).toBe(200);
632
    expect(calls.at(-1)).toBe(`session-assurance:scoped-session-token:${interactionId}`);
633
    expect(
634
      (
635
        await app.inject({
636
          method: 'GET',
637
          url: `/v0.1/federated-interactions/${interactionId}/assurance-brief?agentId=agent-a`,
638
          headers: {
639
            'x-openclasp-operator': 'user-a',
640
            'x-openclasp-bound-agent': 'agent-a',
641
          },
642
        })
643
      ).json(),
644
    ).toMatchObject({ advisoryNotice: 'experimental_estimate_not_a_guarantee' });
645
    const safeguardId = 'ffffffff-ffff-4fff-8fff-ffffffffffff';
646
    expect(
647
      (
648
        await app.inject({
649
          method: 'POST',
650
          url: `/v0.1/federated-interactions/${interactionId}/assurance-safeguards/${safeguardId}/decision`,
651
          headers: {
652
            'x-openclasp-operator': 'user-a',
653
            'x-openclasp-bound-agent': 'agent-a',
654
          },
655
          payload: { status: 'accepted' },
656
        })
657
      ).json(),
658
    ).toMatchObject({ safeguard: { safeguardId, status: 'accepted' } });
659
    expect(calls.at(-1)).toBe(
660
      `assurance-safeguard:user-a:${interactionId}:agent-a:${safeguardId}:accepted`,
661
    );
662
    const providerConnection = await app.inject({
663
      method: 'POST',
664
      url: '/v0.1/provider-connections',
665
      headers: { 'x-openclasp-operator': 'user-a' },
666
      payload: {
667
        provider: 'botpress',
668
        agentName: 'Recruiting agent',
669
        projectName: 'Recruiting',
670
        description: 'Matches candidates with open roles',
671
        capabilities: ['candidate matching'],
672
        limitations: ['no final hiring decisions'],
673
        expiresInDays: 365,
674
      },
675
    });
676
    expect(providerConnection.statusCode).toBe(200);
677
    expect(providerConnection.json()).toMatchObject({
678
      agent: {
679
        name: 'Recruiting agent',
680
        framework: 'Botpress',
681
        identityMode: 'owner_managed',
682
      },
683
      accessToken: { name: 'Botpress', scopes: ['mcp:access', 'runtime:connect'] },
684
    });
685
    expect(calls.at(-1)).toMatch(/^issue-token:user-a:agent_.*:Botpress:365$/);
686
    const removedConversationRoute = await app.inject({
687
      method: 'POST',
688
      url: '/a2a/temporary/agent-a',
689
      headers: { authorization: 'Bearer scoped-token' },
690
      payload: {},
691
    });
692
    expect(removedConversationRoute.statusCode).toBe(404);
693
    await app.close();
694
  });
695
696
  it('publishes only an owned agent public card', async () => {
697
    const published: any[] = [];
698
    const repository = {
699
      dashboard: async () => ({}) as any,
700
      getSettings: async () => ({}) as any,
701
      saveSettings: async () => ({}) as any,
702
      upsert: async () => undefined,
703
      list: async () => [
704
        {
705
          kind: 'agent_profile' as const,
706
          recordId: 'agent-one',
707
          payload: {
708
            agentId: 'agent-one',
709
            projectId: 'secret-project',
710
            name: 'Research agent',
711
            description: 'Finds primary sources',
712
            framework: 'Codex',
713
            agentVersion: '1.0.0',
714
            a2aEndpoint: 'https://agent.example/a2a',
715
            capabilities: ['research'],
716
            limitations: ['no purchases'],
717
            identityMode: 'oauth_installation' as const,
718
            status: 'active' as const,
719
            createdAt: '2026-01-01T00:00:00.000Z',
720
            updatedAt: '2026-01-01T00:00:00.000Z',
721
          },
722
        },
723
      ],
724
      publishAgent: async (_operatorId: string, card: any) => {
725
        published.push(card);
726
        return card;
727
      },
728
      unpublishAgent: async () => true,
729
      getPublishedAgent: async () => published[0],
730
      resolveAgentReference: async (reference: string) =>
731
        published[0]
732
          ? {
733
              reference,
734
              matchedBy: 'slug' as const,
735
              verified: true as const,
736
              card: published[0],
737
              resolvedAt: '2026-08-30T00:00:00.000Z',
738
            }
739
          : undefined,
740
      searchPublishedAgents: async () => published,
741
    };
742
    const app = buildApi(undefined, undefined, repository);
743
    await app.ready();
744
    const response = await app.inject({
745
      method: 'POST',
746
      url: '/v0.1/agents/agent-one/publication',
747
      headers: { 'x-openclasp-operator': 'owner-one' },
748
      payload: { published: true },
749
    });
750
    expect(response.statusCode).toBe(200);
751
    expect(published[0]).toMatchObject({
752
      agentId: 'agent-one',
753
      capabilities: ['research'],
754
      assurance: 'oauth_authenticated',
755
    });
756
    expect(published[0]).not.toHaveProperty('projectId');
757
    expect(published[0]).not.toHaveProperty('operatorId');
758
    const publicCard = await app.inject({
759
      method: 'GET',
760
      url: '/agents/agent-one/card.json',
761
    });
762
    expect(publicCard.statusCode).toBe(200);
763
    expect(publicCard.json()).not.toHaveProperty('projectId');
764
    const a2aCard = await app.inject({
765
      method: 'GET',
766
      url: '/agents/agent-one/a2a-agent-card.json',
767
    });
768
    expect(a2aCard.statusCode).toBe(200);
769
    expect(a2aCard.json().supportedInterfaces[0]).toMatchObject({
770
      url: 'https://agent.example/a2a',
771
      protocolVersion: '1.0',
772
    });
773
    const resolution = await app.inject({
774
      method: 'GET',
775
      url: `/directory/resolve?reference=${encodeURIComponent(published[0].profileUrl)}`,
776
    });
777
    expect(resolution.statusCode).toBe(200);
778
    expect(resolution.json()).toMatchObject({ verified: true, card: { agentId: 'agent-one' } });
779
    const profile = await app.inject({ method: 'GET', url: `/a/${published[0].slug}` });
780
    expect(profile.statusCode).toBe(200);
781
    expect(profile.headers['content-type']).toContain('text/html');
782
    expect(profile.body).toContain('Publisher verified');
783
    expect(profile.body).toContain('Share-ready Agent Card');
784
    expect(profile.body).toContain('Add your agent — free');
785
    expect(profile.body).toContain('Capabilities and limitations are declared by the publisher');
786
    expect(profile.body).toContain('property="og:image"');
787
    expect(profile.body).toContain('/agents/agent-one/og.png');
788
    expect(profile.body).toContain('name="twitter:card" content="summary_large_image"');
789
    const socialCard = await app.inject({ method: 'GET', url: '/agents/agent-one/og.png' });
790
    expect(socialCard.statusCode).toBe(200);
791
    expect(socialCard.headers['content-type']).toContain('image/png');
792
    expect(socialCard.rawPayload.subarray(1, 4).toString()).toBe('PNG');
793
    const automation = await app.inject({
794
      method: 'PUT',
795
      url: '/v0.1/agents/agent-one/automation',
796
      headers: { 'x-openclasp-operator': 'owner-one' },
797
      payload: {
798
        a2aEndpoint: 'https://agent.example/a2a',
799
        autoPublish: true,
800
        autoAcceptPolicy: 'safe_matching',
801
        autoAcceptTaskCategories: ['research'],
802
      },
803
    });
804
    expect(automation.statusCode).toBe(200);
805
    expect(automation.json()).toMatchObject({
806
      autoPublish: true,
807
      autoAcceptPolicy: 'safe_matching',
808
    });
809
    await app.close();
810
  });
811
812
  it('accepts only structured live-session events at the reporting endpoint', async () => {
813
    const events: any[] = [];
814
    const repository = {
815
      dashboard: async () => ({}) as any,
816
      getSettings: async () => ({}) as any,
817
      saveSettings: async () => ({}) as any,
818
      upsert: async () => undefined,
819
      list: async () => [],
820
      publishAgent: async (_operatorId: string, card: any) => card,
821
      unpublishAgent: async () => true,
822
      getPublishedAgent: async () => undefined,
823
      resolveAgentReference: async () => undefined,
824
      searchPublishedAgents: async () => [],
825
      recordLiveSessionEvent: async (token: string, value: any) => {
826
        events.push({ token, value });
827
        return {
828
          recorded: true,
829
          deduplicated: false,
830
          eventId: value.eventId,
831
          attestation: {
832
            algorithm: 'Ed25519' as const,
833
            keyId: 'openclasp:test',
834
            value: 'signature',
835
            digest: 'digest',
836
          },
837
        };
838
      },
839
    };
840
    const app = buildApi(undefined, undefined, repository);
841
    await app.ready();
842
    const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
843
    expect(
844
      (await app.inject({ method: 'POST', url: `/sessions/${interactionId}/events` })).statusCode,
845
    ).toBe(401);
846
    const response = await app.inject({
847
      method: 'POST',
848
      url: `/sessions/${interactionId}/events`,
849
      headers: { authorization: 'Bearer live-session-token' },
850
      payload: {
851
        eventId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
852
        interactionId,
853
        agentId: 'agent-a',
854
        sequence: 1,
855
        type: 'message_sent',
856
        occurredAt: new Date().toISOString(),
857
        messageHash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
858
        evidenceReferences: [],
859
        details: {},
860
      },
861
    });
862
    expect(response.statusCode).toBe(200);
863
    expect(events[0]).toMatchObject({
864
      token: 'live-session-token',
865
      value: { interactionId, agentId: 'agent-a', type: 'message_sent' },
866
    });
867
    const rawMessageAttempt = await app.inject({
868
      method: 'POST',
869
      url: `/sessions/${interactionId}/events`,
870
      headers: { authorization: 'Bearer live-session-token' },
871
      payload: {
872
        eventId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
873
        interactionId,
874
        agentId: 'agent-a',
875
        sequence: 2,
876
        type: 'message_sent',
877
        occurredAt: new Date().toISOString(),
878
        details: { message: 'raw conversation text' },
879
      },
880
    });
881
    expect(rawMessageAttempt.statusCode).toBe(400);
882
    expect(events).toHaveLength(1);
883
    await app.close();
884
  });
885
886
  it('binds contract proposals and responses to the authenticated participating agent', async () => {
887
    const calls: any[] = [];
888
    const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
889
    const revisionId = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
890
    const contract = {
891
      protocolVersion: '0.1',
892
      interactionId,
893
      purpose: 'Buy paper',
894
      parties: ['agent-a', 'agent-b'],
895
      taskCategory: 'procurement',
896
      requestedOutcome: 'Five tonnes delivered',
897
      successCriteria: ['80 GSM verified', 'Delivered on time'],
898
      allowedActions: ['negotiate'],
899
      prohibitedActions: ['exceed_budget'],
900
      allowedData: [],
901
      prohibitedData: ['credentials'],
902
      evidenceRequirements: ['invoice'],
903
      delegationRules: ['explicit_contract_scope'],
904
      humanApprovalRequirements: [],
905
      factCheckingPolicy: 'important_claims',
906
      mediationPolicy: 'mutual_consent',
907
      retentionDays: 30,
908
      completionConditions: ['delivery accepted'],
909
      cancellationConditions: ['either party before acceptance'],
910
      signatures: {},
911
    };
912
    const repository: any = {
913
      dashboard: async () => ({}),
914
      getSettings: async () => ({}),
915
      saveSettings: async () => ({}),
916
      upsert: async () => undefined,
917
      list: async () => [],
918
      publishAgent: async (_operatorId: string, card: any) => card,
919
      unpublishAgent: async () => true,
920
      getPublishedAgent: async () => undefined,
921
      resolveAgentReference: async () => undefined,
922
      searchPublishedAgents: async () => [],
923
      proposeContractRevision: async (...args: any[]) => {
924
        calls.push(['propose', ...args]);
925
        return { interactionId, status: 'pending' };
926
      },
927
      respondToContractRevision: async (...args: any[]) => {
928
        calls.push(['respond', ...args]);
929
        return { interactionId, status: 'active' };
930
      },
931
    };
932
    const app = buildApi(undefined, undefined, repository);
933
    await app.ready();
934
    const headers = {
935
      'x-openclasp-operator': 'owner-a',
936
      'x-openclasp-bound-agent': 'agent-a',
937
      'x-openclasp-credential-type': 'agent_access_token',
938
    };
939
    const proposed = await app.inject({
940
      method: 'POST',
941
      url: `/v0.1/federated-interactions/${interactionId}/contract-proposals`,
942
      headers,
943
      payload: { agentId: 'agent-a', expectedTermsHash: 'old-hash', contract },
944
    });
945
    expect(proposed.statusCode).toBe(200);
946
    expect(calls[0]).toMatchObject([
947
      'propose',
948
      'owner-a',
949
      interactionId,
950
      'agent-a',
951
      contract,
952
      'old-hash',
953
      'oauth_installation',
954
    ]);
955
    const accepted = await app.inject({
956
      method: 'POST',
957
      url: `/v0.1/federated-interactions/${interactionId}/contract-proposals/${revisionId}/respond`,
958
      headers,
959
      payload: { agentId: 'agent-a', decision: 'accept' },
960
    });
961
    expect(accepted.statusCode).toBe(200);
962
    expect(calls[1]).toEqual([
963
      'respond',
964
      'owner-a',
965
      interactionId,
966
      'agent-a',
967
      revisionId,
968
      'accept',
969
      'oauth_installation',
970
    ]);
971
    const impersonation = await app.inject({
972
      method: 'POST',
973
      url: `/v0.1/federated-interactions/${interactionId}/contract-proposals`,
974
      headers,
975
      payload: { agentId: 'agent-b', contract },
976
    });
977
    expect(impersonation.statusCode).toBe(403);
978
    await app.close();
979
  });
980
});