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

Viewing files at commit 310519e1fe0fCommit detailsView latest

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