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