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

Viewing files at commit 296bb55574a2Commit detailsView latest

tests/runtime.test.ts 191 lines · 6.6 KB · typescript
1
import { describe, expect, it, vi } from 'vitest';
2
import { canonicalHash, type LiveSessionActivation } from '../packages/protocol/src/index.js';
3
import {
4
  getSessionVerificationKey,
5
  issueSessionGrant,
6
  signSessionControl,
7
} from '../packages/persistence/src/relay.js';
8
import { resolvePublicRuntimeEndpoint } from '../packages/persistence/src/runtime.js';
9
import { createOpenClaspRuntimeHandler } from '../packages/sdk/src/index.js';
10
11
describe('direct live agent runtime connector', () => {
12
  const platformSecret = 'platform-secret-long-enough-for-testing';
13
  const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
14
  const sessions = new Map<string, LiveSessionActivation>();
15
  const onMessage = vi.fn(async () => ({ text: 'hello back' }));
16
  const handler = createOpenClaspRuntimeHandler({
17
    agentId: 'agent-b',
18
    a2aEndpoint: 'https://agent-b.example/a2a',
19
    openClaspVerificationKey: getSessionVerificationKey(platformSecret),
20
    onSessionOffer: () => ({
21
      accepted: true,
22
      sessionId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
23
    }),
24
    onSessionActivated(session) {
25
      sessions.set(session.interactionId, session);
26
    },
27
    loadSession: async (id) => sessions.get(id),
28
    onMessage,
29
  });
30
31
  it('verifies endpoint ownership and advertises the direct A2A endpoint', async () => {
32
    const response = await handler(
33
      new Request('https://agent-b.example/openclasp', {
34
        method: 'POST',
35
        body: JSON.stringify({
36
          type: 'openclasp.runtime.verify',
37
          version: '1',
38
          agentId: 'agent-b',
39
          challenge: 'challenge-1',
40
        }),
41
      }),
42
    );
43
    await expect(response.json()).resolves.toEqual({
44
      type: 'openclasp.runtime.verified',
45
      version: '1',
46
      agentId: 'agent-b',
47
      challenge: 'challenge-1',
48
      a2aEndpoint: 'https://agent-b.example/a2a',
49
    });
50
  });
51
52
  it('prepares and activates a signed live session, then accepts direct A2A', async () => {
53
    const now = new Date();
54
    const contract = {
55
      protocolVersion: '0.1' as const,
56
      interactionId,
57
      purpose: 'Coordinate research',
58
      parties: ['agent-a', 'agent-b'],
59
      taskCategory: 'research',
60
      requestedOutcome: 'Return sources',
61
      successCriteria: ['Sources returned'],
62
      allowedActions: [],
63
      prohibitedActions: [],
64
      allowedData: [],
65
      prohibitedData: [],
66
      evidenceRequirements: [],
67
      delegationRules: [],
68
      humanApprovalRequirements: [],
69
      factCheckingPolicy: 'important_claims',
70
      mediationPolicy: 'mutual_consent' as const,
71
      retentionDays: 30,
72
      completionConditions: ['Sources returned'],
73
      cancellationConditions: [],
74
      signatures: {},
75
    };
76
    const offer = {
77
      type: 'openclasp.session.offer' as const,
78
      version: '1' as const,
79
      offerId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
80
      interactionId,
81
      agentId: 'agent-b',
82
      role: 'responder' as const,
83
      counterparty: {
84
        agentId: 'agent-a',
85
        name: 'Agent A',
86
        agentVersion: '1.0.0',
87
        capabilities: ['research'],
88
      },
89
      contract,
90
      contractHash: canonicalHash(contract),
91
      privateInsights: [],
92
      issuedAt: now.toISOString(),
93
      expiresAt: new Date(now.getTime() + 60_000).toISOString(),
94
    };
95
    const offerBody = JSON.stringify(offer);
96
    const offerTimestamp = now.toISOString();
97
    const accepted = await handler(
98
      new Request('https://agent-b.example/openclasp', {
99
        method: 'POST',
100
        headers: {
101
          'openclasp-request-id': offer.offerId,
102
          'openclasp-timestamp': offerTimestamp,
103
          'openclasp-signature': `v1=${signSessionControl(platformSecret, offer.offerId, offerTimestamp, offerBody)}`,
104
        },
105
        body: offerBody,
106
      }),
107
    );
108
    expect(accepted.status).toBe(200);
109
    const acceptance = (await accepted.json()) as { sessionId: string };
110
111
    const credential = issueSessionGrant(platformSecret, {
112
      interactionId,
113
      senderAgentId: 'agent-b',
114
      recipientAgentId: 'agent-a',
115
      expiresAt: now.getTime() + 60_000,
116
    });
117
    const activation: LiveSessionActivation = {
118
      type: 'openclasp.session.activation',
119
      version: '1',
120
      activationId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
121
      interactionId,
122
      agentId: 'agent-b',
123
      sessionId: acceptance.sessionId,
124
      role: 'responder',
125
      peer: {
126
        agentId: 'agent-a',
127
        sessionId: 'dddddddd-dddd-4ddd-8ddd-dddddddddddd',
128
        endpoint: 'https://agent-a.example/a2a',
129
        bearerToken: credential,
130
        verificationKey: getSessionVerificationKey(platformSecret),
131
      },
132
      reporting: {
133
        endpoint: `https://openclasp.vercel.app/sessions/${interactionId}/events`,
134
        bearerToken: credential,
135
      },
136
      contractHash: offer.contractHash,
137
      activatedAt: now.toISOString(),
138
      expiresAt: offer.expiresAt,
139
    };
140
    const activationBody = JSON.stringify(activation);
141
    const activationTimestamp = new Date().toISOString();
142
    const activated = await handler(
143
      new Request('https://agent-b.example/openclasp', {
144
        method: 'POST',
145
        headers: {
146
          'openclasp-request-id': activation.activationId,
147
          'openclasp-timestamp': activationTimestamp,
148
          'openclasp-signature': `v1=${signSessionControl(platformSecret, activation.activationId, activationTimestamp, activationBody)}`,
149
        },
150
        body: activationBody,
151
      }),
152
    );
153
    expect(activated.status).toBe(200);
154
155
    const inboundCredential = issueSessionGrant(platformSecret, {
156
      interactionId,
157
      senderAgentId: 'agent-a',
158
      recipientAgentId: 'agent-b',
159
      expiresAt: Date.now() + 60_000,
160
    });
161
    const direct = await handler(
162
      new Request('https://agent-b.example/a2a', {
163
        method: 'POST',
164
        headers: { authorization: `Bearer ${inboundCredential}` },
165
        body: JSON.stringify({
166
          jsonrpc: '2.0',
167
          id: 'turn-1',
168
          method: 'message/send',
169
          params: { message: { parts: [{ kind: 'text', text: 'hello' }] } },
170
        }),
171
      }),
172
    );
173
    expect(direct.status).toBe(200);
174
    await expect(direct.json()).resolves.toMatchObject({
175
      jsonrpc: '2.0',
176
      id: 'turn-1',
177
      result: { text: 'hello back' },
178
    });
179
    expect(onMessage).toHaveBeenCalledOnce();
180
  });
181
});
182
183
it('rejects local and non-HTTPS runtime targets before connecting', async () => {
184
  await expect(resolvePublicRuntimeEndpoint('http://example.com/runtime')).rejects.toThrow('HTTPS');
185
  await expect(resolvePublicRuntimeEndpoint('https://localhost/runtime')).rejects.toThrow(
186
    'public DNS',
187
  );
188
  await expect(resolvePublicRuntimeEndpoint('https://127.0.0.1/runtime')).rejects.toThrow(
189
    'public DNS',
190
  );
191
});