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

Viewing files at commit 5dab768c7927Commit detailsView latest

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