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

Viewing files at commit 578449aaa53eCommit detailsView latest

connectors/botpress/src/index.ts 850 lines · 33.4 KB · typescript
1
import { createPublicKey, generateKeyPairSync, privateDecrypt, verify } from 'node:crypto';
2
import * as sdk from '@botpress/sdk';
3
import * as bp from '.botpress';
4
import { parseCheckpoint } from './checkpoint';
5
import { parseFinalizationAssessment } from './finalization';
6
import { parseAgentProfile, type AgentProfile } from './profile';
7
8
type Json = Record<string, any>;
9
type RuntimeState = {
10
  agentId: string;
11
  agentVersion: string;
12
  sessionsJson: string;
13
  offersJson: string;
14
  finalizationsJson: string;
15
  accessToken: string;
16
  setupJson: string;
17
};
18
19
const EXTENSION_URI = 'https://openclasp.dev/extensions/trust/v0.1';
20
const normalizeUrl = (value: string) => value.replace(/\/$/, '');
21
const platformUrl = (ctx: bp.Context) => ctx.configuration.openClaspUrl ?? 'https://openclasp.dev';
22
const botpressWebhookEndpoint = (webhookId: string) =>
23
  `https://webhook.botpress.cloud/${webhookId}`;
24
const jsonResponse = (status: number, value: unknown) => ({
25
  status,
26
  headers: { 'content-type': 'application/json' },
27
  body: JSON.stringify(value),
28
});
29
const parseRecord = (value: string): Record<string, Json> => {
30
  const parsed = JSON.parse(value) as unknown;
31
  return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
32
    ? (parsed as Record<string, Json>)
33
    : {};
34
};
35
36
const openClaspRequest = async <T>(
37
  url: string,
38
  token: string,
39
  path: string,
40
  init: RequestInit = {},
41
): Promise<T> => {
42
  const response = await fetch(`${normalizeUrl(url)}${path}`, {
43
    ...init,
44
    headers: {
45
      authorization: `Bearer ${token}`,
46
      ...(init.body ? { 'content-type': 'application/json' } : {}),
47
      ...init.headers,
48
    },
49
  });
50
  const body = (await response.json()) as Json;
51
  if (!response.ok)
52
    throw new sdk.RuntimeError(String(body.error ?? `OpenClasp HTTP ${response.status}`));
53
  return body as T;
54
};
55
56
const getState = async (client: bp.Client, integrationId: string): Promise<RuntimeState> => {
57
  const { state } = await client.getOrSetState({
58
    type: 'integration',
59
    id: integrationId,
60
    name: 'runtime',
61
    payload: {
62
      agentId: '',
63
      agentVersion: '1.0.0',
64
      sessionsJson: '{}',
65
      offersJson: '{}',
66
      finalizationsJson: '{}',
67
      accessToken: '',
68
      setupJson: '{}',
69
    },
70
  });
71
  return {
72
    agentId: state.payload.agentId ?? '',
73
    agentVersion: state.payload.agentVersion ?? '1.0.0',
74
    sessionsJson: state.payload.sessionsJson ?? '{}',
75
    offersJson: state.payload.offersJson ?? '{}',
76
    finalizationsJson: state.payload.finalizationsJson ?? '{}',
77
    accessToken: state.payload.accessToken ?? '',
78
    setupJson: state.payload.setupJson ?? '{}',
79
  };
80
};
81
const setState = async (client: bp.Client, integrationId: string, value: RuntimeState) => {
82
  await client.setState({
83
    type: 'integration',
84
    id: integrationId,
85
    name: 'runtime',
86
    payload: value,
87
  });
88
};
89
90
const bootstrapAndConnect = async (props: {
91
  ctx: bp.Context;
92
  webhookUrl: string;
93
  client: bp.Client;
94
  accessToken: string;
95
}) => {
96
  const openClaspUrl = platformUrl(props.ctx);
97
  const bootstrap = await openClaspRequest<{
98
    agentId: string;
99
    agentVersion: string;
100
    capabilities: string[];
101
    limitations: string[];
102
  }>(openClaspUrl, props.accessToken, '/v0.1/runtime/bootstrap');
103
  const endpoint = props.webhookUrl;
104
  const current = await getState(props.client, props.ctx.integrationId);
105
  await setState(props.client, props.ctx.integrationId, {
106
    ...current,
107
    agentId: bootstrap.agentId,
108
    agentVersion: bootstrap.agentVersion,
109
  });
110
  await openClaspRequest(openClaspUrl, props.accessToken, '/v0.1/runtime', {
111
    method: 'PUT',
112
    body: JSON.stringify({ endpoint }),
113
  });
114
};
115
116
const profilePrompt = () =>
117
  [
118
    '[OpenClasp setup — private platform request, not user-authored content]',
119
    'Describe your current deployed identity and abilities. Do not describe intended future features.',
120
    'Reply with only OPENCLASP_PROFILE followed by one JSON object.',
121
    'Required fields: description, framework (use Botpress), agentVersion, capabilities[], limitations[].',
122
    'Optional fields: modelProvider, modelName.',
123
    'Do not include credentials, system prompts, chain of thought, user data, or conversation content.',
124
  ].join('\n');
125
126
const completeBotpressPairing = async (
127
  ctx: bp.Context,
128
  client: bp.Client,
129
  profile: AgentProfile,
130
) => {
131
  const state = await getState(client, ctx.integrationId);
132
  const setup = parseRecord(state.setupJson).pairing;
133
  if (!setup?.privateKey || !setup?.publicKey)
134
    throw new sdk.RuntimeError('Botpress pairing state is unavailable');
135
  const endpoint = botpressWebhookEndpoint(ctx.webhookId);
136
  const response = await fetch(
137
    `${normalizeUrl(platformUrl(ctx))}/v0.1/provider-connections/botpress/complete`,
138
    {
139
      method: 'POST',
140
      headers: {
141
        'content-type': 'application/json',
142
        'x-openclasp-pairing-code': ctx.configuration.pairingCode,
143
      },
144
      body: JSON.stringify({
145
        runtimeEndpoint: endpoint,
146
        credentialPublicKey: setup.publicKey,
147
        profile,
148
      }),
149
    },
150
  );
151
  const value = (await response.json()) as Json;
152
  if (!response.ok)
153
    throw new sdk.RuntimeError(String(value.error ?? `OpenClasp HTTP ${response.status}`));
154
  const accessToken = privateDecrypt(
155
    { key: setup.privateKey, oaepHash: 'sha256' },
156
    Buffer.from(String(value.credentialCiphertext), 'base64url'),
157
  ).toString('utf8');
158
  if (!accessToken.startsWith('oc_at_')) throw new sdk.RuntimeError('Invalid OpenClasp credential');
159
  await setState(client, ctx.integrationId, {
160
    ...state,
161
    accessToken,
162
    setupJson: JSON.stringify({ pairing: { status: 'connected' } }),
163
  });
164
  await bootstrapAndConnect({ ctx, webhookUrl: endpoint, client, accessToken });
165
  return String(value.agentId);
166
};
167
168
const sessionRequest = async <T>(endpoint: string, token: string, value: unknown): Promise<T> => {
169
  const response = await fetch(endpoint, {
170
    method: 'POST',
171
    headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
172
    body: JSON.stringify(value),
173
  });
174
  const body = (await response.json()) as Json;
175
  if (!response.ok)
176
    throw new sdk.RuntimeError(String(body.error ?? `OpenClasp HTTP ${response.status}`));
177
  return body as T;
178
};
179
180
let keyCache: { url: string; key: string } | undefined;
181
const verificationKey = async (openClaspUrl: string) => {
182
  if (keyCache?.url === openClaspUrl) return keyCache.key;
183
  const response = await fetch(`${normalizeUrl(openClaspUrl)}/.well-known/openclasp-session-key`);
184
  if (!response.ok) throw new Error('OpenClasp verification key is unavailable');
185
  const body = (await response.json()) as Json;
186
  if (typeof body.publicKey !== 'string') throw new Error('Invalid OpenClasp verification key');
187
  keyCache = { url: openClaspUrl, key: body.publicKey };
188
  return body.publicKey;
189
};
190
const validControlSignature = async (
191
  openClaspUrl: string,
192
  requestId: string,
193
  timestamp: string,
194
  body: string,
195
  signature: string,
196
) => {
197
  if (Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60_000) return false;
198
  return verify(
199
    null,
200
    Buffer.from(`${timestamp}.${requestId}.${body}`),
201
    createPublicKey({
202
      key: Buffer.from(await verificationKey(openClaspUrl), 'base64url'),
203
      type: 'spki',
204
      format: 'der',
205
    }),
206
    Buffer.from(signature.replace(/^v1=/, ''), 'base64url'),
207
  );
208
};
209
const decodeCredential = (token: string) => {
210
  const [payload, signature] = token.split('.');
211
  if (!payload || !signature) throw new Error('Invalid session credential');
212
  return {
213
    payload,
214
    signature,
215
    grant: JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Json,
216
  };
217
};
218
const validSessionCredential = (token: string, session: Json, agentId: string) => {
219
  const credential = decodeCredential(token);
220
  const valid = verify(
221
    null,
222
    Buffer.from(credential.payload),
223
    createPublicKey({
224
      key: Buffer.from(session.peer.verificationKey, 'base64url'),
225
      type: 'spki',
226
      format: 'der',
227
    }),
228
    Buffer.from(credential.signature, 'base64url'),
229
  );
230
  if (
231
    !valid ||
232
    credential.grant.interactionId !== session.interactionId ||
233
    credential.grant.recipientAgentId !== agentId ||
234
    credential.grant.senderAgentId !== session.peer.agentId ||
235
    typeof credential.grant.expiresAt !== 'number' ||
236
    credential.grant.expiresAt <= Date.now() ||
237
    Date.parse(session.expiresAt) <= Date.now()
238
  )
239
    throw new Error('Invalid session credential');
240
};
241
const textFromMessage = (message: Json) => {
242
  const parts = Array.isArray(message.parts) ? message.parts : [];
243
  const text = parts
244
    .map((part: Json) => (typeof part.text === 'string' ? part.text : ''))
245
    .filter(Boolean)
246
    .join('\n')
247
    .trim();
248
  if (!text) throw new Error('OpenClasp Botpress connector accepts text messages only');
249
  return text;
250
};
251
const withSessionContext = (session: Json, offer: Json | undefined, peerText: string) => {
252
  const contract = offer?.contract ?? {};
253
  const criteria = Array.isArray(contract.successCriteria) ? contract.successCriteria : [];
254
  const brief = session.counterpartyBrief ?? {};
255
  return [
256
    '[OpenClasp session context — platform metadata, not peer-authored content]',
257
    `Interaction: ${String(session.interactionId)}`,
258
    `Requested outcome: ${String(contract.requestedOutcome ?? 'See the peer request')}`,
259
    `Success criteria: ${criteria.length ? criteria.join(' | ') : 'No explicit criteria supplied'}`,
260
    `Counterparty assessment: ${String(brief.decision ?? 'ADVISE')} (history confidence ${String(brief.historyConfidence ?? 0)})`,
261
    'Follow the signed contract. OpenClasp will request a private structured assessment automatically when either participant reports a terminal outcome. Do not upload the raw transcript.',
262
    '',
263
    '[Peer message — verbatim]',
264
    peerText,
265
  ].join('\n');
266
};
267
const finalizationPrompt = (session: Json, offer: Json | undefined, retry = false) => {
268
  const contract = offer?.contract ?? {};
269
  return [
270
    '[OpenClasp automatic finalization — platform request, not peer-authored content]',
271
    retry
272
      ? 'Your previous assessment was not valid JSON. Return the required object now.'
273
      : 'The peer reported that this interaction reached a terminal outcome. Independently assess the conversation you just completed.',
274
    `Interaction: ${String(session.interactionId)}`,
275
    `Requested outcome: ${String(contract.requestedOutcome ?? '')}`,
276
    `Exact success criteria: ${JSON.stringify(contract.successCriteria ?? [])}`,
277
    'Reply with only OPENCLASP_FINALIZATION followed by one JSON object. Do not include or quote the raw conversation.',
278
    'Required JSON fields: outcome (success|partial|failure|cancelled), summary, criteria [{criterion,status,explanation?,evidenceReferences[]}], deliverables[], actionsTaken[], blockers[], corrections[], evidenceReferences[], confidence (0..1), ratings {overall_satisfaction,outcome_satisfaction,communication,timeliness,scope_adherence,evidence_quality,correction_handling,reliability} (each 0..1), wouldWorkAgain (yes|no|unsure), reasonCodes[], privateComment?.',
279
  ].join('\n');
280
};
281
const checkpointPrompt = (offer: Json | undefined, retry = false) => {
282
  const contract = offer?.contract ?? {};
283
  return [
284
    '[OpenClasp progress checkpoint — private platform request, not peer-authored content]',
285
    retry
286
      ? 'Your previous checkpoint was invalid. Return the required JSON object now.'
287
      : 'Briefly assess progress without repeating or quoting the conversation.',
288
    `Requested outcome: ${String(contract.requestedOutcome ?? '')}`,
289
    `Success criteria: ${JSON.stringify(contract.successCriteria ?? [])}`,
290
    'Reply with only OPENCLASP_CHECKPOINT followed by one compact JSON object.',
291
    'Required fields: state (active|blocked|ready_to_finalize|done|cancelled), progress (0..1), criteriaMet[], criteriaRemaining[], blockerCodes[], topicStatus (in_scope|drifting|changed), expectedRemainingTurns?, needsHuman, confidence (0..1).',
292
  ].join('\n');
293
};
294
const createIncomingMessage = async (
295
  client: bp.Client,
296
  interactionId: string,
297
  peerAgentId: string,
298
  text: string,
299
) => {
300
  const { conversation } = await client.getOrCreateConversation({
301
    channel: 'a2a',
302
    tags: { interactionId },
303
  });
304
  const { user } = await client.getOrCreateUser({ tags: { agentId: peerAgentId } });
305
  const { message } = await client.createMessage({
306
    type: 'text',
307
    conversationId: conversation.id,
308
    userId: user.id,
309
    payload: { text },
310
    tags: {},
311
  });
312
  return message;
313
};
314
const heartbeat = async (ctx: bp.Context, client: bp.Client) => {
315
  const state = await getState(client, ctx.integrationId);
316
  if (!state.accessToken) return;
317
  await openClaspRequest(platformUrl(ctx), state.accessToken, '/v0.1/runtime/heartbeat', {
318
    method: 'POST',
319
  });
320
};
321
322
const completeInteraction = async (ctx: bp.Context, client: bp.Client, input: Json) => {
323
  const state = await getState(client, ctx.integrationId);
324
  const sessions = parseRecord(state.sessionsJson);
325
  const finalizations = parseRecord(state.finalizationsJson);
326
  const unfinished = Object.keys(sessions).filter(
327
    (id) =>
328
      finalizations[id]?.status !== 'completed' &&
329
      Date.parse(String(sessions[id]?.expiresAt ?? 0)) > Date.now(),
330
  );
331
  const interactionId = input.interactionId ?? (unfinished.length === 1 ? unfinished[0] : '');
332
  if (!interactionId)
333
    throw new sdk.RuntimeError(
334
      'Specify interactionId because there is not exactly one unfinished OpenClasp session',
335
    );
336
  const session = sessions[interactionId];
337
  if (!session) throw new sdk.RuntimeError('OpenClasp live session is unavailable');
338
  if (finalizations[interactionId]?.status === 'completed') {
339
    return {
340
      interactionId,
341
      status: 'already_completed' as const,
342
      feedbackRevealed: Boolean(finalizations[interactionId]?.feedbackRevealed),
343
    };
344
  }
345
  const offer = parseRecord(state.offersJson)[interactionId]?.offer;
346
  const contract = offer?.contract;
347
  if (!contract || !Array.isArray(contract.successCriteria))
348
    throw new sdk.RuntimeError('Signed OpenClasp contract is unavailable');
349
350
  const existing = finalizations[interactionId] ?? {};
351
  const assessment = existing.assessment ?? input;
352
  const suppliedCriteria = Array.isArray(assessment.criteria) ? assessment.criteria : [];
353
  const report =
354
    existing.report ??
355
    ({
356
      reportId: crypto.randomUUID(),
357
      interactionId,
358
      contractHash: session.contractHash,
359
      reportingAgentId: state.agentId,
360
      counterpartyAgentId: session.peer.agentId,
361
      agentVersion: state.agentVersion,
362
      outcome: assessment.outcome,
363
      summary: assessment.summary,
364
      requestedOutcome: contract.requestedOutcome,
365
      criteria: contract.successCriteria.map((criterion: string) => {
366
        const supplied = suppliedCriteria.find((item: Json) => item.criterion === criterion);
367
        return supplied ?? { criterion, status: 'unknown', evidenceReferences: [] };
368
      }),
369
      deliverables: assessment.deliverables ?? [],
370
      actionsTaken: assessment.actionsTaken ?? [],
371
      blockers: assessment.blockers ?? [],
372
      scopeChanges: [],
373
      corrections: assessment.corrections ?? [],
374
      evidenceReferences: assessment.evidenceReferences ?? [],
375
      startedAt: session.activatedAt,
376
      completedAt: new Date().toISOString(),
377
      confidence: assessment.confidence,
378
      dataSharingMode: 'structured_only',
379
      submissionMethod: 'runtime_session',
380
    } as Json);
381
  finalizations[interactionId] = { status: 'report_pending', assessment, report };
382
  await setState(client, ctx.integrationId, {
383
    ...state,
384
    finalizationsJson: JSON.stringify(finalizations),
385
  });
386
  const completion = await sessionRequest<{ feedbackRequest: Json }>(
387
    session.reporting.completionEndpoint,
388
    session.reporting.bearerToken,
389
    report,
390
  );
391
  const request = completion.feedbackRequest;
392
  if (!request?.requestId)
393
    throw new sdk.RuntimeError('OpenClasp did not return a feedback request');
394
  const feedback =
395
    existing.feedback ??
396
    ({
397
      feedbackId: crypto.randomUUID(),
398
      requestId: request.requestId,
399
      interactionId,
400
      reviewerAgentId: state.agentId,
401
      subjectAgentId: session.peer.agentId,
402
      reviewerAgentVersion: state.agentVersion,
403
      ratings: assessment.ratings,
404
      wouldWorkAgain: assessment.wouldWorkAgain,
405
      reasonCodes: assessment.reasonCodes ?? [],
406
      ...(assessment.privateComment ? { privateComment: assessment.privateComment } : {}),
407
      evidenceReferences: assessment.evidenceReferences ?? [],
408
      confidence: assessment.confidence,
409
      submittedAt: new Date().toISOString(),
410
      submissionMethod: 'runtime_session',
411
    } as Json);
412
  if (request.status === 'submitted') {
413
    finalizations[interactionId] = {
414
      status: 'completed',
415
      assessment,
416
      report,
417
      feedback,
418
      feedbackRevealed: false,
419
    };
420
    await setState(client, ctx.integrationId, {
421
      ...state,
422
      finalizationsJson: JSON.stringify(finalizations),
423
    });
424
    return { interactionId, status: 'completed' as const, feedbackRevealed: false };
425
  }
426
  finalizations[interactionId] = {
427
    status: 'feedback_pending',
428
    assessment,
429
    report,
430
    feedback,
431
  };
432
  await setState(client, ctx.integrationId, {
433
    ...state,
434
    finalizationsJson: JSON.stringify(finalizations),
435
  });
436
  const result = await sessionRequest<{ revealed: boolean }>(
437
    session.reporting.feedbackEndpoint,
438
    session.reporting.bearerToken,
439
    feedback,
440
  );
441
  finalizations[interactionId] = {
442
    status: 'completed',
443
    assessment,
444
    report,
445
    feedback,
446
    feedbackRevealed: result.revealed,
447
  };
448
  await setState(client, ctx.integrationId, {
449
    ...state,
450
    finalizationsJson: JSON.stringify(finalizations),
451
  });
452
  await heartbeat(ctx, client);
453
  return {
454
    interactionId,
455
    status: 'completed' as const,
456
    feedbackRevealed: result.revealed,
457
  };
458
};
459
460
export default new bp.Integration({
461
  register: async ({ ctx, webhookUrl, client }) => {
462
    const state = await getState(client, ctx.integrationId);
463
    if (state.accessToken) {
464
      await bootstrapAndConnect({ ctx, webhookUrl, client, accessToken: state.accessToken });
465
      return;
466
    }
467
    const { publicKey, privateKey } = generateKeyPairSync('rsa', {
468
      modulusLength: 2048,
469
      publicKeyEncoding: { type: 'spki', format: 'pem' },
470
      privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
471
    });
472
    await setState(client, ctx.integrationId, {
473
      ...state,
474
      setupJson: JSON.stringify({
475
        pairing: { status: 'profile_requested', publicKey, privateKey },
476
      }),
477
    });
478
    await createIncomingMessage(
479
      client,
480
      `openclasp-setup:${ctx.integrationId}`,
481
      'openclasp-platform',
482
      profilePrompt(),
483
    );
484
  },
485
  unregister: async ({ ctx, client }) => {
486
    const state = await getState(client, ctx.integrationId);
487
    if (!state.accessToken) return;
488
    await openClaspRequest(platformUrl(ctx), state.accessToken, '/v0.1/runtime', {
489
      method: 'DELETE',
490
    });
491
  },
492
  actions: {
493
    completeInteraction: async ({ ctx, client, input }) => completeInteraction(ctx, client, input),
494
  },
495
  channels: {
496
    a2a: {
497
      messages: {
498
        text: async ({ ctx, client, conversation, payload }) => {
499
          const interactionId = conversation.tags.interactionId;
500
          if (!interactionId) throw new sdk.RuntimeError('Missing OpenClasp interaction ID');
501
          if (interactionId === `openclasp-setup:${ctx.integrationId}`) {
502
            const profile = parseAgentProfile(payload.text);
503
            await completeBotpressPairing(ctx, client, profile);
504
            return;
505
          }
506
          const state = await getState(client, ctx.integrationId);
507
          if (!state.accessToken) throw new sdk.RuntimeError('OpenClasp pairing is not complete');
508
          const sessions = parseRecord(state.sessionsJson);
509
          const session = sessions[interactionId];
510
          if (!session) throw new sdk.RuntimeError('OpenClasp live session is unavailable');
511
          if (session.finalizationRequested) {
512
            try {
513
              const assessment = parseFinalizationAssessment(payload.text, interactionId);
514
              await completeInteraction(ctx, client, assessment);
515
              const latest = await getState(client, ctx.integrationId);
516
              const latestSessions = parseRecord(latest.sessionsJson);
517
              latestSessions[interactionId] = {
518
                ...latestSessions[interactionId],
519
                finalizationRequested: false,
520
                finalizationCompleted: true,
521
              };
522
              await setState(client, ctx.integrationId, {
523
                ...latest,
524
                sessionsJson: JSON.stringify(latestSessions),
525
              });
526
              return;
527
            } catch {
528
              const attempts = Number(session.finalizationAttempts ?? 0) + 1;
529
              sessions[interactionId] = {
530
                ...session,
531
                finalizationRequested: attempts < 3,
532
                finalizationAttempts: attempts,
533
              };
534
              await setState(client, ctx.integrationId, {
535
                ...state,
536
                sessionsJson: JSON.stringify(sessions),
537
              });
538
              if (attempts < 3) {
539
                const offer = parseRecord(state.offersJson)[interactionId]?.offer;
540
                await createIncomingMessage(
541
                  client,
542
                  interactionId,
543
                  session.peer.agentId,
544
                  finalizationPrompt(session, offer, true),
545
                );
546
              }
547
              return;
548
            }
549
          }
550
          if (session.checkpointRequested) {
551
            try {
552
              const checkpoint = parseCheckpoint(payload.text);
553
              await sessionRequest(session.reporting.endpoint, session.reporting.bearerToken, {
554
                eventId: crypto.randomUUID(),
555
                interactionId,
556
                agentId: state.agentId,
557
                sequence: Date.now(),
558
                type: 'progress_checkpoint',
559
                occurredAt: new Date().toISOString(),
560
                evidenceReferences: [],
561
                checkpoint,
562
                details: {
563
                  labels: [`state:${checkpoint.state}`, `topic:${checkpoint.topicStatus}`],
564
                  metrics: { progress: checkpoint.progress, confidence: checkpoint.confidence },
565
                  flags: { needsHuman: checkpoint.needsHuman },
566
                },
567
              });
568
              sessions[interactionId] = {
569
                ...session,
570
                checkpointRequested: false,
571
                checkpointAttempts: 0,
572
                lastCheckpointExchange: Number(session.exchangeCount ?? 0),
573
                lastCheckpointAt: new Date().toISOString(),
574
                latestCheckpoint: checkpoint,
575
              };
576
              await setState(client, ctx.integrationId, {
577
                ...state,
578
                sessionsJson: JSON.stringify(sessions),
579
              });
580
              if (checkpoint.state === 'done' || checkpoint.state === 'ready_to_finalize') {
581
                const latest = await getState(client, ctx.integrationId);
582
                const latestSessions = parseRecord(latest.sessionsJson);
583
                latestSessions[interactionId] = {
584
                  ...latestSessions[interactionId],
585
                  finalizationRequested: true,
586
                  finalizationAttempts: 0,
587
                };
588
                await setState(client, ctx.integrationId, {
589
                  ...latest,
590
                  sessionsJson: JSON.stringify(latestSessions),
591
                });
592
                const offer = parseRecord(latest.offersJson)[interactionId]?.offer;
593
                await createIncomingMessage(
594
                  client,
595
                  interactionId,
596
                  session.peer.agentId,
597
                  finalizationPrompt(session, offer),
598
                );
599
              }
600
              return;
601
            } catch {
602
              const attempts = Number(session.checkpointAttempts ?? 0) + 1;
603
              sessions[interactionId] = {
604
                ...session,
605
                checkpointRequested: attempts < 3,
606
                checkpointAttempts: attempts,
607
              };
608
              await setState(client, ctx.integrationId, {
609
                ...state,
610
                sessionsJson: JSON.stringify(sessions),
611
              });
612
              if (attempts < 3) {
613
                const offer = parseRecord(state.offersJson)[interactionId]?.offer;
614
                await createIncomingMessage(
615
                  client,
616
                  interactionId,
617
                  session.peer.agentId,
618
                  checkpointPrompt(offer, true),
619
                );
620
              }
621
              return;
622
            }
623
          }
624
          const response = await fetch(session.peer.endpoint, {
625
            method: 'POST',
626
            headers: {
627
              authorization: `Bearer ${session.peer.bearerToken}`,
628
              'content-type': 'application/json',
629
              'A2A-Extensions': EXTENSION_URI,
630
            },
631
            body: JSON.stringify({
632
              jsonrpc: '2.0',
633
              id: crypto.randomUUID(),
634
              method: 'message/send',
635
              params: {
636
                message: {
637
                  role: 'agent',
638
                  parts: [{ kind: 'text', text: payload.text }],
639
                  metadata: {
640
                    [EXTENSION_URI]: {
641
                      interactionId,
642
                      termsHash: session.contractHash,
643
                      initiatorAgentId:
644
                        session.role === 'initiator' ? session.agentId : session.peer.agentId,
645
                      responderAgentId:
646
                        session.role === 'responder' ? session.agentId : session.peer.agentId,
647
                    },
648
                  },
649
                },
650
              },
651
            }),
652
          });
653
          if (!response.ok)
654
            throw new sdk.RuntimeError(`Peer A2A endpoint returned HTTP ${response.status}`);
655
          const exchangeCount = Number(session.exchangeCount ?? 0) + 1;
656
          const shouldCheckpoint = exchangeCount - Number(session.lastCheckpointExchange ?? 0) >= 5;
657
          sessions[interactionId] = {
658
            ...session,
659
            exchangeCount,
660
            ...(shouldCheckpoint ? { checkpointRequested: true, checkpointAttempts: 0 } : {}),
661
          };
662
          await setState(client, ctx.integrationId, {
663
            ...state,
664
            sessionsJson: JSON.stringify(sessions),
665
          });
666
          if (shouldCheckpoint) {
667
            const offer = parseRecord(state.offersJson)[interactionId]?.offer;
668
            await createIncomingMessage(
669
              client,
670
              interactionId,
671
              session.peer.agentId,
672
              checkpointPrompt(offer),
673
            );
674
          }
675
          await heartbeat(ctx, client);
676
        },
677
      },
678
    },
679
  },
680
  handler: async ({ ctx, client, req }) => {
681
    if (req.method !== 'POST' || !req.body)
682
      return jsonResponse(405, { error: 'method_not_allowed' });
683
    let body: Json;
684
    try {
685
      body = JSON.parse(req.body) as Json;
686
    } catch {
687
      return jsonResponse(400, { error: 'invalid_json' });
688
    }
689
    const state = await getState(client, ctx.integrationId);
690
    if (body.type === 'openclasp.runtime.verify') {
691
      if (body.agentId !== state.agentId) return jsonResponse(403, { error: 'wrong_agent' });
692
      return jsonResponse(200, {
693
        type: 'openclasp.runtime.verified',
694
        version: '1',
695
        agentId: state.agentId,
696
        challenge: body.challenge,
697
        a2aEndpoint: botpressWebhookEndpoint(ctx.webhookId),
698
      });
699
    }
700
    if (
701
      body.type === 'openclasp.session.offer' ||
702
      body.type === 'openclasp.session.activation' ||
703
      body.type === 'openclasp.session.finalization_request'
704
    ) {
705
      const requestId = req.headers['openclasp-request-id'] ?? '';
706
      const timestamp = req.headers['openclasp-timestamp'] ?? '';
707
      const signature = req.headers['openclasp-signature'] ?? '';
708
      if (
709
        !requestId ||
710
        !timestamp ||
711
        !signature ||
712
        !(await validControlSignature(platformUrl(ctx), requestId, timestamp, req.body, signature))
713
      )
714
        return jsonResponse(401, { error: 'invalid_openclasp_signature' });
715
      if (body.agentId !== state.agentId) return jsonResponse(403, { error: 'wrong_agent' });
716
      if (body.type === 'openclasp.session.finalization_request') {
717
        const sessions = parseRecord(state.sessionsJson);
718
        const session = sessions[body.interactionId];
719
        if (
720
          body.requestId !== requestId ||
721
          typeof body.interactionId !== 'string' ||
722
          !session ||
723
          body.contractHash !== session.contractHash ||
724
          body.peerAgentId !== session.peer.agentId
725
        )
726
          return jsonResponse(400, { error: 'invalid_finalization_request' });
727
        if (parseRecord(state.finalizationsJson)[body.interactionId]?.status === 'completed')
728
          return jsonResponse(200, { accepted: true, alreadyCompleted: true });
729
        if (session.finalizationRequested)
730
          return jsonResponse(200, { accepted: true, alreadyRequested: true });
731
        sessions[body.interactionId] = {
732
          ...session,
733
          finalizationRequested: true,
734
          finalizationAttempts: 0,
735
        };
736
        await setState(client, ctx.integrationId, {
737
          ...state,
738
          sessionsJson: JSON.stringify(sessions),
739
        });
740
        const offer = parseRecord(state.offersJson)[body.interactionId]?.offer;
741
        await createIncomingMessage(
742
          client,
743
          body.interactionId,
744
          session.peer.agentId,
745
          finalizationPrompt(session, offer),
746
        );
747
        return jsonResponse(202, { accepted: true });
748
      }
749
      if (body.type === 'openclasp.session.offer') {
750
        const offers = parseRecord(state.offersJson);
751
        if (
752
          body.offerId !== requestId ||
753
          typeof body.interactionId !== 'string' ||
754
          typeof body.expiresAt !== 'string' ||
755
          Date.parse(body.expiresAt) <= Date.now()
756
        )
757
          return jsonResponse(400, { error: 'invalid_session_offer' });
758
        const previous = offers[body.interactionId];
759
        if (previous && previous.offer?.offerId !== body.offerId)
760
          return jsonResponse(409, { error: 'interaction_offer_conflict' });
761
        const sessionId = previous?.sessionId ?? crypto.randomUUID();
762
        offers[body.interactionId] = { offer: body, sessionId };
763
        await setState(client, ctx.integrationId, {
764
          ...state,
765
          offersJson: JSON.stringify(offers),
766
        });
767
        return jsonResponse(200, {
768
          type: 'openclasp.session.accepted',
769
          version: '1',
770
          offerId: body.offerId,
771
          interactionId: body.interactionId,
772
          agentId: state.agentId,
773
          sessionId,
774
          a2aEndpoint: botpressWebhookEndpoint(ctx.webhookId),
775
          expiresAt: body.expiresAt,
776
        });
777
      }
778
      const sessions = parseRecord(state.sessionsJson);
779
      if (
780
        body.activationId !== requestId ||
781
        typeof body.interactionId !== 'string' ||
782
        typeof body.expiresAt !== 'string' ||
783
        Date.parse(body.expiresAt) <= Date.now()
784
      )
785
        return jsonResponse(400, { error: 'invalid_session_activation' });
786
      if (sessions[body.interactionId]?.activationId === body.activationId)
787
        return jsonResponse(200, { accepted: true, activationId: body.activationId });
788
      sessions[body.interactionId] = body;
789
      await setState(client, ctx.integrationId, {
790
        ...state,
791
        sessionsJson: JSON.stringify(sessions),
792
      });
793
      if (body.role === 'initiator') {
794
        const offer = parseRecord(state.offersJson)[body.interactionId]?.offer;
795
        await createIncomingMessage(
796
          client,
797
          body.interactionId,
798
          body.peer.agentId,
799
          `Start this accepted agent-to-agent task now. Contract: ${JSON.stringify(offer?.contract ?? {})}\nOpenClasp will request a private structured assessment automatically when either participant reports a terminal outcome. Do not upload the raw transcript.`,
800
        );
801
      }
802
      return jsonResponse(200, { accepted: true, activationId: body.activationId });
803
    }
804
    if (body.jsonrpc !== '2.0' || body.method !== 'message/send' || body.id === undefined)
805
      return jsonResponse(400, { error: 'invalid_a2a_request' });
806
    const authorization = req.headers.authorization ?? '';
807
    if (!authorization.startsWith('Bearer '))
808
      return jsonResponse(401, { error: 'session_credential_required' });
809
    try {
810
      const credential = decodeCredential(authorization.slice(7));
811
      const interactionId = credential.grant.interactionId;
812
      if (typeof interactionId !== 'string') throw new Error('Invalid session credential');
813
      const sessions = parseRecord(state.sessionsJson);
814
      let session = sessions[interactionId];
815
      if (!session) return jsonResponse(404, { error: 'live_session_not_found' });
816
      validSessionCredential(authorization.slice(7), session, state.agentId);
817
      const peerText = textFromMessage(body.params?.message ?? {});
818
      let deliveredText = peerText;
819
      if (!session.contextDelivered) {
820
        deliveredText = withSessionContext(
821
          session,
822
          parseRecord(state.offersJson)[interactionId]?.offer,
823
          peerText,
824
        );
825
        session = { ...session, contextDelivered: true };
826
        sessions[interactionId] = session;
827
        await setState(client, ctx.integrationId, {
828
          ...state,
829
          sessionsJson: JSON.stringify(sessions),
830
        });
831
      }
832
      const message = await createIncomingMessage(
833
        client,
834
        interactionId,
835
        session.peer.agentId,
836
        deliveredText,
837
      );
838
      await heartbeat(ctx, client);
839
      return jsonResponse(200, {
840
        jsonrpc: '2.0',
841
        id: body.id,
842
        result: { task: { id: message.id, state: 'submitted' } },
843
      });
844
    } catch (error) {
845
      return jsonResponse(401, {
846
        error: error instanceof Error ? error.message : 'invalid_session_credential',
847
      });
848
    }
849
  },
850
});