| 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 | });
|