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

Viewing files at commit 77f5465edca6Commit detailsView latest

packages/sdk/src/index.ts 674 lines · 23.5 KB · typescript
1
import {
2
  DEFAULT_EXTENSION_URI,
3
  canonicalHash,
4
  signObject,
5
  verifyRecordAttestation,
6
  type InteractionEvent,
7
  type KeyPair,
8
  type TrustEnvelope,
9
  type FederatedInteraction,
10
  type AgentResolution,
11
  type InteractionContract,
12
  LiveSessionActivationSchema,
13
  LiveSessionEventSchema,
14
  LiveSessionOfferSchema,
15
  type LiveSessionActivation,
16
  type LiveSessionEvent,
17
  type LiveSessionOffer,
18
  type PublicAgentCard,
19
  type HostedThread,
20
  type HostedMessage,
21
} from '../../protocol/src/index.js';
22
import { createPublicKey, verify } from 'node:crypto';
23
export { createIdentity } from '../../core/src/index.js';
24
export * from '../../protocol/src/index.js';
25
26
export class OpenClaspClient {
27
  constructor(
28
    readonly baseUrl = 'http://localhost:3100/v0.1',
29
    readonly accessToken?: string,
30
  ) {}
31
  private async request<T>(path: string, init?: RequestInit): Promise<T> {
32
    const response = await fetch(`${this.baseUrl}${path}`, {
33
      ...init,
34
      headers: {
35
        ...(init?.body !== undefined && init.body !== null
36
          ? { 'content-type': 'application/json' }
37
          : {}),
38
        ...(this.accessToken ? { authorization: `Bearer ${this.accessToken}` } : {}),
39
        ...init?.headers,
40
      },
41
    });
42
    const body = (await response.json()) as any;
43
    if (!response.ok) throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
44
    return body as T;
45
  }
46
  registerAgent(identity: unknown) {
47
    return this.request('/agents', { method: 'POST', body: JSON.stringify(identity) });
48
  }
49
  getProfile(agentId: string, version: string, taskCategory: string) {
50
    return this.request(
51
      `/profiles/${encodeURIComponent(agentId)}?version=${encodeURIComponent(version)}&taskCategory=${encodeURIComponent(taskCategory)}`,
52
    );
53
  }
54
  saveContract(contract: unknown) {
55
    return this.request('/interactions/contracts', {
56
      method: 'POST',
57
      body: JSON.stringify(contract),
58
    });
59
  }
60
  assess(input: unknown) {
61
    return this.request('/risk/assess', { method: 'POST', body: JSON.stringify(input) });
62
  }
63
  recordEvent(event: unknown) {
64
    return this.request('/events', { method: 'POST', body: JSON.stringify(event) });
65
  }
66
  checkClaim(claim: string) {
67
    return this.request('/claims/check', { method: 'POST', body: JSON.stringify({ claim }) });
68
  }
69
  submitReceipt(receipt: unknown) {
70
    return this.request('/receipts', { method: 'POST', body: JSON.stringify(receipt) });
71
  }
72
  verifyReceipt(receipt: unknown) {
73
    return this.request('/receipts/verify', { method: 'POST', body: JSON.stringify(receipt) });
74
  }
75
  submitFeedback(feedback: unknown) {
76
    return this.request('/feedback', { method: 'POST', body: JSON.stringify(feedback) });
77
  }
78
  setContributionConsent(agentId: string, enabled: boolean) {
79
    return this.request('/contributions/consent', {
80
      method: 'POST',
81
      body: JSON.stringify({ agentId, enabled }),
82
    });
83
  }
84
  getPublicAgentCard(agentId: string): Promise<PublicAgentCard> {
85
    const root = this.baseUrl.replace(/\/v0\.1\/?$/, '');
86
    return fetch(`${root}/agents/${encodeURIComponent(agentId)}/card.json`).then(
87
      async (response) => {
88
        const body = (await response.json()) as any;
89
        if (!response.ok)
90
          throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
91
        return body as PublicAgentCard;
92
      },
93
    );
94
  }
95
  resolveAgent(reference: string): Promise<AgentResolution> {
96
    const root = this.baseUrl.replace(/\/v0\.1\/?$/, '');
97
    return fetch(`${root}/directory/resolve?reference=${encodeURIComponent(reference)}`).then(
98
      async (response) => {
99
        const body = (await response.json()) as any;
100
        if (!response.ok)
101
          throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
102
        return body as AgentResolution;
103
      },
104
    );
105
  }
106
  searchAgents(input: { query?: string; capability?: string; limit?: number } = {}) {
107
    const root = this.baseUrl.replace(/\/v0\.1\/?$/, '');
108
    const search = new URLSearchParams();
109
    if (input.query) search.set('query', input.query);
110
    if (input.capability) search.set('capability', input.capability);
111
    if (input.limit) search.set('limit', String(input.limit));
112
    return fetch(`${root}/directory/search?${search}`).then(async (response) => {
113
      const body = (await response.json()) as any;
114
      if (!response.ok)
115
        throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
116
      return body as PublicAgentCard[];
117
    });
118
  }
119
  getContextualIntelligence(input: { agentId?: string; taskCategory?: string } = {}) {
120
    const search = new URLSearchParams();
121
    if (input.agentId) search.set('agentId', input.agentId);
122
    if (input.taskCategory) search.set('taskCategory', input.taskCategory);
123
    return this.request(`/intelligence?${search}`);
124
  }
125
  recommendAgents(
126
    input: { agentId?: string; taskCategory?: string; query?: string; limit?: number } = {},
127
  ) {
128
    const search = new URLSearchParams();
129
    if (input.agentId) search.set('agentId', input.agentId);
130
    if (input.taskCategory) search.set('taskCategory', input.taskCategory);
131
    if (input.query) search.set('query', input.query);
132
    if (input.limit) search.set('limit', String(input.limit));
133
    return this.request(`/marketplace?${search}`);
134
  }
135
  async verifyPlatformRecord(value: Record<string, unknown>): Promise<boolean> {
136
    const root = this.baseUrl.replace(/\/v0\.1\/?$/, '');
137
    const keyUrl =
138
      typeof value.verification === 'object' &&
139
      value.verification !== null &&
140
      'verificationKeyUrl' in value.verification &&
141
      typeof value.verification.verificationKeyUrl === 'string'
142
        ? value.verification.verificationKeyUrl
143
        : `${root}/.well-known/openclasp-session-key`;
144
    const response = await fetch(keyUrl);
145
    if (!response.ok) return false;
146
    const key = (await response.json()) as { publicKey?: string };
147
    return typeof key.publicKey === 'string' && verifyRecordAttestation(value, key.publicKey);
148
  }
149
  createFederatedInteraction(value: FederatedInteraction) {
150
    return this.request<FederatedInteraction>('/federated-interactions', {
151
      method: 'POST',
152
      body: JSON.stringify(value),
153
    });
154
  }
155
  listFederatedInteractions() {
156
    return this.request<FederatedInteraction[]>('/federated-interactions');
157
  }
158
  getFederatedInteraction(interactionId: string) {
159
    return this.request<FederatedInteraction>(
160
      `/federated-interactions/${encodeURIComponent(interactionId)}`,
161
    );
162
  }
163
  getLiveSession(interactionId: string, agentId: string): Promise<LiveSessionActivation> {
164
    return this.request(
165
      `/federated-interactions/${encodeURIComponent(interactionId)}/session?agentId=${encodeURIComponent(agentId)}`,
166
    );
167
  }
168
  respondToFederatedInteraction(
169
    interactionId: string,
170
    agentId: string,
171
    decision: 'accept' | 'reject',
172
  ) {
173
    return this.request<FederatedInteraction>(
174
      `/federated-interactions/${encodeURIComponent(interactionId)}/respond`,
175
      { method: 'POST', body: JSON.stringify({ agentId, decision }) },
176
    );
177
  }
178
  proposeContractRevision(
179
    interactionId: string,
180
    agentId: string,
181
    contract: InteractionContract,
182
    expectedTermsHash?: string,
183
  ) {
184
    return this.request<FederatedInteraction>(
185
      `/federated-interactions/${encodeURIComponent(interactionId)}/contract-proposals`,
186
      {
187
        method: 'POST',
188
        body: JSON.stringify({ agentId, contract, expectedTermsHash }),
189
      },
190
    );
191
  }
192
  respondToContractRevision(
193
    interactionId: string,
194
    revisionId: string,
195
    agentId: string,
196
    decision: 'accept' | 'reject',
197
  ) {
198
    return this.request<FederatedInteraction>(
199
      `/federated-interactions/${encodeURIComponent(interactionId)}/contract-proposals/${encodeURIComponent(revisionId)}/respond`,
200
      { method: 'POST', body: JSON.stringify({ agentId, decision }) },
201
    );
202
  }
203
  listHostedThreads(agentId: string): Promise<HostedThread[]> {
204
    return this.request(`/agents/${encodeURIComponent(agentId)}/threads`);
205
  }
206
  getHostedThread(
207
    agentId: string,
208
    threadId: string,
209
  ): Promise<{
210
    thread: HostedThread;
211
    messages: HostedMessage[];
212
    insights: unknown[];
213
  }> {
214
    return this.request(
215
      `/agents/${encodeURIComponent(agentId)}/threads/${encodeURIComponent(threadId)}`,
216
    );
217
  }
218
  sendTemporaryMessage(agentId: string, interactionId: string, content: string) {
219
    return this.request(`/agents/${encodeURIComponent(agentId)}/messages`, {
220
      method: 'POST',
221
      body: JSON.stringify({ interactionId, content }),
222
    });
223
  }
224
  markHostedThreadRead(agentId: string, threadId: string) {
225
    return this.request(
226
      `/agents/${encodeURIComponent(agentId)}/threads/${encodeURIComponent(threadId)}/read`,
227
      { method: 'POST' },
228
    );
229
  }
230
  closeHostedThread(agentId: string, threadId: string) {
231
    return this.request(
232
      `/agents/${encodeURIComponent(agentId)}/threads/${encodeURIComponent(threadId)}/close`,
233
      { method: 'POST' },
234
    );
235
  }
236
  getRuntimeBootstrap(): Promise<RuntimeBootstrap> {
237
    return this.request('/runtime/bootstrap');
238
  }
239
  connectRuntime(endpoint: string): Promise<RuntimeConnection> {
240
    return this.request('/runtime', {
241
      method: 'PUT',
242
      body: JSON.stringify({ endpoint }),
243
    });
244
  }
245
  disconnectRuntime(): Promise<{ agentId: string; status: 'disabled' }> {
246
    return this.request('/runtime', { method: 'DELETE' });
247
  }
248
  heartbeatRuntime(): Promise<{ status: 'online' | 'offline'; checkedAt: string }> {
249
    return this.request('/runtime/heartbeat', { method: 'POST' });
250
  }
251
}
252
253
export type RuntimeBootstrap = {
254
  agentId: string;
255
  openClaspUrl: string;
256
  runtimeRegistrationEndpoint: string;
257
  protocol: 'A2A/1.0';
258
  controlProtocol: 'OpenClasp/0.1';
259
};
260
261
export type RuntimeConnection = {
262
  agentId: string;
263
  endpoint: string;
264
  a2aEndpoint: string;
265
  status: 'verified';
266
  verifiedAt: string;
267
  verificationKey: string;
268
};
269
270
export function createSignedEvent(
271
  input: Omit<InteractionEvent, 'payloadHash' | 'signature'>,
272
  key: KeyPair,
273
): InteractionEvent {
274
  return signObject(
275
    { ...input, payloadHash: canonicalHash(input.payload) },
276
    key,
277
  ) as InteractionEvent;
278
}
279
280
export function createSignedEnvelope(
281
  input: Omit<TrustEnvelope, 'signature'>,
282
  key: KeyPair,
283
): TrustEnvelope {
284
  return signObject(input, key) as TrustEnvelope;
285
}
286
287
function validRuntimeSignature(
288
  publicKey: string,
289
  requestId: string,
290
  timestamp: string,
291
  body: string,
292
  signature: string,
293
) {
294
  if (Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60_000) return false;
295
  return verify(
296
    null,
297
    Buffer.from(`${timestamp}.${requestId}.${body}`),
298
    createPublicKey({ key: Buffer.from(publicKey, 'base64url'), type: 'spki', format: 'der' }),
299
    Buffer.from(signature.replace(/^v1=/, ''), 'base64url'),
300
  );
301
}
302
303
export function createOpenClaspRuntimeHandler(input: {
304
  agentId: string;
305
  a2aEndpoint: string;
306
  openClaspUrl?: string;
307
  openClaspVerificationKey?: string;
308
  onSessionOffer: (
309
    offer: LiveSessionOffer,
310
  ) =>
311
    Promise<{ accepted: boolean; sessionId?: string }> | { accepted: boolean; sessionId?: string };
312
  onSessionActivated: (session: LiveSessionActivation) => Promise<void> | void;
313
  loadSession: (interactionId: string) => Promise<LiveSessionActivation | undefined>;
314
  onMessage: (input: {
315
    session: LiveSessionActivation;
316
    requestId: string | number;
317
    message: unknown;
318
  }) => Promise<unknown> | unknown;
319
}) {
320
  let verificationKeyPromise: Promise<string> | undefined;
321
  const verificationKey = () => {
322
    if (input.openClaspVerificationKey) return Promise.resolve(input.openClaspVerificationKey);
323
    return (verificationKeyPromise ??= fetch(
324
      `${(input.openClaspUrl ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/.well-known/openclasp-session-key`,
325
    )
326
      .then(async (response) => {
327
        if (!response.ok) throw new Error(`OpenClasp key discovery failed: ${response.status}`);
328
        const value = (await response.json()) as { publicKey?: unknown };
329
        if (typeof value.publicKey !== 'string')
330
          throw new Error('Invalid OpenClasp verification key');
331
        return value.publicKey;
332
      })
333
      .catch((error) => {
334
        verificationKeyPromise = undefined;
335
        throw error;
336
      }));
337
  };
338
  return async (request: Request): Promise<Response> => {
339
    const body = await request.text();
340
    let parsed: Record<string, unknown>;
341
    try {
342
      parsed = JSON.parse(body) as Record<string, unknown>;
343
    } catch {
344
      return Response.json({ error: 'invalid_json' }, { status: 400 });
345
    }
346
    if (parsed.type === 'openclasp.runtime.verify')
347
      return Response.json({
348
        type: 'openclasp.runtime.verified',
349
        version: '1',
350
        agentId: parsed.agentId,
351
        challenge: parsed.challenge,
352
        a2aEndpoint: input.a2aEndpoint,
353
      });
354
    if (parsed.type !== 'openclasp.session.offer' && parsed.type !== 'openclasp.session.activation')
355
      return handleDirectMessage(request, parsed, input);
356
    const requestId = request.headers.get('openclasp-request-id') ?? '';
357
    const timestamp = request.headers.get('openclasp-timestamp') ?? '';
358
    const signature = request.headers.get('openclasp-signature') ?? '';
359
    let signatureValid = false;
360
    try {
361
      signatureValid = Boolean(
362
        requestId &&
363
        timestamp &&
364
        signature &&
365
        validRuntimeSignature(await verificationKey(), requestId, timestamp, body, signature),
366
      );
367
    } catch {
368
      return Response.json({ error: 'openclasp_key_unavailable' }, { status: 503 });
369
    }
370
    if (!signatureValid)
371
      return Response.json({ error: 'invalid_openclasp_signature' }, { status: 401 });
372
    if (parsed.type === 'openclasp.session.offer') {
373
      const result = LiveSessionOfferSchema.safeParse(parsed);
374
      if (
375
        !result.success ||
376
        result.data.offerId !== requestId ||
377
        result.data.agentId !== input.agentId
378
      )
379
        return Response.json({ error: 'invalid_session_offer' }, { status: 400 });
380
      const decision = await input.onSessionOffer(result.data);
381
      if (!decision.accepted) return Response.json({ error: 'session_declined' }, { status: 409 });
382
      if (!decision.sessionId)
383
        return Response.json({ error: 'durable_session_id_required' }, { status: 500 });
384
      return Response.json({
385
        type: 'openclasp.session.accepted',
386
        version: '1',
387
        offerId: result.data.offerId,
388
        interactionId: result.data.interactionId,
389
        agentId: input.agentId,
390
        sessionId: decision.sessionId,
391
        a2aEndpoint: input.a2aEndpoint,
392
        expiresAt: result.data.expiresAt,
393
      });
394
    }
395
    const activation = LiveSessionActivationSchema.safeParse(parsed);
396
    if (
397
      !activation.success ||
398
      activation.data.activationId !== requestId ||
399
      activation.data.agentId !== input.agentId
400
    )
401
      return Response.json({ error: 'invalid_session_activation' }, { status: 400 });
402
    await input.onSessionActivated(activation.data);
403
    return Response.json({ accepted: true, activationId: activation.data.activationId });
404
  };
405
}
406
407
type RuntimeHandlerInput = Parameters<typeof createOpenClaspRuntimeHandler>[0];
408
409
export interface RuntimeSessionStore {
410
  get(interactionId: string): Promise<LiveSessionActivation | undefined>;
411
  put(interactionId: string, session: LiveSessionActivation): Promise<void>;
412
}
413
414
export interface AgentRuntimeAdapter {
415
  readonly name: string;
416
  prepareSession(
417
    offer: LiveSessionOffer,
418
  ): Promise<{ accepted: boolean; sessionId?: string }> | { accepted: boolean; sessionId?: string };
419
  activateSession(session: LiveSessionActivation): Promise<void> | void;
420
  receiveMessage(input: {
421
    session: LiveSessionActivation;
422
    requestId: string | number;
423
    message: unknown;
424
  }): Promise<unknown> | unknown;
425
}
426
427
/** In-memory storage is useful for tests only. Production runtimes must use durable storage. */
428
export class MemoryRuntimeSessionStore implements RuntimeSessionStore {
429
  readonly sessions = new Map<string, LiveSessionActivation>();
430
431
  async get(interactionId: string) {
432
    return this.sessions.get(interactionId);
433
  }
434
435
  async put(interactionId: string, session: LiveSessionActivation) {
436
    this.sessions.set(interactionId, session);
437
  }
438
}
439
440
export function createAgentRuntimeConnector(input: {
441
  agentId: string;
442
  a2aEndpoint: string;
443
  adapter: AgentRuntimeAdapter;
444
  sessions: RuntimeSessionStore;
445
  openClaspUrl?: string;
446
  openClaspVerificationKey?: string;
447
}) {
448
  return createOpenClaspRuntimeHandler({
449
    agentId: input.agentId,
450
    a2aEndpoint: input.a2aEndpoint,
451
    ...(input.openClaspUrl ? { openClaspUrl: input.openClaspUrl } : {}),
452
    ...(input.openClaspVerificationKey
453
      ? { openClaspVerificationKey: input.openClaspVerificationKey }
454
      : {}),
455
    async onSessionOffer(offer) {
456
      const decision = await input.adapter.prepareSession(offer);
457
      return decision.accepted
458
        ? { accepted: true, sessionId: decision.sessionId ?? crypto.randomUUID() }
459
        : { accepted: false };
460
    },
461
    async onSessionActivated(session) {
462
      await input.sessions.put(session.interactionId, session);
463
      await input.adapter.activateSession(session);
464
    },
465
    loadSession: (interactionId) => input.sessions.get(interactionId),
466
    onMessage: (message) => input.adapter.receiveMessage(message),
467
  });
468
}
469
470
export class HttpAgentRuntimeAdapter implements AgentRuntimeAdapter {
471
  readonly name = 'http-webhook';
472
  private readonly baseUrl: string;
473
474
  constructor(
475
    baseUrl: string,
476
    private readonly options: {
477
      bearerToken?: string;
478
      timeoutMs?: number;
479
    } = {},
480
  ) {
481
    this.baseUrl = baseUrl.replace(/\/$/, '');
482
  }
483
484
  private async post<T>(path: string, body: unknown): Promise<T> {
485
    const response = await fetch(`${this.baseUrl}${path}`, {
486
      method: 'POST',
487
      headers: {
488
        'content-type': 'application/json',
489
        ...(this.options.bearerToken
490
          ? { authorization: `Bearer ${this.options.bearerToken}` }
491
          : {}),
492
      },
493
      body: JSON.stringify(body),
494
      signal: AbortSignal.timeout(this.options.timeoutMs ?? 30_000),
495
    });
496
    const text = await response.text();
497
    let value: unknown = {};
498
    if (text) {
499
      try {
500
        value = JSON.parse(text);
501
      } catch {
502
        throw new Error(`Agent adapter returned non-JSON from ${path}`);
503
      }
504
    }
505
    if (!response.ok) {
506
      const error = value as { error?: unknown };
507
      throw new Error(
508
        typeof error.error === 'string'
509
          ? error.error
510
          : `Agent adapter ${path} returned HTTP ${response.status}`,
511
      );
512
    }
513
    return value as T;
514
  }
515
516
  async prepareSession(offer: LiveSessionOffer) {
517
    const decision = await this.post<{ accepted?: unknown; sessionId?: unknown }>(
518
      '/openclasp/session-offer',
519
      offer,
520
    );
521
    return {
522
      accepted: decision.accepted === true,
523
      ...(typeof decision.sessionId === 'string' ? { sessionId: decision.sessionId } : {}),
524
    };
525
  }
526
527
  async activateSession(session: LiveSessionActivation) {
528
    await this.post('/openclasp/session-activated', session);
529
  }
530
531
  receiveMessage(input: {
532
    session: LiveSessionActivation;
533
    requestId: string | number;
534
    message: unknown;
535
  }) {
536
    return this.post('/openclasp/message', input);
537
  }
538
}
539
540
function decodeSessionCredential(token: string) {
541
  const [payload, signature] = token.split('.');
542
  if (!payload || !signature) throw new Error('Invalid live-session credential');
543
  return {
544
    payload,
545
    signature,
546
    grant: JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as {
547
      interactionId?: unknown;
548
      senderAgentId?: unknown;
549
      recipientAgentId?: unknown;
550
      expiresAt?: unknown;
551
    },
552
  };
553
}
554
555
async function handleDirectMessage(
556
  request: Request,
557
  parsed: Record<string, unknown>,
558
  input: RuntimeHandlerInput,
559
) {
560
  const rpc = parsed as { jsonrpc?: unknown; id?: unknown; method?: unknown; params?: unknown };
561
  if (
562
    rpc.jsonrpc !== '2.0' ||
563
    (typeof rpc.id !== 'string' && typeof rpc.id !== 'number') ||
564
    rpc.method !== 'message/send'
565
  )
566
    return Response.json({ error: 'invalid_a2a_request' }, { status: 400 });
567
  const authorization = request.headers.get('authorization');
568
  if (!authorization?.startsWith('Bearer '))
569
    return Response.json({ error: 'session_credential_required' }, { status: 401 });
570
  let session: LiveSessionActivation;
571
  try {
572
    const credential = decodeSessionCredential(authorization.slice(7));
573
    if (typeof credential.grant.interactionId !== 'string')
574
      throw new Error('Invalid interaction credential');
575
    const loaded = await input.loadSession(credential.grant.interactionId);
576
    if (!loaded) return Response.json({ error: 'live_session_not_found' }, { status: 404 });
577
    session = loaded;
578
    const valid = verify(
579
      null,
580
      Buffer.from(credential.payload),
581
      createPublicKey({
582
        key: Buffer.from(session.peer.verificationKey, 'base64url'),
583
        type: 'spki',
584
        format: 'der',
585
      }),
586
      Buffer.from(credential.signature, 'base64url'),
587
    );
588
    if (
589
      !valid ||
590
      credential.grant.recipientAgentId !== input.agentId ||
591
      credential.grant.senderAgentId !== session.peer.agentId ||
592
      typeof credential.grant.expiresAt !== 'number' ||
593
      credential.grant.expiresAt <= Date.now() ||
594
      Date.parse(session.expiresAt) <= Date.now()
595
    )
596
      return Response.json({ error: 'invalid_session_credential' }, { status: 401 });
597
  } catch {
598
    return Response.json({ error: 'invalid_session_credential' }, { status: 401 });
599
  }
600
  try {
601
    const result = await input.onMessage({ session, requestId: rpc.id, message: rpc.params });
602
    return Response.json({ jsonrpc: '2.0', id: rpc.id, result });
603
  } catch (error) {
604
    return Response.json(
605
      {
606
        jsonrpc: '2.0',
607
        id: rpc.id,
608
        error: { code: -32000, message: error instanceof Error ? error.message : 'Agent failed' },
609
      },
610
      { status: 500 },
611
    );
612
  }
613
}
614
615
export async function sendOpenClaspDirectMessage(
616
  session: LiveSessionActivation,
617
  payload: unknown,
618
  requestId = crypto.randomUUID(),
619
) {
620
  const message =
621
    payload && typeof payload === 'object'
622
      ? {
623
          ...(payload as Record<string, unknown>),
624
          metadata: {
625
            ...(((payload as Record<string, unknown>).metadata as Record<string, unknown>) ?? {}),
626
            [DEFAULT_EXTENSION_URI]: {
627
              interactionId: session.interactionId,
628
              termsHash: session.contractHash,
629
              initiatorAgentId:
630
                session.role === 'initiator' ? session.agentId : session.peer.agentId,
631
              responderAgentId:
632
                session.role === 'responder' ? session.agentId : session.peer.agentId,
633
            },
634
          },
635
        }
636
      : payload;
637
  const response = await fetch(session.peer.endpoint, {
638
    method: 'POST',
639
    headers: {
640
      authorization: `Bearer ${session.peer.bearerToken}`,
641
      'content-type': 'application/json',
642
      'A2A-Extensions': DEFAULT_EXTENSION_URI,
643
    },
644
    body: JSON.stringify({
645
      jsonrpc: '2.0',
646
      id: requestId,
647
      method: 'message/send',
648
      params: { message },
649
    }),
650
  });
651
  const result = (await response.json()) as unknown;
652
  if (!response.ok) throw new Error(`Direct agent request failed with HTTP ${response.status}`);
653
  return result;
654
}
655
656
export async function reportOpenClaspSessionEvent(
657
  session: LiveSessionActivation,
658
  event: LiveSessionEvent,
659
) {
660
  const value = LiveSessionEventSchema.parse(event);
661
  if (value.interactionId !== session.interactionId || value.agentId !== session.agentId)
662
    throw new Error('Session event does not match the active session');
663
  const response = await fetch(session.reporting.endpoint, {
664
    method: 'POST',
665
    headers: {
666
      authorization: `Bearer ${session.reporting.bearerToken}`,
667
      'content-type': 'application/json',
668
    },
669
    body: JSON.stringify(value),
670
  });
671
  const result = (await response.json()) as unknown;
672
  if (!response.ok) throw new Error(`OpenClasp event report failed with HTTP ${response.status}`);
673
  return result;
674
}