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

Viewing files at commit 1efaca5a5f5eCommit detailsView latest

connectors/botpress/src/index.ts 871 lines · 34.1 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 deleteSetupConversations = async (client: bp.Client, integrationId: string) => {
315
  let nextToken: string | undefined;
316
  do {
317
    const page = await client.listConversations({
318
      channel: 'a2a',
319
      tags: { interactionId: `openclasp-setup:${integrationId}` },
320
      pageSize: 100,
321
      ...(nextToken ? { nextToken } : {}),
322
    });
323
    await Promise.all(
324
      page.conversations.map((conversation) => client.deleteConversation({ id: conversation.id })),
325
    );
326
    nextToken = page.meta.nextToken;
327
  } while (nextToken);
328
};
329
const heartbeat = async (ctx: bp.Context, client: bp.Client) => {
330
  const state = await getState(client, ctx.integrationId);
331
  if (!state.accessToken) return;
332
  await openClaspRequest(platformUrl(ctx), state.accessToken, '/v0.1/runtime/heartbeat', {
333
    method: 'POST',
334
  });
335
};
336
337
const completeInteraction = async (ctx: bp.Context, client: bp.Client, input: Json) => {
338
  const state = await getState(client, ctx.integrationId);
339
  const sessions = parseRecord(state.sessionsJson);
340
  const finalizations = parseRecord(state.finalizationsJson);
341
  const unfinished = Object.keys(sessions).filter(
342
    (id) =>
343
      finalizations[id]?.status !== 'completed' &&
344
      Date.parse(String(sessions[id]?.expiresAt ?? 0)) > Date.now(),
345
  );
346
  const interactionId = input.interactionId ?? (unfinished.length === 1 ? unfinished[0] : '');
347
  if (!interactionId)
348
    throw new sdk.RuntimeError(
349
      'Specify interactionId because there is not exactly one unfinished OpenClasp session',
350
    );
351
  const session = sessions[interactionId];
352
  if (!session) throw new sdk.RuntimeError('OpenClasp live session is unavailable');
353
  if (finalizations[interactionId]?.status === 'completed') {
354
    return {
355
      interactionId,
356
      status: 'already_completed' as const,
357
      feedbackRevealed: Boolean(finalizations[interactionId]?.feedbackRevealed),
358
    };
359
  }
360
  const offer = parseRecord(state.offersJson)[interactionId]?.offer;
361
  const contract = offer?.contract;
362
  if (!contract || !Array.isArray(contract.successCriteria))
363
    throw new sdk.RuntimeError('Signed OpenClasp contract is unavailable');
364
365
  const existing = finalizations[interactionId] ?? {};
366
  const assessment = existing.assessment ?? input;
367
  const suppliedCriteria = Array.isArray(assessment.criteria) ? assessment.criteria : [];
368
  const report =
369
    existing.report ??
370
    ({
371
      reportId: crypto.randomUUID(),
372
      interactionId,
373
      contractHash: session.contractHash,
374
      reportingAgentId: state.agentId,
375
      counterpartyAgentId: session.peer.agentId,
376
      agentVersion: state.agentVersion,
377
      outcome: assessment.outcome,
378
      summary: assessment.summary,
379
      requestedOutcome: contract.requestedOutcome,
380
      criteria: contract.successCriteria.map((criterion: string) => {
381
        const supplied = suppliedCriteria.find((item: Json) => item.criterion === criterion);
382
        return supplied ?? { criterion, status: 'unknown', evidenceReferences: [] };
383
      }),
384
      deliverables: assessment.deliverables ?? [],
385
      actionsTaken: assessment.actionsTaken ?? [],
386
      blockers: assessment.blockers ?? [],
387
      scopeChanges: [],
388
      corrections: assessment.corrections ?? [],
389
      evidenceReferences: assessment.evidenceReferences ?? [],
390
      startedAt: session.activatedAt,
391
      completedAt: new Date().toISOString(),
392
      confidence: assessment.confidence,
393
      dataSharingMode: 'structured_only',
394
      submissionMethod: 'runtime_session',
395
    } as Json);
396
  finalizations[interactionId] = { status: 'report_pending', assessment, report };
397
  await setState(client, ctx.integrationId, {
398
    ...state,
399
    finalizationsJson: JSON.stringify(finalizations),
400
  });
401
  const completion = await sessionRequest<{ feedbackRequest: Json }>(
402
    session.reporting.completionEndpoint,
403
    session.reporting.bearerToken,
404
    report,
405
  );
406
  const request = completion.feedbackRequest;
407
  if (!request?.requestId)
408
    throw new sdk.RuntimeError('OpenClasp did not return a feedback request');
409
  const feedback =
410
    existing.feedback ??
411
    ({
412
      feedbackId: crypto.randomUUID(),
413
      requestId: request.requestId,
414
      interactionId,
415
      reviewerAgentId: state.agentId,
416
      subjectAgentId: session.peer.agentId,
417
      reviewerAgentVersion: state.agentVersion,
418
      ratings: assessment.ratings,
419
      wouldWorkAgain: assessment.wouldWorkAgain,
420
      reasonCodes: assessment.reasonCodes ?? [],
421
      ...(assessment.privateComment ? { privateComment: assessment.privateComment } : {}),
422
      evidenceReferences: assessment.evidenceReferences ?? [],
423
      confidence: assessment.confidence,
424
      submittedAt: new Date().toISOString(),
425
      submissionMethod: 'runtime_session',
426
    } as Json);
427
  if (request.status === 'submitted') {
428
    finalizations[interactionId] = {
429
      status: 'completed',
430
      assessment,
431
      report,
432
      feedback,
433
      feedbackRevealed: false,
434
    };
435
    await setState(client, ctx.integrationId, {
436
      ...state,
437
      finalizationsJson: JSON.stringify(finalizations),
438
    });
439
    return { interactionId, status: 'completed' as const, feedbackRevealed: false };
440
  }
441
  finalizations[interactionId] = {
442
    status: 'feedback_pending',
443
    assessment,
444
    report,
445
    feedback,
446
  };
447
  await setState(client, ctx.integrationId, {
448
    ...state,
449
    finalizationsJson: JSON.stringify(finalizations),
450
  });
451
  const result = await sessionRequest<{ revealed: boolean }>(
452
    session.reporting.feedbackEndpoint,
453
    session.reporting.bearerToken,
454
    feedback,
455
  );
456
  finalizations[interactionId] = {
457
    status: 'completed',
458
    assessment,
459
    report,
460
    feedback,
461
    feedbackRevealed: result.revealed,
462
  };
463
  await setState(client, ctx.integrationId, {
464
    ...state,
465
    finalizationsJson: JSON.stringify(finalizations),
466
  });
467
  await heartbeat(ctx, client);
468
  return {
469
    interactionId,
470
    status: 'completed' as const,
471
    feedbackRevealed: result.revealed,
472
  };
473
};
474
475
export default new bp.Integration({
476
  register: async ({ ctx, webhookUrl, client }) => {
477
    const state = await getState(client, ctx.integrationId);
478
    if (state.accessToken) {
479
      try {
480
        await deleteSetupConversations(client, ctx.integrationId);
481
      } catch {
482
        // Cleanup must not disconnect an already-paired runtime.
483
      }
484
      await bootstrapAndConnect({ ctx, webhookUrl, client, accessToken: state.accessToken });
485
      return;
486
    }
487
    const { publicKey, privateKey } = generateKeyPairSync('rsa', {
488
      modulusLength: 2048,
489
      publicKeyEncoding: { type: 'spki', format: 'pem' },
490
      privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
491
    });
492
    await setState(client, ctx.integrationId, {
493
      ...state,
494
      setupJson: JSON.stringify({
495
        pairing: { status: 'profile_requested', publicKey, privateKey },
496
      }),
497
    });
498
    await createIncomingMessage(
499
      client,
500
      `openclasp-setup:${ctx.integrationId}`,
501
      'openclasp-platform',
502
      profilePrompt(),
503
    );
504
  },
505
  unregister: async ({ ctx, client }) => {
506
    const state = await getState(client, ctx.integrationId);
507
    if (!state.accessToken) return;
508
    await openClaspRequest(platformUrl(ctx), state.accessToken, '/v0.1/runtime', {
509
      method: 'DELETE',
510
    });
511
  },
512
  actions: {
513
    completeInteraction: async ({ ctx, client, input }) => completeInteraction(ctx, client, input),
514
  },
515
  channels: {
516
    a2a: {
517
      messages: {
518
        text: async ({ ctx, client, conversation, payload }) => {
519
          const interactionId = conversation.tags.interactionId;
520
          if (!interactionId) throw new sdk.RuntimeError('Missing OpenClasp interaction ID');
521
          if (interactionId === `openclasp-setup:${ctx.integrationId}`) {
522
            const profile = parseAgentProfile(payload.text);
523
            await completeBotpressPairing(ctx, client, profile);
524
            await client.deleteConversation({ id: conversation.id });
525
            return;
526
          }
527
          const state = await getState(client, ctx.integrationId);
528
          if (!state.accessToken) throw new sdk.RuntimeError('OpenClasp pairing is not complete');
529
          const sessions = parseRecord(state.sessionsJson);
530
          const session = sessions[interactionId];
531
          if (!session) throw new sdk.RuntimeError('OpenClasp live session is unavailable');
532
          if (session.finalizationRequested) {
533
            try {
534
              const assessment = parseFinalizationAssessment(payload.text, interactionId);
535
              await completeInteraction(ctx, client, assessment);
536
              const latest = await getState(client, ctx.integrationId);
537
              const latestSessions = parseRecord(latest.sessionsJson);
538
              latestSessions[interactionId] = {
539
                ...latestSessions[interactionId],
540
                finalizationRequested: false,
541
                finalizationCompleted: true,
542
              };
543
              await setState(client, ctx.integrationId, {
544
                ...latest,
545
                sessionsJson: JSON.stringify(latestSessions),
546
              });
547
              return;
548
            } catch {
549
              const attempts = Number(session.finalizationAttempts ?? 0) + 1;
550
              sessions[interactionId] = {
551
                ...session,
552
                finalizationRequested: attempts < 3,
553
                finalizationAttempts: attempts,
554
              };
555
              await setState(client, ctx.integrationId, {
556
                ...state,
557
                sessionsJson: JSON.stringify(sessions),
558
              });
559
              if (attempts < 3) {
560
                const offer = parseRecord(state.offersJson)[interactionId]?.offer;
561
                await createIncomingMessage(
562
                  client,
563
                  interactionId,
564
                  session.peer.agentId,
565
                  finalizationPrompt(session, offer, true),
566
                );
567
              }
568
              return;
569
            }
570
          }
571
          if (session.checkpointRequested) {
572
            try {
573
              const checkpoint = parseCheckpoint(payload.text);
574
              await sessionRequest(session.reporting.endpoint, session.reporting.bearerToken, {
575
                eventId: crypto.randomUUID(),
576
                interactionId,
577
                agentId: state.agentId,
578
                sequence: Date.now(),
579
                type: 'progress_checkpoint',
580
                occurredAt: new Date().toISOString(),
581
                evidenceReferences: [],
582
                checkpoint,
583
                details: {
584
                  labels: [`state:${checkpoint.state}`, `topic:${checkpoint.topicStatus}`],
585
                  metrics: { progress: checkpoint.progress, confidence: checkpoint.confidence },
586
                  flags: { needsHuman: checkpoint.needsHuman },
587
                },
588
              });
589
              sessions[interactionId] = {
590
                ...session,
591
                checkpointRequested: false,
592
                checkpointAttempts: 0,
593
                lastCheckpointExchange: Number(session.exchangeCount ?? 0),
594
                lastCheckpointAt: new Date().toISOString(),
595
                latestCheckpoint: checkpoint,
596
              };
597
              await setState(client, ctx.integrationId, {
598
                ...state,
599
                sessionsJson: JSON.stringify(sessions),
600
              });
601
              if (checkpoint.state === 'done' || checkpoint.state === 'ready_to_finalize') {
602
                const latest = await getState(client, ctx.integrationId);
603
                const latestSessions = parseRecord(latest.sessionsJson);
604
                latestSessions[interactionId] = {
605
                  ...latestSessions[interactionId],
606
                  finalizationRequested: true,
607
                  finalizationAttempts: 0,
608
                };
609
                await setState(client, ctx.integrationId, {
610
                  ...latest,
611
                  sessionsJson: JSON.stringify(latestSessions),
612
                });
613
                const offer = parseRecord(latest.offersJson)[interactionId]?.offer;
614
                await createIncomingMessage(
615
                  client,
616
                  interactionId,
617
                  session.peer.agentId,
618
                  finalizationPrompt(session, offer),
619
                );
620
              }
621
              return;
622
            } catch {
623
              const attempts = Number(session.checkpointAttempts ?? 0) + 1;
624
              sessions[interactionId] = {
625
                ...session,
626
                checkpointRequested: attempts < 3,
627
                checkpointAttempts: attempts,
628
              };
629
              await setState(client, ctx.integrationId, {
630
                ...state,
631
                sessionsJson: JSON.stringify(sessions),
632
              });
633
              if (attempts < 3) {
634
                const offer = parseRecord(state.offersJson)[interactionId]?.offer;
635
                await createIncomingMessage(
636
                  client,
637
                  interactionId,
638
                  session.peer.agentId,
639
                  checkpointPrompt(offer, true),
640
                );
641
              }
642
              return;
643
            }
644
          }
645
          const response = await fetch(session.peer.endpoint, {
646
            method: 'POST',
647
            headers: {
648
              authorization: `Bearer ${session.peer.bearerToken}`,
649
              'content-type': 'application/json',
650
              'A2A-Extensions': EXTENSION_URI,
651
            },
652
            body: JSON.stringify({
653
              jsonrpc: '2.0',
654
              id: crypto.randomUUID(),
655
              method: 'message/send',
656
              params: {
657
                message: {
658
                  role: 'agent',
659
                  parts: [{ kind: 'text', text: payload.text }],
660
                  metadata: {
661
                    [EXTENSION_URI]: {
662
                      interactionId,
663
                      termsHash: session.contractHash,
664
                      initiatorAgentId:
665
                        session.role === 'initiator' ? session.agentId : session.peer.agentId,
666
                      responderAgentId:
667
                        session.role === 'responder' ? session.agentId : session.peer.agentId,
668
                    },
669
                  },
670
                },
671
              },
672
            }),
673
          });
674
          if (!response.ok)
675
            throw new sdk.RuntimeError(`Peer A2A endpoint returned HTTP ${response.status}`);
676
          const exchangeCount = Number(session.exchangeCount ?? 0) + 1;
677
          const shouldCheckpoint = exchangeCount - Number(session.lastCheckpointExchange ?? 0) >= 5;
678
          sessions[interactionId] = {
679
            ...session,
680
            exchangeCount,
681
            ...(shouldCheckpoint ? { checkpointRequested: true, checkpointAttempts: 0 } : {}),
682
          };
683
          await setState(client, ctx.integrationId, {
684
            ...state,
685
            sessionsJson: JSON.stringify(sessions),
686
          });
687
          if (shouldCheckpoint) {
688
            const offer = parseRecord(state.offersJson)[interactionId]?.offer;
689
            await createIncomingMessage(
690
              client,
691
              interactionId,
692
              session.peer.agentId,
693
              checkpointPrompt(offer),
694
            );
695
          }
696
          await heartbeat(ctx, client);
697
        },
698
      },
699
    },
700
  },
701
  handler: async ({ ctx, client, req }) => {
702
    if (req.method !== 'POST' || !req.body)
703
      return jsonResponse(405, { error: 'method_not_allowed' });
704
    let body: Json;
705
    try {
706
      body = JSON.parse(req.body) as Json;
707
    } catch {
708
      return jsonResponse(400, { error: 'invalid_json' });
709
    }
710
    const state = await getState(client, ctx.integrationId);
711
    if (body.type === 'openclasp.runtime.verify') {
712
      if (body.agentId !== state.agentId) return jsonResponse(403, { error: 'wrong_agent' });
713
      return jsonResponse(200, {
714
        type: 'openclasp.runtime.verified',
715
        version: '1',
716
        agentId: state.agentId,
717
        challenge: body.challenge,
718
        a2aEndpoint: botpressWebhookEndpoint(ctx.webhookId),
719
      });
720
    }
721
    if (
722
      body.type === 'openclasp.session.offer' ||
723
      body.type === 'openclasp.session.activation' ||
724
      body.type === 'openclasp.session.finalization_request'
725
    ) {
726
      const requestId = req.headers['openclasp-request-id'] ?? '';
727
      const timestamp = req.headers['openclasp-timestamp'] ?? '';
728
      const signature = req.headers['openclasp-signature'] ?? '';
729
      if (
730
        !requestId ||
731
        !timestamp ||
732
        !signature ||
733
        !(await validControlSignature(platformUrl(ctx), requestId, timestamp, req.body, signature))
734
      )
735
        return jsonResponse(401, { error: 'invalid_openclasp_signature' });
736
      if (body.agentId !== state.agentId) return jsonResponse(403, { error: 'wrong_agent' });
737
      if (body.type === 'openclasp.session.finalization_request') {
738
        const sessions = parseRecord(state.sessionsJson);
739
        const session = sessions[body.interactionId];
740
        if (
741
          body.requestId !== requestId ||
742
          typeof body.interactionId !== 'string' ||
743
          !session ||
744
          body.contractHash !== session.contractHash ||
745
          body.peerAgentId !== session.peer.agentId
746
        )
747
          return jsonResponse(400, { error: 'invalid_finalization_request' });
748
        if (parseRecord(state.finalizationsJson)[body.interactionId]?.status === 'completed')
749
          return jsonResponse(200, { accepted: true, alreadyCompleted: true });
750
        if (session.finalizationRequested)
751
          return jsonResponse(200, { accepted: true, alreadyRequested: true });
752
        sessions[body.interactionId] = {
753
          ...session,
754
          finalizationRequested: true,
755
          finalizationAttempts: 0,
756
        };
757
        await setState(client, ctx.integrationId, {
758
          ...state,
759
          sessionsJson: JSON.stringify(sessions),
760
        });
761
        const offer = parseRecord(state.offersJson)[body.interactionId]?.offer;
762
        await createIncomingMessage(
763
          client,
764
          body.interactionId,
765
          session.peer.agentId,
766
          finalizationPrompt(session, offer),
767
        );
768
        return jsonResponse(202, { accepted: true });
769
      }
770
      if (body.type === 'openclasp.session.offer') {
771
        const offers = parseRecord(state.offersJson);
772
        if (
773
          body.offerId !== requestId ||
774
          typeof body.interactionId !== 'string' ||
775
          typeof body.expiresAt !== 'string' ||
776
          Date.parse(body.expiresAt) <= Date.now()
777
        )
778
          return jsonResponse(400, { error: 'invalid_session_offer' });
779
        const previous = offers[body.interactionId];
780
        if (previous && previous.offer?.offerId !== body.offerId)
781
          return jsonResponse(409, { error: 'interaction_offer_conflict' });
782
        const sessionId = previous?.sessionId ?? crypto.randomUUID();
783
        offers[body.interactionId] = { offer: body, sessionId };
784
        await setState(client, ctx.integrationId, {
785
          ...state,
786
          offersJson: JSON.stringify(offers),
787
        });
788
        return jsonResponse(200, {
789
          type: 'openclasp.session.accepted',
790
          version: '1',
791
          offerId: body.offerId,
792
          interactionId: body.interactionId,
793
          agentId: state.agentId,
794
          sessionId,
795
          a2aEndpoint: botpressWebhookEndpoint(ctx.webhookId),
796
          expiresAt: body.expiresAt,
797
        });
798
      }
799
      const sessions = parseRecord(state.sessionsJson);
800
      if (
801
        body.activationId !== requestId ||
802
        typeof body.interactionId !== 'string' ||
803
        typeof body.expiresAt !== 'string' ||
804
        Date.parse(body.expiresAt) <= Date.now()
805
      )
806
        return jsonResponse(400, { error: 'invalid_session_activation' });
807
      if (sessions[body.interactionId]?.activationId === body.activationId)
808
        return jsonResponse(200, { accepted: true, activationId: body.activationId });
809
      sessions[body.interactionId] = body;
810
      await setState(client, ctx.integrationId, {
811
        ...state,
812
        sessionsJson: JSON.stringify(sessions),
813
      });
814
      if (body.role === 'initiator') {
815
        const offer = parseRecord(state.offersJson)[body.interactionId]?.offer;
816
        await createIncomingMessage(
817
          client,
818
          body.interactionId,
819
          body.peer.agentId,
820
          `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.`,
821
        );
822
      }
823
      return jsonResponse(200, { accepted: true, activationId: body.activationId });
824
    }
825
    if (body.jsonrpc !== '2.0' || body.method !== 'message/send' || body.id === undefined)
826
      return jsonResponse(400, { error: 'invalid_a2a_request' });
827
    const authorization = req.headers.authorization ?? '';
828
    if (!authorization.startsWith('Bearer '))
829
      return jsonResponse(401, { error: 'session_credential_required' });
830
    try {
831
      const credential = decodeCredential(authorization.slice(7));
832
      const interactionId = credential.grant.interactionId;
833
      if (typeof interactionId !== 'string') throw new Error('Invalid session credential');
834
      const sessions = parseRecord(state.sessionsJson);
835
      let session = sessions[interactionId];
836
      if (!session) return jsonResponse(404, { error: 'live_session_not_found' });
837
      validSessionCredential(authorization.slice(7), session, state.agentId);
838
      const peerText = textFromMessage(body.params?.message ?? {});
839
      let deliveredText = peerText;
840
      if (!session.contextDelivered) {
841
        deliveredText = withSessionContext(
842
          session,
843
          parseRecord(state.offersJson)[interactionId]?.offer,
844
          peerText,
845
        );
846
        session = { ...session, contextDelivered: true };
847
        sessions[interactionId] = session;
848
        await setState(client, ctx.integrationId, {
849
          ...state,
850
          sessionsJson: JSON.stringify(sessions),
851
        });
852
      }
853
      const message = await createIncomingMessage(
854
        client,
855
        interactionId,
856
        session.peer.agentId,
857
        deliveredText,
858
      );
859
      await heartbeat(ctx, client);
860
      return jsonResponse(200, {
861
        jsonrpc: '2.0',
862
        id: body.id,
863
        result: { task: { id: message.id, state: 'submitted' } },
864
      });
865
    } catch (error) {
866
      return jsonResponse(401, {
867
        error: error instanceof Error ? error.message : 'invalid_session_credential',
868
      });
869
    }
870
  },
871
});