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

Viewing files at commit 03be656e7804Commit detailsView latest

tests/runtime.test.ts 250 lines · 8.7 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 {
10
  createAgentRuntimeConnector,
11
  createOpenClaspRuntimeHandler,
12
  MemoryRuntimeSessionStore,
13
  OpenClaspClient,
14
} from '../packages/sdk/src/index.js';
15
16
describe('direct live agent runtime connector', () => {
17
  const platformSecret = 'platform-secret-long-enough-for-testing';
18
  const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
19
  const sessions = new Map<string, LiveSessionActivation>();
20
  const onMessage = vi.fn(async () => ({ text: 'hello back' }));
21
  const handler = createOpenClaspRuntimeHandler({
22
    agentId: 'agent-b',
23
    a2aEndpoint: 'https://agent-b.example/a2a',
24
    openClaspVerificationKey: getSessionVerificationKey(platformSecret),
25
    onSessionOffer: () => ({
26
      accepted: true,
27
      sessionId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
28
    }),
29
    onSessionActivated(session) {
30
      sessions.set(session.interactionId, session);
31
    },
32
    loadSession: async (id) => sessions.get(id),
33
    onMessage,
34
  });
35
36
  it('verifies endpoint ownership and advertises the direct A2A endpoint', async () => {
37
    const response = await handler(
38
      new Request('https://agent-b.example/openclasp', {
39
        method: 'POST',
40
        body: JSON.stringify({
41
          type: 'openclasp.runtime.verify',
42
          version: '1',
43
          agentId: 'agent-b',
44
          challenge: 'challenge-1',
45
        }),
46
      }),
47
    );
48
    await expect(response.json()).resolves.toEqual({
49
      type: 'openclasp.runtime.verified',
50
      version: '1',
51
      agentId: 'agent-b',
52
      challenge: 'challenge-1',
53
      a2aEndpoint: 'https://agent-b.example/a2a',
54
    });
55
  });
56
57
  it('prepares and activates a signed live session, then accepts direct A2A', async () => {
58
    const now = new Date();
59
    const contract = {
60
      protocolVersion: '0.1' as const,
61
      interactionId,
62
      purpose: 'Coordinate research',
63
      parties: ['agent-a', 'agent-b'],
64
      taskCategory: 'research',
65
      requestedOutcome: 'Return sources',
66
      successCriteria: ['Sources returned'],
67
      allowedActions: [],
68
      prohibitedActions: [],
69
      allowedData: [],
70
      prohibitedData: [],
71
      evidenceRequirements: [],
72
      delegationRules: [],
73
      humanApprovalRequirements: [],
74
      factCheckingPolicy: 'important_claims',
75
      mediationPolicy: 'mutual_consent' as const,
76
      retentionDays: 30,
77
      completionConditions: ['Sources returned'],
78
      cancellationConditions: [],
79
      signatures: {},
80
    };
81
    const offer = {
82
      type: 'openclasp.session.offer' as const,
83
      version: '1' as const,
84
      offerId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
85
      interactionId,
86
      agentId: 'agent-b',
87
      role: 'responder' as const,
88
      counterparty: {
89
        agentId: 'agent-a',
90
        name: 'Agent A',
91
        agentVersion: '1.0.0',
92
        capabilities: ['research'],
93
      },
94
      contract,
95
      contractHash: canonicalHash(contract),
96
      privateInsights: [],
97
      issuedAt: now.toISOString(),
98
      expiresAt: new Date(now.getTime() + 60_000).toISOString(),
99
    };
100
    const offerBody = JSON.stringify(offer);
101
    const offerTimestamp = now.toISOString();
102
    const accepted = await handler(
103
      new Request('https://agent-b.example/openclasp', {
104
        method: 'POST',
105
        headers: {
106
          'openclasp-request-id': offer.offerId,
107
          'openclasp-timestamp': offerTimestamp,
108
          'openclasp-signature': `v1=${signSessionControl(platformSecret, offer.offerId, offerTimestamp, offerBody)}`,
109
        },
110
        body: offerBody,
111
      }),
112
    );
113
    expect(accepted.status).toBe(200);
114
    const acceptance = (await accepted.json()) as { sessionId: string };
115
116
    const credential = issueSessionGrant(platformSecret, {
117
      interactionId,
118
      senderAgentId: 'agent-b',
119
      recipientAgentId: 'agent-a',
120
      expiresAt: now.getTime() + 60_000,
121
    });
122
    const activation: LiveSessionActivation = {
123
      type: 'openclasp.session.activation',
124
      version: '1',
125
      activationId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
126
      interactionId,
127
      agentId: 'agent-b',
128
      sessionId: acceptance.sessionId,
129
      role: 'responder',
130
      peer: {
131
        agentId: 'agent-a',
132
        sessionId: 'dddddddd-dddd-4ddd-8ddd-dddddddddddd',
133
        endpoint: 'https://agent-a.example/a2a',
134
        bearerToken: credential,
135
        verificationKey: getSessionVerificationKey(platformSecret),
136
      },
137
      reporting: {
138
        endpoint: `https://openclasp.vercel.app/sessions/${interactionId}/events`,
139
        bearerToken: credential,
140
      },
141
      contractHash: offer.contractHash,
142
      activatedAt: now.toISOString(),
143
      expiresAt: offer.expiresAt,
144
    };
145
    const activationBody = JSON.stringify(activation);
146
    const activationTimestamp = new Date().toISOString();
147
    const activated = await handler(
148
      new Request('https://agent-b.example/openclasp', {
149
        method: 'POST',
150
        headers: {
151
          'openclasp-request-id': activation.activationId,
152
          'openclasp-timestamp': activationTimestamp,
153
          'openclasp-signature': `v1=${signSessionControl(platformSecret, activation.activationId, activationTimestamp, activationBody)}`,
154
        },
155
        body: activationBody,
156
      }),
157
    );
158
    expect(activated.status).toBe(200);
159
160
    const inboundCredential = issueSessionGrant(platformSecret, {
161
      interactionId,
162
      senderAgentId: 'agent-a',
163
      recipientAgentId: 'agent-b',
164
      expiresAt: Date.now() + 60_000,
165
    });
166
    const direct = await handler(
167
      new Request('https://agent-b.example/a2a', {
168
        method: 'POST',
169
        headers: { authorization: `Bearer ${inboundCredential}` },
170
        body: JSON.stringify({
171
          jsonrpc: '2.0',
172
          id: 'turn-1',
173
          method: 'message/send',
174
          params: { message: { parts: [{ kind: 'text', text: 'hello' }] } },
175
        }),
176
      }),
177
    );
178
    expect(direct.status).toBe(200);
179
    await expect(direct.json()).resolves.toMatchObject({
180
      jsonrpc: '2.0',
181
      id: 'turn-1',
182
      result: { text: 'hello back' },
183
    });
184
    expect(onMessage).toHaveBeenCalledOnce();
185
  });
186
});
187
188
it('adapts a provider runtime and persists activation before delivering messages', async () => {
189
  const sessions = new MemoryRuntimeSessionStore();
190
  const calls: string[] = [];
191
  const handler = createAgentRuntimeConnector({
192
    agentId: 'agent-provider',
193
    a2aEndpoint: 'https://provider.example/a2a',
194
    openClaspVerificationKey: getSessionVerificationKey('provider-test-secret'),
195
    sessions,
196
    adapter: {
197
      name: 'test-provider',
198
      prepareSession: () => ({ accepted: true }),
199
      activateSession: async (session) => {
200
        calls.push(`activate:${session.interactionId}`);
201
        expect(await sessions.get(session.interactionId)).toEqual(session);
202
      },
203
      receiveMessage: ({ requestId }) => ({ task: { id: String(requestId), state: 'submitted' } }),
204
    },
205
  });
206
  const verified = await handler(
207
    new Request('https://provider.example/a2a', {
208
      method: 'POST',
209
      body: JSON.stringify({
210
        type: 'openclasp.runtime.verify',
211
        version: '1',
212
        agentId: 'agent-provider',
213
        challenge: 'provider-challenge',
214
      }),
215
    }),
216
  );
217
  expect(verified.status).toBe(200);
218
  await expect(verified.json()).resolves.toMatchObject({
219
    type: 'openclasp.runtime.verified',
220
    agentId: 'agent-provider',
221
    a2aEndpoint: 'https://provider.example/a2a',
222
  });
223
  expect(calls).toEqual([]);
224
});
225
226
it('uses an agent token for bodyless runtime heartbeats without a JSON content header', async () => {
227
  const fetcher = vi.fn(async (_url: string, init?: RequestInit) => {
228
    expect(init?.method).toBe('POST');
229
    expect(new Headers(init?.headers).get('authorization')).toBe('Bearer oc_at_test');
230
    expect(new Headers(init?.headers).has('content-type')).toBe(false);
231
    return Response.json({ status: 'online', checkedAt: new Date().toISOString() });
232
  });
233
  vi.stubGlobal('fetch', fetcher);
234
  try {
235
    await new OpenClaspClient('https://openclasp.example/v0.1', 'oc_at_test').heartbeatRuntime();
236
    expect(fetcher).toHaveBeenCalledOnce();
237
  } finally {
238
    vi.unstubAllGlobals();
239
  }
240
});
241
242
it('rejects local and non-HTTPS runtime targets before connecting', async () => {
243
  await expect(resolvePublicRuntimeEndpoint('http://example.com/runtime')).rejects.toThrow('HTTPS');
244
  await expect(resolvePublicRuntimeEndpoint('https://localhost/runtime')).rejects.toThrow(
245
    'public DNS',
246
  );
247
  await expect(resolvePublicRuntimeEndpoint('https://127.0.0.1/runtime')).rejects.toThrow(
248
    'public DNS',
249
  );
250
});