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

Viewing files at commit 310519e1fe0fCommit detailsView latest

packages/persistence/src/hosted.ts 4897 lines · 191.0 KB · typescript
1
import { neon, type NeonQueryFunction } from '@neondatabase/serverless';
2
import { createHash, randomBytes } from 'node:crypto';
3
import {
4
  buildCounterpartyBrief,
5
  buildInteractionConclusion,
6
  deriveBehaviouralObservations,
7
  evaluateLearningEligibility,
8
  summarizeContextualReliability,
9
  updateContextualBehaviouralProfile,
10
} from '../../core/src/index.js';
11
import {
12
  AgentIdentitySchema,
13
  AgentResolutionSchema,
14
  AssuranceDecisionSchema,
15
  AssuranceEffectivenessEvaluationSchema,
16
  AssuranceClaimOutcomeComparisonSchema,
17
  AssurancePredictionSnapshotSchema,
18
  AssuranceProbePlanSchema,
19
  AssuranceProbeResponseSchema,
20
  AssuranceSafeguardSchema,
21
  BehaviouralProfileDeltaSchema,
22
  DEFAULT_AGENT_AUTH_SCOPES,
23
  DEFAULT_EXTENSION_URI,
24
  CounterpartyBriefSchema,
25
  ContractRevisionSchema,
26
  FederatedInteractionSchema,
27
  FeedbackDimensionSchema,
28
  FeedbackRequestSchema,
29
  LiveSessionAcceptanceSchema,
30
  LiveSessionActivationSchema,
31
  LiveSessionEventSchema,
32
  LiveSessionOfferSchema,
33
  LiveSessionStateRecordSchema,
34
  InteractionCompletionReportSchema,
35
  InteractionContractSchema,
36
  InteractionConclusionSchema,
37
  InteractionFeedbackSchema,
38
  LearningEligibilityDecisionSchema,
39
  PublicAgentCardSchema,
40
  ReceiptSchema,
41
  ShieldCaseSchema,
42
  ShieldConsultationSchema,
43
  ShieldOutcomeSchema,
44
  canonicalHash,
45
  verifyObject,
46
  type FederatedInteraction,
47
  type AgentPresence,
48
  type AgentResolution,
49
  type AssuranceDecision,
50
  type AssuranceEffectivenessEvaluation,
51
  type AssuranceClaimOutcomeComparison,
52
  type AssurancePredictionSnapshot,
53
  type AssuranceProbePlan,
54
  type AssuranceProbeResponse,
55
  type BehaviouralProfileDelta,
56
  type CounterpartyBrief,
57
  type ContractRevision,
58
  type LiveSessionActivation,
59
  type LiveSessionEvent,
60
  type LiveSessionInsight,
61
  type InteractionCompletionReport,
62
  type InteractionContract,
63
  type InteractionFeedback,
64
  type InteractionConclusion,
65
  type OpenClaspAuthScope,
66
  type PublicAgentCard,
67
  type ShieldCase,
68
  type ShieldConsultation,
69
  type ShieldOutcome,
70
} from '../../protocol/src/index.js';
71
import type { AgentProfile } from './onboarding.js';
72
import type { AgentInstallation } from './onboarding.js';
73
import {
74
  connectorClaimExpired,
75
  createConnectorAgent,
76
  createConnectorClaimInput,
77
  encryptConnectorCredential,
78
  matchesConnectorClaimSecret,
79
  publicConnectorClaim,
80
  type ConnectorClaim,
81
} from './connector-claim.js';
82
import {
83
  createProviderConnectionInput,
84
  hashProviderConnectionCode,
85
  publicProviderConnection,
86
  type ProviderConnection,
87
} from './provider-connection.js';
88
import {
89
  agentAccessTokenClientId,
90
  agentAccessTokenId,
91
  createAgentAccessToken,
92
  matchesAgentAccessToken,
93
  type AgentAccessTokenMetadata,
94
} from './access-token.js';
95
import {
96
  decryptGatewayPayload,
97
  encryptGatewayPayload,
98
  attestSessionRecord,
99
  getSessionKeyId,
100
  getSessionVerificationKey,
101
  issueSessionGrant,
102
  signSessionControl,
103
  verifySessionGrant,
104
} from './relay.js';
105
import { postRuntimeJson, resolvePublicRuntimeEndpoint } from './runtime.js';
106
import { verifyHostedMigrations } from './hosted-migrations.js';
107
import {
108
  buildSourceRecordEnvelope,
109
  shouldJournalSourceRecord,
110
  type SourceRecordWriteMetadata,
111
} from './source-record.js';
112
113
type DiagnosticFields = Record<string, string | number | boolean | null | undefined>;
114
115
function a2aInfo(event: string, fields: DiagnosticFields = {}) {
116
  if (process.env.OPENCLASP_DIAGNOSTICS === 'off') return;
117
  console.info('[a2a]', { event, ...fields });
118
}
119
120
function a2aError(event: string, error: unknown, fields: DiagnosticFields = {}) {
121
  if (process.env.OPENCLASP_DIAGNOSTICS === 'off') return;
122
  console.error('[a2a]', {
123
    event,
124
    ...fields,
125
    errorName: error instanceof Error ? error.name.slice(0, 80) : 'UnknownError',
126
    errorCode:
127
      typeof error === 'object' &&
128
      error !== null &&
129
      'code' in error &&
130
      typeof error.code === 'string'
131
        ? error.code.slice(0, 80)
132
        : undefined,
133
  });
134
}
135
136
export type HostedRecordKind =
137
  | 'agent'
138
  | 'delegation'
139
  | 'contract'
140
  | 'interaction'
141
  | 'event'
142
  | 'receipt'
143
  | 'feedback'
144
  | 'counterparty_brief'
145
  | 'completion_report'
146
  | 'feedback_request'
147
  | 'interaction_feedback'
148
  | 'interaction_conclusion'
149
  | 'learning_eligibility'
150
  | 'profile_delta'
151
  | 'conflict'
152
  | 'profile'
153
  | 'consent'
154
  | 'revocation'
155
  | 'project'
156
  | 'agent_profile'
157
  | 'installation'
158
  | 'setup_request'
159
  | 'publication'
160
  | 'presence'
161
  | 'shield_case'
162
  | 'shield_consultation'
163
  | 'shield_outcome';
164
165
export type { PublicAgentCard, FederatedInteraction } from '../../protocol/src/index.js';
166
167
type ContextualProfile = {
168
  recordId: string;
169
  agentId: string;
170
  agentVersion: string;
171
  taskCategory: string;
172
  sampleSize: number;
173
  effectiveSampleSize?: number;
174
  dimensionSampleSizes?: Partial<
175
    Record<
176
      | 'completion'
177
      | 'acceptance'
178
      | 'specification'
179
      | 'deadline'
180
      | 'communication'
181
      | 'evidence'
182
      | 'scope'
183
      | 'correction'
184
      | 'limitations'
185
      | 'disputes',
186
      number
187
    >
188
  >;
189
  updatedAt: string;
190
  completion: number;
191
  acceptance: number;
192
  specification: number;
193
  deadline: number;
194
  communication: number;
195
  evidence: number;
196
  scope: number;
197
  correction: number;
198
  limitations: number;
199
  disputes: number;
200
};
201
202
const FEEDBACK_DIMENSIONS = FeedbackDimensionSchema.options;
203
204
function feedbackWindowMilliseconds() {
205
  const configured = Number(process.env.OPENCLASP_FEEDBACK_WINDOW_MINUTES ?? 120);
206
  const minutes = Number.isFinite(configured) ? Math.max(15, Math.min(1440, configured)) : 120;
207
  return minutes * 60_000;
208
}
209
210
function reviewerCredibility(feedback: InteractionFeedback): number {
211
  const provenance =
212
    feedback.submissionMethod === 'agent_signature'
213
      ? 0.9
214
      : feedback.submissionMethod === 'runtime_session'
215
        ? 0.8
216
        : feedback.submissionMethod === 'agent_access_token'
217
          ? 0.75
218
          : 0.65;
219
  return Math.min(1, provenance + (feedback.evidenceReferences.length ? 0.1 : 0));
220
}
221
222
function deterministicUuid(value: string): string {
223
  const bytes = createHash('sha256').update(value).digest().subarray(0, 16);
224
  bytes[6] = (bytes[6]! & 0x0f) | 0x50;
225
  bytes[8] = (bytes[8]! & 0x3f) | 0x80;
226
  const hex = bytes.toString('hex');
227
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
228
}
229
230
type AssuranceLearningSummary = {
231
  sampleSize: number;
232
  averageBrierScore?: number;
233
  questionFamilies: Array<{
234
    questionFamily: AssuranceEffectivenessEvaluation['questionScores'][number]['questionFamily'];
235
    sampleSize: number;
236
    riskRevealRate: number;
237
    predictiveAccuracy: number;
238
    averageAbsolutePredictionDelta: number;
239
    utilityScore: number;
240
  }>;
241
  safeguardTypes: Array<{
242
    type: AssuranceEffectivenessEvaluation['safeguardScores'][number]['type'];
243
    sampleSize: number;
244
    positiveOutcomeRate: number;
245
  }>;
246
};
247
248
function summarizeAssuranceLearning(
249
  evaluations: AssuranceEffectivenessEvaluation[],
250
): AssuranceLearningSummary {
251
  const predictionScores = evaluations.flatMap((evaluation) => evaluation.predictionScores);
252
  const questionGroups = new Map<
253
    string,
254
    {
255
      questionFamily: AssuranceLearningSummary['questionFamilies'][number]['questionFamily'];
256
      sampleSize: number;
257
      riskReveals: number;
258
      predictiveScore: number;
259
      absolutePredictionDelta: number;
260
    }
261
  >();
262
  for (const evaluation of evaluations) {
263
    for (const question of evaluation.questionScores) {
264
      const group = questionGroups.get(question.questionFamily) ?? {
265
        questionFamily: question.questionFamily,
266
        sampleSize: 0,
267
        riskReveals: 0,
268
        predictiveScore: 0,
269
        absolutePredictionDelta: 0,
270
      };
271
      group.sampleSize += 1;
272
      if (question.exposedMaterialRisk) group.riskReveals += 1;
273
      const predictedOutcome = question.exposedMaterialRisk ? 0 : 1;
274
      group.predictiveScore += 1 - Math.abs(predictedOutcome - evaluation.outcomeValue);
275
      group.absolutePredictionDelta += Math.abs(question.predictionDelta);
276
      questionGroups.set(question.questionFamily, group);
277
    }
278
  }
279
  const safeguardGroups = new Map<
280
    string,
281
    {
282
      type: AssuranceLearningSummary['safeguardTypes'][number]['type'];
283
      sampleSize: number;
284
      positive: number;
285
    }
286
  >();
287
  for (const safeguard of evaluations.flatMap((evaluation) => evaluation.safeguardScores)) {
288
    if (safeguard.status !== 'accepted' && safeguard.status !== 'modified') continue;
289
    const group = safeguardGroups.get(safeguard.type) ?? {
290
      type: safeguard.type,
291
      sampleSize: 0,
292
      positive: 0,
293
    };
294
    group.sampleSize += 1;
295
    if (safeguard.outcomeAssociation === 'positive') group.positive += 1;
296
    safeguardGroups.set(safeguard.type, group);
297
  }
298
  return {
299
    sampleSize: evaluations.length,
300
    ...(predictionScores.length
301
      ? {
302
          averageBrierScore:
303
            predictionScores.reduce((sum, score) => sum + score.brierScore, 0) /
304
            predictionScores.length,
305
        }
306
      : {}),
307
    questionFamilies: [...questionGroups.values()]
308
      .map((group) => {
309
        const riskRevealRate = (group.riskReveals + 1) / (group.sampleSize + 2);
310
        const predictiveAccuracy = (group.predictiveScore + 1) / (group.sampleSize + 2);
311
        const averageAbsolutePredictionDelta = group.absolutePredictionDelta / group.sampleSize;
312
        return {
313
          questionFamily: group.questionFamily,
314
          sampleSize: group.sampleSize,
315
          riskRevealRate,
316
          predictiveAccuracy,
317
          averageAbsolutePredictionDelta,
318
          utilityScore: Math.min(
319
            1,
320
            riskRevealRate * 0.4 +
321
              predictiveAccuracy * 0.5 +
322
              Math.min(1, averageAbsolutePredictionDelta * 4) * 0.1,
323
          ),
324
        };
325
      })
326
      .sort((left, right) => right.utilityScore - left.utilityScore),
327
    safeguardTypes: [...safeguardGroups.values()]
328
      .map((group) => ({
329
        type: group.type,
330
        sampleSize: group.sampleSize,
331
        positiveOutcomeRate: (group.positive + 1) / (group.sampleSize + 2),
332
      }))
333
      .sort((left, right) => right.positiveOutcomeRate - left.positiveOutcomeRate),
334
  };
335
}
336
337
function assuranceClaimComparison(
338
  answer: AssuranceProbeResponse['answers'][number],
339
  report: InteractionCompletionReport,
340
) {
341
  const code = answer.questionCode;
342
  const claim = answer.answer;
343
  const normalized = String(claim).toLowerCase();
344
  const positive = ['true', 'yes', 'complete', 'all', 'accurate'].includes(normalized);
345
  const partial = ['partially', 'partial', 'some'].includes(normalized);
346
  const negative = ['false', 'no', 'none'].includes(normalized);
347
  const observedOutcome = `${report.outcome}: ${report.summary}`.slice(0, 500);
348
  let status: 'aligned' | 'partially_aligned' | 'contradicted' | 'unverifiable' = 'unverifiable';
349
  if (/capability|commitment|complete|deliver|deadline/.test(code)) {
350
    if (report.outcome === 'success')
351
      status = positive
352
        ? 'aligned'
353
        : partial
354
          ? 'partially_aligned'
355
          : negative
356
            ? 'contradicted'
357
            : 'unverifiable';
358
    if (report.outcome === 'partial')
359
      status = partial ? 'aligned' : positive || negative ? 'partially_aligned' : 'unverifiable';
360
    if (report.outcome === 'failure' || report.outcome === 'cancelled')
361
      status = negative
362
        ? 'aligned'
363
        : partial
364
          ? 'partially_aligned'
365
          : positive
366
            ? 'contradicted'
367
            : 'unverifiable';
368
  } else if (/evidence/.test(code)) {
369
    const evidenceProvided = report.evidenceReferences.length > 0;
370
    status = evidenceProvided
371
      ? negative
372
        ? 'contradicted'
373
        : positive
374
          ? 'aligned'
375
          : 'partially_aligned'
376
      : positive
377
        ? 'contradicted'
378
        : negative
379
          ? 'aligned'
380
          : 'unverifiable';
381
  } else if (/depend|blocker|approval|tool/.test(code)) {
382
    const blockersObserved = report.blockers.length > 0;
383
    status =
384
      normalized === 'none'
385
        ? blockersObserved
386
          ? 'contradicted'
387
          : 'aligned'
388
        : blockersObserved
389
          ? 'aligned'
390
          : 'unverifiable';
391
  }
392
  return {
393
    questionCode: code,
394
    preTaskClaim: claim,
395
    observedOutcome,
396
    status,
397
    evidenceReferences: [
398
      ...new Set([...answer.evidenceReferences, ...report.evidenceReferences]),
399
    ].slice(0, 10),
400
  };
401
}
402
403
function normalizePublicAgentCard(value: unknown): PublicAgentCard {
404
  const current = PublicAgentCardSchema.safeParse(value);
405
  const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
406
    /\/$/,
407
    '',
408
  );
409
  if (current.success) {
410
    const slug = current.data.slug ?? publicAgentSlug(current.data.name, current.data.agentId);
411
    return PublicAgentCardSchema.parse({
412
      ...current.data,
413
      slug,
414
      profileUrl: `${baseUrl}/a/${encodeURIComponent(slug)}`,
415
      cardUrl: `${baseUrl}/agents/${encodeURIComponent(current.data.agentId)}/card.json`,
416
      a2aAgentCardUrl: `${baseUrl}/agents/${encodeURIComponent(current.data.agentId)}/a2a-agent-card.json`,
417
      extensionUri: current.data.extensionUri ?? DEFAULT_EXTENSION_URI,
418
      verification: {
419
        ...(current.data.verification ?? {
420
          status: 'verified',
421
          method: 'openclasp_oauth_account',
422
          verifiedAt: current.data.publishedAt,
423
        }),
424
        verificationKeyUrl: `${baseUrl}/.well-known/openclasp-session-key`,
425
      },
426
    });
427
  }
428
  const legacy = value as Record<string, unknown>;
429
  const agentId = String(legacy.agentId ?? '');
430
  const slug = publicAgentSlug(String(legacy.name ?? agentId), agentId);
431
  return PublicAgentCardSchema.parse({
432
    protocolVersion: '0.1',
433
    agentId,
434
    name: legacy.name,
435
    description: '',
436
    framework: legacy.framework,
437
    agentVersion: '1.0.0',
438
    capabilities: legacy.capabilities,
439
    limitations: legacy.limitations,
440
    assurance: legacy.assurance,
441
    transports:
442
      typeof legacy.a2aEndpoint === 'string'
443
        ? [
444
            {
445
              protocol: 'A2A/1.0',
446
              protocolBinding: 'JSONRPC',
447
              endpoint: legacy.a2aEndpoint,
448
              managedBy: 'agent',
449
            },
450
          ]
451
        : [],
452
    slug,
453
    profileUrl: `${baseUrl}/a/${encodeURIComponent(slug)}`,
454
    cardUrl: `${baseUrl}/agents/${encodeURIComponent(agentId)}/card.json`,
455
    a2aAgentCardUrl: `${baseUrl}/agents/${encodeURIComponent(agentId)}/a2a-agent-card.json`,
456
    extensionUri: DEFAULT_EXTENSION_URI,
457
    verification: {
458
      status: 'verified',
459
      method: 'openclasp_oauth_account',
460
      verifiedAt: legacy.publishedAt,
461
      verificationKeyUrl: `${baseUrl}/.well-known/openclasp-session-key`,
462
    },
463
    publishedAt: legacy.publishedAt,
464
    updatedAt: legacy.updatedAt,
465
  });
466
}
467
468
function publicAgentSlug(name: string, agentId: string) {
469
  const prefix =
470
    name
471
      .trim()
472
      .toLowerCase()
473
      .replace(/[^a-z0-9]+/g, '-')
474
      .replace(/^-|-$/g, '')
475
      .slice(0, 48) || 'agent';
476
  const suffix = createHash('sha256').update(agentId).digest('hex').slice(0, 8);
477
  return `${prefix}-${suffix}`;
478
}
479
480
export function buildPublicAgentCard(
481
  agent: AgentProfile,
482
  baseUrl: string,
483
  previous?: PublicAgentCard,
484
): PublicAgentCard {
485
  const now = new Date().toISOString();
486
  const root = baseUrl.replace(/\/$/, '');
487
  const slug = previous?.slug ?? publicAgentSlug(agent.name, agent.agentId);
488
  return PublicAgentCardSchema.parse({
489
    protocolVersion: '0.1',
490
    agentId: agent.agentId,
491
    name: agent.name,
492
    description: agent.description ?? '',
493
    framework: agent.framework,
494
    agentVersion: agent.agentVersion ?? '1.0.0',
495
    capabilities: agent.capabilities,
496
    limitations: agent.limitations,
497
    assurance: 'oauth_authenticated',
498
    agentMode: 'persistent_runtime',
499
    transports: agent.a2aEndpoint
500
      ? [
501
          {
502
            protocol: 'A2A/1.0',
503
            protocolBinding: 'JSONRPC',
504
            endpoint: agent.a2aEndpoint,
505
            managedBy: 'agent',
506
          },
507
        ]
508
      : [],
509
    slug,
510
    profileUrl: `${root}/a/${encodeURIComponent(slug)}`,
511
    cardUrl: `${root}/agents/${encodeURIComponent(agent.agentId)}/card.json`,
512
    a2aAgentCardUrl: `${root}/agents/${encodeURIComponent(agent.agentId)}/a2a-agent-card.json`,
513
    extensionUri: DEFAULT_EXTENSION_URI,
514
    verification: {
515
      status: 'verified',
516
      method: 'openclasp_oauth_account',
517
      verifiedAt: previous?.verification?.verifiedAt ?? previous?.publishedAt ?? now,
518
      verificationKeyUrl: `${root}/.well-known/openclasp-session-key`,
519
    },
520
    publishedAt: previous?.publishedAt ?? now,
521
    updatedAt: now,
522
  });
523
}
524
525
const normalized = (value: string) =>
526
  value
527
    .trim()
528
    .toLowerCase()
529
    .replace(/[^a-z0-9]+/g, '_');
530
531
export const AGENT_ONLINE_WINDOW_MS = 2 * 60_000;
532
533
export function resolveAgentPresence(lastSeenAt?: string, now = new Date()): AgentPresence {
534
  const online = Boolean(
535
    lastSeenAt && now.getTime() - Date.parse(lastSeenAt) <= AGENT_ONLINE_WINDOW_MS,
536
  );
537
  return {
538
    status: online ? 'online' : 'offline',
539
    ...(lastSeenAt ? { lastSeenAt } : {}),
540
    checkedAt: now.toISOString(),
541
  };
542
}
543
544
export function canAutoAcceptInteraction(
545
  agent: AgentProfile,
546
  interaction: FederatedInteraction,
547
): boolean {
548
  if (agent.autoAcceptPolicy !== 'safe_matching' || agent.status !== 'active') return false;
549
  const categories = new Set(
550
    [...(agent.autoAcceptTaskCategories ?? []), ...(agent.capabilities ?? [])].map(normalized),
551
  );
552
  const taskCategory = normalized(interaction.contract.taskCategory);
553
  if (!taskCategory || !categories.has(taskCategory)) return false;
554
  if (interaction.contract.allowedData.length > 0) return false;
555
  if (
556
    /\b(password|secret|api[_ -]?key|access[_ -]?token|credential|bank|payment|medical|health|ssn|personal[_ -]?data|customer[_ -]?record)\b/i.test(
557
      `${interaction.contract.purpose} ${interaction.contract.requestedOutcome}`,
558
    )
559
  )
560
    return false;
561
  if (interaction.contract.humanApprovalRequirements.length > 0) return false;
562
  if (interaction.contract.retentionDays > 30) return false;
563
  if (interaction.contract.allowedActions.length > 0) {
564
    const capabilities = new Set((agent.capabilities ?? []).map(normalized));
565
    if (
566
      !interaction.contract.allowedActions.every((action) => capabilities.has(normalized(action)))
567
    )
568
      return false;
569
  }
570
  return true;
571
}
572
573
function withContractRevisionHistory(value: FederatedInteraction): FederatedInteraction {
574
  if (value.contractRevisions.length) return value;
575
  const status =
576
    value.status === 'pending'
577
      ? 'proposed'
578
      : ['active', 'completed'].includes(value.status)
579
        ? 'accepted'
580
        : 'rejected';
581
  const revision = ContractRevisionSchema.parse({
582
    revisionId: deterministicUuid(`${value.interactionId}:contract:1:${value.termsHash}`),
583
    interactionId: value.interactionId,
584
    revision: value.contractRevision,
585
    termsHash: value.termsHash,
586
    contract: value.contract,
587
    proposedByAgentId: value.initiatorAgentId,
588
    status,
589
    acceptances: value.acceptances,
590
    createdAt: value.createdAt,
591
    updatedAt: value.updatedAt,
592
    expiresAt: value.expiresAt,
593
  });
594
  return FederatedInteractionSchema.parse({ ...value, contractRevisions: [revision] });
595
}
596
597
function openContractRevision(value: FederatedInteraction): ContractRevision | undefined {
598
  return [...value.contractRevisions].reverse().find((revision) => revision.status === 'proposed');
599
}
600
601
export type AccountSettings = {
602
  displayName: string;
603
  contributionEnabled: boolean;
604
  rawConversationsStored: false;
605
};
606
607
const defaults: AccountSettings = {
608
  displayName: '',
609
  contributionEnabled: false,
610
  rawConversationsStored: false,
611
};
612
613
export class HostedRepository {
614
  private readonly sql: NeonQueryFunction<false, false>;
615
  private initialized?: Promise<void>;
616
617
  constructor(databaseUrl: string) {
618
    this.sql = neon(databaseUrl);
619
  }
620
621
  private gatewaySecret() {
622
    const value = process.env.OPENCLASP_RELAY_ENCRYPTION_KEY;
623
    if (!value || value.length < 32) throw new Error('Gateway encryption is not configured');
624
    return value;
625
  }
626
627
  private gatewaySecrets() {
628
    const active = this.gatewaySecret();
629
    const previous = (process.env.OPENCLASP_RELAY_PREVIOUS_KEYS ?? '')
630
      .split(',')
631
      .map((value) => value.trim())
632
      .filter(Boolean);
633
    if (previous.some((value) => value.length < 32))
634
      throw new Error('A previous gateway key is too short');
635
    if (previous.length > 3) throw new Error('At most three previous gateway keys are supported');
636
    return [...new Set([active, ...previous])];
637
  }
638
639
  private decryptGatewayPayload(encrypted: { ciphertext: string; iv: string; authTag: string }) {
640
    for (const secret of this.gatewaySecrets()) {
641
      try {
642
        return decryptGatewayPayload(secret, encrypted);
643
      } catch {
644
        // Try retained rotation keys without exposing which key failed.
645
      }
646
    }
647
    throw new Error('Encrypted gateway payload could not be decrypted');
648
  }
649
650
  private verifySessionGrant(token: string) {
651
    for (const secret of this.gatewaySecrets()) {
652
      try {
653
        return verifySessionGrant(secret, token);
654
      } catch {
655
        // Short-lived sessions remain valid during an orderly key rotation.
656
      }
657
    }
658
    throw new Error('Invalid or expired live-session credential');
659
  }
660
661
  private attestPublicAgentCard(value: PublicAgentCard): PublicAgentCard {
662
    const unsigned = { ...value };
663
    delete unsigned.platformAttestation;
664
    return PublicAgentCardSchema.parse({
665
      ...unsigned,
666
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unsigned),
667
    });
668
  }
669
670
  ensureSchema(): Promise<void> {
671
    return (this.initialized ??= verifyHostedMigrations(this.sql));
672
  }
673
674
  private connectorClaimFromRow(row: Record<string, any>): ConnectorClaim {
675
    return {
676
      claimId: String(row.claim_id),
677
      secretHash: String(row.secret_hash),
678
      runtimeEndpoint: String(row.runtime_endpoint),
679
      credentialPublicKey: String(row.credential_public_key),
680
      profile: row.profile,
681
      status: row.status,
682
      ...(row.operator_id ? { operatorId: String(row.operator_id) } : {}),
683
      ...(row.agent_id ? { agentId: String(row.agent_id) } : {}),
684
      ...(row.credential_ciphertext
685
        ? { credentialCiphertext: String(row.credential_ciphertext) }
686
        : {}),
687
      createdAt: new Date(String(row.created_at)).toISOString(),
688
      expiresAt: new Date(String(row.expires_at)).toISOString(),
689
      ...(row.decided_at ? { decidedAt: new Date(String(row.decided_at)).toISOString() } : {}),
690
      ...(row.connected_at
691
        ? { connectedAt: new Date(String(row.connected_at)).toISOString() }
692
        : {}),
693
    };
694
  }
695
696
  private providerConnectionFromRow(row: Record<string, any>): ProviderConnection {
697
    return {
698
      connectionId: String(row.connection_id),
699
      operatorId: String(row.operator_id),
700
      provider: row.provider,
701
      agentName: String(row.agent_name),
702
      codeHash: String(row.code_hash),
703
      status: row.status,
704
      ...(row.agent_id ? { agentId: String(row.agent_id) } : {}),
705
      ...(row.runtime_endpoint ? { runtimeEndpoint: String(row.runtime_endpoint) } : {}),
706
      ...(row.credential_public_key
707
        ? { credentialPublicKey: String(row.credential_public_key) }
708
        : {}),
709
      ...(row.credential_ciphertext
710
        ? { credentialCiphertext: String(row.credential_ciphertext) }
711
        : {}),
712
      createdAt: new Date(String(row.created_at)).toISOString(),
713
      expiresAt: new Date(String(row.expires_at)).toISOString(),
714
      ...(row.connected_at
715
        ? { connectedAt: new Date(String(row.connected_at)).toISOString() }
716
        : {}),
717
    };
718
  }
719
720
  async createProviderConnection(operatorId: string, provider: 'botpress', agentName: string) {
721
    await this.ensureSchema();
722
    await this.sql`
723
      DELETE FROM openclasp_provider_connections
724
      WHERE expires_at < NOW() - INTERVAL '7 days'
725
    `;
726
    const { connection, code } = createProviderConnectionInput(operatorId, provider, agentName);
727
    await this.sql`
728
      INSERT INTO openclasp_provider_connections(
729
        connection_id, operator_id, provider, agent_name, code_hash, status, expires_at, created_at
730
      ) VALUES (
731
        ${connection.connectionId}, ${connection.operatorId}, ${connection.provider},
732
        ${connection.agentName}, ${connection.codeHash}, 'pending',
733
        ${connection.expiresAt}, ${connection.createdAt}
734
      )
735
    `;
736
    return { ...publicProviderConnection(connection), code };
737
  }
738
739
  async getProviderConnection(operatorId: string, connectionId: string) {
740
    await this.ensureSchema();
741
    const rows = await this.sql`
742
      SELECT * FROM openclasp_provider_connections
743
      WHERE connection_id = ${connectionId} AND operator_id = ${operatorId}
744
      LIMIT 1
745
    `;
746
    if (!rows[0]) throw new Error('Provider connection not found');
747
    const connection = this.providerConnectionFromRow(rows[0]);
748
    if (connection.status === 'pending' && Date.parse(connection.expiresAt) <= Date.now()) {
749
      await this.sql`
750
        UPDATE openclasp_provider_connections SET status = 'expired'
751
        WHERE connection_id = ${connectionId} AND status = 'pending'
752
      `;
753
      connection.status = 'expired';
754
    }
755
    return publicProviderConnection(connection);
756
  }
757
758
  async completeBotpressConnection(
759
    code: string,
760
    input: {
761
      runtimeEndpoint: string;
762
      credentialPublicKey: string;
763
      profile: import('../../protocol/src/index.js').ConnectorAgentProfile;
764
    },
765
  ) {
766
    await this.ensureSchema();
767
    const codeHash = hashProviderConnectionCode(code);
768
    const rows = await this.sql`
769
      SELECT * FROM openclasp_provider_connections
770
      WHERE code_hash = ${codeHash} AND provider = 'botpress'
771
      LIMIT 1
772
    `;
773
    if (!rows[0]) throw new Error('Invalid Botpress pairing code');
774
    const connection = this.providerConnectionFromRow(rows[0]);
775
    if (Date.parse(connection.expiresAt) <= Date.now()) {
776
      await this.sql`
777
        UPDATE openclasp_provider_connections SET status = 'expired'
778
        WHERE connection_id = ${connection.connectionId} AND status = 'pending'
779
      `;
780
      throw new Error('Botpress pairing code expired');
781
    }
782
    if (connection.status === 'connected') {
783
      if (!connection.agentId || !connection.credentialCiphertext)
784
        throw new Error('Botpress connection is incomplete');
785
      return {
786
        agentId: connection.agentId,
787
        credentialCiphertext: connection.credentialCiphertext,
788
        status: 'connected' as const,
789
      };
790
    }
791
    if (connection.status !== 'pending') throw new Error('Botpress pairing code is unavailable');
792
    await resolvePublicRuntimeEndpoint(input.runtimeEndpoint);
793
    const { agent } = await createConnectorAgent(
794
      this,
795
      connection.operatorId,
796
      connection.agentName,
797
      { ...input.profile, framework: 'Botpress' },
798
    );
799
    const accessToken = await this.issueAgentAccessToken(connection.operatorId, agent.agentId, {
800
      name: 'Botpress',
801
      expiresInDays: 365,
802
    });
803
    const credentialCiphertext = encryptConnectorCredential(
804
      input.credentialPublicKey,
805
      accessToken.token,
806
    );
807
    await this.sql`
808
      UPDATE openclasp_provider_connections
809
      SET status = 'connected', agent_id = ${agent.agentId},
810
          runtime_endpoint = ${input.runtimeEndpoint},
811
          credential_public_key = ${input.credentialPublicKey},
812
          credential_ciphertext = ${credentialCiphertext}, connected_at = NOW()
813
      WHERE connection_id = ${connection.connectionId} AND status = 'pending'
814
    `;
815
    return { agentId: agent.agentId, credentialCiphertext, status: 'connected' as const };
816
  }
817
818
  async createConnectorClaim(input: {
819
    runtimeEndpoint: string;
820
    credentialPublicKey: string;
821
    profile: import('../../protocol/src/index.js').ConnectorAgentProfile;
822
  }) {
823
    await this.ensureSchema();
824
    await this.sql`
825
      DELETE FROM openclasp_connector_claims
826
      WHERE expires_at < NOW() - INTERVAL '7 days'
827
    `;
828
    await resolvePublicRuntimeEndpoint(input.runtimeEndpoint);
829
    const { claim, claimSecret } = createConnectorClaimInput(input);
830
    await this.sql`
831
      INSERT INTO openclasp_connector_claims(
832
        claim_id, secret_hash, runtime_endpoint, credential_public_key, profile,
833
        status, expires_at, created_at
834
      ) VALUES (
835
        ${claim.claimId}, ${claim.secretHash}, ${claim.runtimeEndpoint},
836
        ${claim.credentialPublicKey}, ${JSON.stringify(claim.profile)}::jsonb,
837
        'pending', ${claim.expiresAt}, ${claim.createdAt}
838
      )
839
    `;
840
    return { ...publicConnectorClaim(claim), claimSecret };
841
  }
842
843
  async getConnectorClaim(claimId: string) {
844
    await this.ensureSchema();
845
    const rows = await this.sql`
846
      SELECT * FROM openclasp_connector_claims WHERE claim_id = ${claimId} LIMIT 1
847
    `;
848
    if (!rows[0]) throw new Error('Connector claim not found');
849
    const claim = this.connectorClaimFromRow(rows[0]);
850
    if (
851
      (claim.status === 'pending' || claim.status === 'approved') &&
852
      connectorClaimExpired(claim)
853
    ) {
854
      await this.sql`
855
        UPDATE openclasp_connector_claims SET status = 'expired', credential_ciphertext = NULL
856
        WHERE claim_id = ${claimId} AND status IN ('pending', 'approved')
857
      `;
858
      if (claim.agentId)
859
        await this.sql`
860
          UPDATE openclasp_agent_access_tokens SET revoked_at = COALESCE(revoked_at, NOW())
861
          WHERE agent_id = ${claim.agentId} AND name = 'Runtime connector'
862
        `;
863
      claim.status = 'expired';
864
    }
865
    return publicConnectorClaim(claim);
866
  }
867
868
  async pollConnectorClaim(claimId: string, claimSecret: string) {
869
    await this.ensureSchema();
870
    const rows = await this.sql`
871
      SELECT * FROM openclasp_connector_claims WHERE claim_id = ${claimId} LIMIT 1
872
    `;
873
    if (!rows[0]) throw new Error('Connector claim not found');
874
    const claim = this.connectorClaimFromRow(rows[0]);
875
    if (!matchesConnectorClaimSecret(claim.secretHash, claimSecret))
876
      throw new Error('Invalid connector claim secret');
877
    if (
878
      (claim.status === 'pending' || claim.status === 'approved') &&
879
      connectorClaimExpired(claim)
880
    ) {
881
      await this.sql`
882
        UPDATE openclasp_connector_claims SET status = 'expired', credential_ciphertext = NULL
883
        WHERE claim_id = ${claimId} AND status IN ('pending', 'approved')
884
      `;
885
      if (claim.agentId)
886
        await this.sql`
887
          UPDATE openclasp_agent_access_tokens SET revoked_at = COALESCE(revoked_at, NOW())
888
          WHERE agent_id = ${claim.agentId} AND name = 'Runtime connector'
889
        `;
890
      return { status: 'expired' as const };
891
    }
892
    return {
893
      status: claim.status,
894
      ...(claim.agentId ? { agentId: claim.agentId } : {}),
895
      ...(claim.credentialCiphertext ? { credentialCiphertext: claim.credentialCiphertext } : {}),
896
    };
897
  }
898
899
  async approveConnectorClaim(operatorId: string, claimId: string, name: string) {
900
    await this.ensureSchema();
901
    const rows = await this.sql`
902
      SELECT * FROM openclasp_connector_claims WHERE claim_id = ${claimId} LIMIT 1
903
    `;
904
    if (!rows[0]) throw new Error('Connector claim not found');
905
    const claim = this.connectorClaimFromRow(rows[0]);
906
    if (connectorClaimExpired(claim)) throw new Error('Connector claim expired');
907
    if (claim.status === 'rejected') throw new Error('Connector claim was rejected');
908
    if (claim.status !== 'pending') {
909
      if (claim.operatorId !== operatorId)
910
        throw new Error('Connector claim belongs to another account');
911
      return publicConnectorClaim(claim);
912
    }
913
    const reserved = await this.sql`
914
      UPDATE openclasp_connector_claims
915
      SET status = 'approved', operator_id = ${operatorId}, decided_at = NOW()
916
      WHERE claim_id = ${claimId} AND status = 'pending' AND expires_at > NOW()
917
      RETURNING claim_id
918
    `;
919
    if (!reserved.length) throw new Error('Connector claim is no longer pending');
920
    try {
921
      const { agent } = await createConnectorAgent(this, operatorId, name, claim.profile);
922
      const accessToken = await this.issueAgentAccessToken(operatorId, agent.agentId, {
923
        name: 'Runtime connector',
924
        expiresInDays: 365,
925
      });
926
      const credentialCiphertext = encryptConnectorCredential(
927
        claim.credentialPublicKey,
928
        accessToken.token,
929
      );
930
      await this.sql`
931
        UPDATE openclasp_connector_claims
932
        SET agent_id = ${agent.agentId}, credential_ciphertext = ${credentialCiphertext}
933
        WHERE claim_id = ${claimId} AND operator_id = ${operatorId}
934
      `;
935
      return {
936
        ...publicConnectorClaim({
937
          ...claim,
938
          status: 'approved',
939
          operatorId,
940
          agentId: agent.agentId,
941
          credentialCiphertext,
942
          decidedAt: new Date().toISOString(),
943
        }),
944
        agent,
945
      };
946
    } catch (error) {
947
      await this.sql`
948
        UPDATE openclasp_connector_claims
949
        SET status = 'rejected', credential_ciphertext = NULL
950
        WHERE claim_id = ${claimId} AND operator_id = ${operatorId} AND agent_id IS NULL
951
      `;
952
      throw error;
953
    }
954
  }
955
956
  async rejectConnectorClaim(operatorId: string, claimId: string) {
957
    await this.ensureSchema();
958
    const rows = await this.sql`
959
      UPDATE openclasp_connector_claims
960
      SET status = 'rejected', operator_id = ${operatorId}, decided_at = NOW()
961
      WHERE claim_id = ${claimId} AND status = 'pending' AND expires_at > NOW()
962
      RETURNING *
963
    `;
964
    if (!rows[0]) throw new Error('Connector claim is no longer pending');
965
    return publicConnectorClaim(this.connectorClaimFromRow(rows[0]));
966
  }
967
968
  async completeConnectorClaim(agentId: string) {
969
    await this.ensureSchema();
970
    await this.sql`
971
      UPDATE openclasp_connector_claims
972
      SET status = 'connected', credential_ciphertext = NULL, connected_at = NOW()
973
      WHERE agent_id = ${agentId} AND status = 'approved'
974
    `;
975
  }
976
977
  private async appendSourceRecord(
978
    operatorId: string,
979
    kind: string,
980
    recordId: string,
981
    payload: unknown,
982
    metadata?: SourceRecordWriteMetadata,
983
  ) {
984
    const source = buildSourceRecordEnvelope({
985
      operatorId,
986
      kind,
987
      recordId,
988
      payload,
989
      ...(metadata ? { metadata } : {}),
990
    });
991
    await this.sql`
992
      INSERT INTO openclasp_source_records(
993
        event_id, operator_id, kind, record_id, schema_name, schema_version, payload,
994
        payload_digest, entity_refs, provenance, visibility, retention_class, learning_scope,
995
        reported_at, ingested_at
996
      ) VALUES (
997
        ${source.eventId}, ${source.operatorId}, ${source.kind}, ${source.recordId},
998
        ${source.schemaName}, ${source.schemaVersion}, ${JSON.stringify(source.payload)}::jsonb,
999
        ${source.payloadDigest}, ${JSON.stringify(source.entityRefs)}::jsonb,
1000
        ${source.provenance}, ${source.visibility}, ${source.retentionClass},
1001
        ${source.learningScope}, ${source.reportedAt}, ${source.ingestedAt}
1002
      )
1003
      ON CONFLICT (operator_id, kind, record_id, payload_digest) DO NOTHING
1004
    `;
1005
  }
1006
1007
  private async interactionOperatorIds(interactionId: string): Promise<string[]> {
1008
    const rows = await this.sql`
1009
      SELECT initiator_operator_id, responder_operator_id
1010
      FROM openclasp_federated_interactions
1011
      WHERE interaction_id = ${interactionId}
1012
      LIMIT 1
1013
    `;
1014
    if (!rows[0]) return [];
1015
    return [
1016
      ...new Set([String(rows[0].initiator_operator_id), String(rows[0].responder_operator_id)]),
1017
    ];
1018
  }
1019
1020
  private async journalFederatedInteraction(interactionId: string) {
1021
    const rows = await this.sql`
1022
      SELECT payload, initiator_operator_id, responder_operator_id
1023
      FROM openclasp_federated_interactions
1024
      WHERE interaction_id = ${interactionId}
1025
      LIMIT 1
1026
    `;
1027
    if (!rows[0]) return;
1028
    const payload = FederatedInteractionSchema.parse(rows[0].payload);
1029
    const operatorIds = [
1030
      ...new Set([String(rows[0].initiator_operator_id), String(rows[0].responder_operator_id)]),
1031
    ];
1032
    await Promise.all(
1033
      operatorIds.map((operatorId) =>
1034
        this.appendSourceRecord(operatorId, 'federated_interaction', interactionId, payload, {
1035
          schemaName: 'openclasp.federated_interaction',
1036
          schemaVersion: '0.1',
1037
          visibility: 'shared_participants',
1038
          retentionClass: 'audit',
1039
        }),
1040
      ),
1041
    );
1042
  }
1043
1044
  private async journalLiveSessionState(interactionId: string) {
1045
    const rows = await this.sql`
1046
      SELECT session.interaction_id, session.initiator_agent_id, session.responder_agent_id,
1047
        session.status, session.expires_at, session.created_at, session.activated_at,
1048
        session.completed_at, interaction.initiator_operator_id,
1049
        interaction.responder_operator_id
1050
      FROM openclasp_live_sessions session
1051
      INNER JOIN openclasp_federated_interactions interaction
1052
        ON interaction.interaction_id = session.interaction_id
1053
      WHERE session.interaction_id = ${interactionId}
1054
      LIMIT 1
1055
    `;
1056
    if (!rows[0]) return;
1057
    const row = rows[0];
1058
    const payload = LiveSessionStateRecordSchema.parse({
1059
      interactionId: String(row.interaction_id),
1060
      initiatorAgentId: String(row.initiator_agent_id),
1061
      responderAgentId: String(row.responder_agent_id),
1062
      status: String(row.status),
1063
      expiresAt: new Date(String(row.expires_at)).toISOString(),
1064
      createdAt: new Date(String(row.created_at)).toISOString(),
1065
      ...(row.activated_at
1066
        ? { activatedAt: new Date(String(row.activated_at)).toISOString() }
1067
        : {}),
1068
      ...(row.completed_at
1069
        ? { completedAt: new Date(String(row.completed_at)).toISOString() }
1070
        : {}),
1071
      ...(row.status === 'failed' ? { failureCode: 'session_failed' } : {}),
1072
    });
1073
    const operatorIds = [
1074
      ...new Set([String(row.initiator_operator_id), String(row.responder_operator_id)]),
1075
    ];
1076
    await Promise.all(
1077
      operatorIds.map((operatorId) =>
1078
        this.appendSourceRecord(operatorId, 'live_session_state', interactionId, payload, {
1079
          schemaName: 'openclasp.live_session_state',
1080
          schemaVersion: '0.1',
1081
          visibility: 'shared_participants',
1082
          retentionClass: 'audit',
1083
          reportedAt: payload.completedAt ?? payload.activatedAt ?? payload.createdAt,
1084
        }),
1085
      ),
1086
    );
1087
  }
1088
1089
  private async journalLiveSessionEvent(payload: Record<string, unknown>) {
1090
    const interactionId = String(payload.interactionId ?? '');
1091
    const eventId = String(payload.eventId ?? '');
1092
    if (!interactionId || !eventId) return;
1093
    const operatorIds = await this.interactionOperatorIds(interactionId);
1094
    await Promise.all(
1095
      operatorIds.map((operatorId) =>
1096
        this.appendSourceRecord(operatorId, 'live_session_event', eventId, payload, {
1097
          schemaName: 'openclasp.live_session_event',
1098
          schemaVersion: '0.1',
1099
          visibility: 'shared_participants',
1100
          retentionClass: 'audit',
1101
        }),
1102
      ),
1103
    );
1104
  }
1105
1106
  getRuntimeVerificationKey() {
1107
    const [active, ...previous] = this.gatewaySecrets();
1108
    return {
1109
      algorithm: 'Ed25519' as const,
1110
      keyId: getSessionKeyId(active!),
1111
      publicKey: getSessionVerificationKey(active!),
1112
      previousKeys: previous.map((secret) => ({
1113
        algorithm: 'Ed25519' as const,
1114
        keyId: getSessionKeyId(secret),
1115
        publicKey: getSessionVerificationKey(secret),
1116
      })),
1117
    };
1118
  }
1119
1120
  async upsert(
1121
    operatorId: string,
1122
    kind: HostedRecordKind,
1123
    recordId: string,
1124
    payload: unknown,
1125
    metadata?: SourceRecordWriteMetadata,
1126
  ): Promise<void> {
1127
    await this.ensureSchema();
1128
    const encoded = JSON.stringify(payload);
1129
    if (!(metadata?.journal ?? shouldJournalSourceRecord(kind))) {
1130
      await this.sql`
1131
        INSERT INTO openclasp_records(operator_id, kind, record_id, payload)
1132
        VALUES (${operatorId}, ${kind}, ${recordId}, ${encoded}::jsonb)
1133
        ON CONFLICT (operator_id, kind, record_id)
1134
        DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()
1135
      `;
1136
      return;
1137
    }
1138
    const source = buildSourceRecordEnvelope({
1139
      operatorId,
1140
      kind,
1141
      recordId,
1142
      payload,
1143
      ...(metadata ? { metadata } : {}),
1144
    });
1145
    const sourcePayload = JSON.stringify(source.payload);
1146
    const sourceEntityRefs = JSON.stringify(source.entityRefs);
1147
    await this.sql.transaction((transaction) => [
1148
      transaction`
1149
        INSERT INTO openclasp_records(operator_id, kind, record_id, payload)
1150
        VALUES (${operatorId}, ${kind}, ${recordId}, ${encoded}::jsonb)
1151
        ON CONFLICT (operator_id, kind, record_id)
1152
        DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()
1153
      `,
1154
      transaction`
1155
        INSERT INTO openclasp_source_records(
1156
          event_id,
1157
          operator_id,
1158
          kind,
1159
          record_id,
1160
          schema_name,
1161
          schema_version,
1162
          payload,
1163
          payload_digest,
1164
          entity_refs,
1165
          provenance,
1166
          visibility,
1167
          retention_class,
1168
          learning_scope,
1169
          reported_at,
1170
          ingested_at
1171
        ) VALUES (
1172
          ${source.eventId},
1173
          ${source.operatorId},
1174
          ${source.kind},
1175
          ${source.recordId},
1176
          ${source.schemaName},
1177
          ${source.schemaVersion},
1178
          ${sourcePayload}::jsonb,
1179
          ${source.payloadDigest},
1180
          ${sourceEntityRefs}::jsonb,
1181
          ${source.provenance},
1182
          ${source.visibility},
1183
          ${source.retentionClass},
1184
          ${source.learningScope},
1185
          ${source.reportedAt},
1186
          ${source.ingestedAt}
1187
        )
1188
        ON CONFLICT (operator_id, kind, record_id, payload_digest) DO NOTHING
1189
      `,
1190
    ]);
1191
  }
1192
1193
  async list(
1194
    operatorId: string,
1195
  ): Promise<{ kind: HostedRecordKind; recordId: string; payload: any }[]> {
1196
    await this.ensureSchema();
1197
    const rows = await this.sql`
1198
      SELECT kind, record_id, payload
1199
      FROM openclasp_records
1200
      WHERE operator_id = ${operatorId}
1201
      ORDER BY created_at ASC
1202
    `;
1203
    return rows.map((row) => ({
1204
      kind: row.kind as HostedRecordKind,
1205
      recordId: String(row.record_id),
1206
      payload: row.payload,
1207
    }));
1208
  }
1209
1210
  async saveShieldCase(operatorId: string, value: ShieldCase): Promise<ShieldCase> {
1211
    const caseRecord = ShieldCaseSchema.parse(value);
1212
    await this.upsert(operatorId, 'shield_case', caseRecord.caseId, caseRecord, {
1213
      journal: true,
1214
      schemaName: 'openclasp.shield.case',
1215
      schemaVersion: '1',
1216
      entityRefs: { caseId: caseRecord.caseId, agentId: caseRecord.agentId },
1217
      retentionClass: 'audit',
1218
      learningScope: 'local_only',
1219
    });
1220
    return caseRecord;
1221
  }
1222
1223
  async getShieldCase(operatorId: string, caseId: string): Promise<ShieldCase | undefined> {
1224
    const row = (await this.list(operatorId)).find(
1225
      (record) => record.kind === 'shield_case' && record.recordId === caseId,
1226
    );
1227
    if (!row) return undefined;
1228
    return ShieldCaseSchema.parse(row.payload);
1229
  }
1230
1231
  async listShieldCases(operatorId: string, agentId?: string): Promise<ShieldCase[]> {
1232
    return (await this.list(operatorId))
1233
      .filter((record) => record.kind === 'shield_case')
1234
      .map((record) => ShieldCaseSchema.parse(record.payload))
1235
      .filter((caseRecord) => !agentId || caseRecord.agentId === agentId)
1236
      .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1237
  }
1238
1239
  async saveShieldConsultation(
1240
    operatorId: string,
1241
    value: ShieldConsultation,
1242
  ): Promise<ShieldConsultation> {
1243
    const consultation = ShieldConsultationSchema.parse(value);
1244
    await this.upsert(
1245
      operatorId,
1246
      'shield_consultation',
1247
      consultation.consultationId,
1248
      consultation,
1249
      {
1250
        journal: true,
1251
        schemaName: 'openclasp.shield.consultation',
1252
        schemaVersion: '1',
1253
        entityRefs: {
1254
          caseId: consultation.caseId,
1255
          agentId: consultation.agentId,
1256
          consultationId: consultation.consultationId,
1257
        },
1258
        retentionClass: 'audit',
1259
        learningScope: 'local_only',
1260
      },
1261
    );
1262
    return consultation;
1263
  }
1264
1265
  async listShieldConsultations(operatorId: string, caseId: string): Promise<ShieldConsultation[]> {
1266
    return (await this.list(operatorId))
1267
      .filter((record) => record.kind === 'shield_consultation')
1268
      .map((record) => ShieldConsultationSchema.parse(record.payload))
1269
      .filter((consultation) => consultation.caseId === caseId)
1270
      .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1271
  }
1272
1273
  async saveShieldOutcome(operatorId: string, value: ShieldOutcome): Promise<ShieldOutcome> {
1274
    const outcome = ShieldOutcomeSchema.parse(value);
1275
    await this.upsert(operatorId, 'shield_outcome', outcome.outcomeId, outcome, {
1276
      journal: true,
1277
      schemaName: 'openclasp.shield.outcome',
1278
      schemaVersion: '1',
1279
      entityRefs: {
1280
        caseId: outcome.caseId,
1281
        agentId: outcome.agentId,
1282
        outcomeId: outcome.outcomeId,
1283
      },
1284
      retentionClass: 'audit',
1285
      learningScope: 'local_only',
1286
    });
1287
    return outcome;
1288
  }
1289
1290
  async dashboard(operatorId: string) {
1291
    await this.ensureSchema();
1292
    const dueFeedback = await this.sql`
1293
      SELECT 1 FROM openclasp_records
1294
      WHERE operator_id = ${operatorId}
1295
        AND kind = 'feedback_request'
1296
        AND payload->>'status' = 'pending'
1297
        AND (payload->>'dueAt')::timestamptz <= NOW()
1298
      LIMIT 1
1299
    `;
1300
    if (dueFeedback.length) await this.processDueFeedback().catch(() => undefined);
1301
    const outstanding = await this.sql`
1302
      SELECT DISTINCT report.payload->>'interactionId' AS interaction_id
1303
      FROM openclasp_records report
1304
      INNER JOIN openclasp_federated_interactions interaction
1305
        ON interaction.interaction_id::text = report.payload->>'interactionId'
1306
      WHERE report.kind = 'completion_report'
1307
        AND (
1308
          interaction.initiator_operator_id = ${operatorId}
1309
          OR interaction.responder_operator_id = ${operatorId}
1310
        )
1311
        AND NOT EXISTS (
1312
          SELECT 1 FROM openclasp_records conclusion
1313
          WHERE conclusion.operator_id = ${operatorId}
1314
            AND conclusion.kind = 'interaction_conclusion'
1315
            AND conclusion.payload->>'interactionId' = report.payload->>'interactionId'
1316
        )
1317
      LIMIT 20
1318
    `;
1319
    for (const row of outstanding) {
1320
      const interactionId = String(row.interaction_id ?? '');
1321
      if (interactionId)
1322
        await this.finalizeInteractionConclusion(interactionId).catch(() => undefined);
1323
    }
1324
    const intelligenceBackfill = await this.sql`
1325
      SELECT DISTINCT interaction.interaction_id
1326
      FROM openclasp_federated_interactions interaction
1327
      INNER JOIN openclasp_records conclusion
1328
        ON conclusion.payload->>'interactionId' = interaction.interaction_id::text
1329
        AND conclusion.kind = 'interaction_conclusion'
1330
        AND conclusion.payload->>'lifecycle' = 'final'
1331
      WHERE (
1332
          interaction.initiator_operator_id = ${operatorId}
1333
          OR interaction.responder_operator_id = ${operatorId}
1334
        )
1335
        AND EXISTS (
1336
          SELECT 1 FROM openclasp_records eligibility
1337
          WHERE eligibility.kind = 'learning_eligibility'
1338
            AND eligibility.payload->>'interactionId' = interaction.interaction_id::text
1339
            AND eligibility.payload->>'eligible' = 'true'
1340
        )
1341
        AND NOT EXISTS (
1342
          SELECT 1 FROM openclasp_records delta
1343
          WHERE delta.operator_id = ${operatorId}
1344
            AND delta.kind = 'profile_delta'
1345
            AND delta.payload->>'interactionId' = interaction.interaction_id::text
1346
            AND delta.payload->>'agentId' = CASE
1347
              WHEN interaction.initiator_operator_id = ${operatorId}
1348
                THEN interaction.initiator_agent_id::text
1349
              ELSE interaction.responder_agent_id::text
1350
            END
1351
        )
1352
      ORDER BY interaction.interaction_id DESC
1353
      LIMIT 20
1354
    `;
1355
    for (const row of intelligenceBackfill) {
1356
      const interactionId = String(row.interaction_id ?? '');
1357
      if (interactionId)
1358
        await this.finalizeInteractionConclusion(interactionId).catch(() => undefined);
1359
    }
1360
    const rows = await this.list(operatorId);
1361
    const [
1362
      federatedInteractions,
1363
      runtimes,
1364
      accessTokens,
1365
      liveSessionRows,
1366
      liveEventRows,
1367
      assuranceAssessmentRows,
1368
      assurancePredictionRows,
1369
      assuranceSafeguardRows,
1370
      assuranceEvaluationRows,
1371
      assurancePlanRows,
1372
      assuranceResponseRows,
1373
    ] = await Promise.all([
1374
      this.listFederatedInteractions(operatorId),
1375
      this.listAgentRuntimes(operatorId),
1376
      this.listAgentAccessTokens(operatorId),
1377
      this.sql`
1378
        SELECT session.interaction_id, session.initiator_agent_id, session.responder_agent_id,
1379
          session.status, session.created_at, session.activated_at, session.completed_at,
1380
          session.expires_at, session.last_error
1381
        FROM openclasp_live_sessions session
1382
        INNER JOIN openclasp_federated_interactions interaction
1383
          ON interaction.interaction_id = session.interaction_id
1384
        WHERE interaction.initiator_operator_id = ${operatorId}
1385
           OR interaction.responder_operator_id = ${operatorId}
1386
        ORDER BY session.created_at DESC
1387
      `,
1388
      this.sql`
1389
        SELECT events.event
1390
        FROM openclasp_live_session_events events
1391
        INNER JOIN openclasp_federated_interactions interaction
1392
          ON interaction.interaction_id = events.interaction_id
1393
        WHERE interaction.initiator_operator_id = ${operatorId}
1394
           OR interaction.responder_operator_id = ${operatorId}
1395
        ORDER BY events.created_at ASC
1396
      `,
1397
      this
1398
        .sql`SELECT payload FROM openclasp_assurance_assessments WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
1399
      this
1400
        .sql`SELECT payload FROM openclasp_assurance_predictions WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
1401
      this
1402
        .sql`SELECT payload FROM openclasp_assurance_safeguards WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
1403
      this
1404
        .sql`SELECT payload FROM openclasp_assurance_evaluations WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
1405
      this
1406
        .sql`SELECT payload FROM openclasp_assurance_probe_plans WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
1407
      this
1408
        .sql`SELECT responses.payload FROM openclasp_assurance_probe_responses responses INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id WHERE plans.operator_id = ${operatorId} ORDER BY responses.created_at ASC`,
1409
    ]);
1410
    const ofKind = (kind: HostedRecordKind) =>
1411
      rows.filter((row) => row.kind === kind).map((row) => row.payload);
1412
    const presence = new Map(
1413
      rows
1414
        .filter((row) => row.kind === 'presence')
1415
        .map((row) => [row.recordId, String(row.payload.lastSeenAt)]),
1416
    );
1417
    const intelligenceSummaries = await this.listContextualIntelligence(operatorId);
1418
    return {
1419
      agents: [...ofKind('agent_profile'), ...ofKind('agent')].map((agent) => ({
1420
        ...agent,
1421
        presence: resolveAgentPresence(presence.get(String(agent.agentId))),
1422
      })),
1423
      projects: ofKind('project'),
1424
      installations: ofKind('installation'),
1425
      setupRequests: ofKind('setup_request'),
1426
      publications: ofKind('publication'),
1427
      interactions: ofKind('interaction'),
1428
      events: [...ofKind('event'), ...liveEventRows.map((row) => row.event)],
1429
      conflicts: ofKind('conflict'),
1430
      receipts: ofKind('receipt'),
1431
      profiles: ofKind('profile'),
1432
      counterpartyBriefs: ofKind('counterparty_brief'),
1433
      completionReports: ofKind('completion_report'),
1434
      feedbackRequests: ofKind('feedback_request'),
1435
      interactionFeedback: ofKind('interaction_feedback'),
1436
      interactionConclusions: ofKind('interaction_conclusion'),
1437
      learningEligibility: ofKind('learning_eligibility'),
1438
      profileDeltas: ofKind('profile_delta'),
1439
      intelligenceSummaries,
1440
      assuranceAssessments: assuranceAssessmentRows.map((row) => row.payload),
1441
      assurancePredictions: assurancePredictionRows.map((row) => row.payload),
1442
      assuranceSafeguards: assuranceSafeguardRows.map((row) => row.payload),
1443
      assuranceEvaluations: assuranceEvaluationRows.map((row) => row.payload),
1444
      assuranceProbePlans: assurancePlanRows.map((row) => row.payload),
1445
      assuranceProbeResponses: assuranceResponseRows.map((row) => row.payload),
1446
      shieldCases: ofKind('shield_case'),
1447
      shieldConsultations: ofKind('shield_consultation'),
1448
      shieldOutcomes: ofKind('shield_outcome'),
1449
      federatedInteractions,
1450
      liveSessions: liveSessionRows.map((row) => ({
1451
        interactionId: String(row.interaction_id),
1452
        initiatorAgentId: String(row.initiator_agent_id),
1453
        responderAgentId: String(row.responder_agent_id),
1454
        status: String(row.status),
1455
        createdAt: new Date(String(row.created_at)).toISOString(),
1456
        ...(row.activated_at
1457
          ? { activatedAt: new Date(String(row.activated_at)).toISOString() }
1458
          : {}),
1459
        ...(row.completed_at
1460
          ? { completedAt: new Date(String(row.completed_at)).toISOString() }
1461
          : {}),
1462
        expiresAt: new Date(String(row.expires_at)).toISOString(),
1463
        ...(row.last_error ? { lastError: String(row.last_error) } : {}),
1464
      })),
1465
      runtimes,
1466
      accessTokens,
1467
    };
1468
  }
1469
1470
  async listContextualIntelligence(
1471
    operatorId: string,
1472
    input: { agentId?: string; taskCategory?: string } = {},
1473
  ) {
1474
    await this.ensureSchema();
1475
    const [records, publicRows] = await Promise.all([
1476
      this.list(operatorId),
1477
      this
1478
        .sql`SELECT agent_id, card FROM openclasp_public_agents ORDER BY updated_at DESC LIMIT 1000`,
1479
    ]);
1480
    const currentVersions = new Map<string, string>();
1481
    for (const row of publicRows) {
1482
      const card = PublicAgentCardSchema.safeParse(normalizePublicAgentCard(row.card));
1483
      if (card.success) currentVersions.set(String(row.agent_id), card.data.agentVersion);
1484
    }
1485
    for (const record of records.filter((item) => item.kind === 'agent_profile')) {
1486
      const profile = record.payload as Partial<AgentProfile>;
1487
      if (typeof profile.agentId === 'string' && typeof profile.agentVersion === 'string')
1488
        currentVersions.set(profile.agentId, profile.agentVersion);
1489
    }
1490
    const deltas = records
1491
      .filter((record) => record.kind === 'profile_delta')
1492
      .map((record) => BehaviouralProfileDeltaSchema.safeParse(record.payload))
1493
      .filter((result) => result.success)
1494
      .map((result) => result.data);
1495
    return records
1496
      .filter((record) => record.kind === 'profile')
1497
      .map((record) => ({ recordId: record.recordId, ...record.payload }) as ContextualProfile)
1498
      .filter(
1499
        (profile) =>
1500
          (!input.agentId || profile.agentId === input.agentId) &&
1501
          (!input.taskCategory || profile.taskCategory === input.taskCategory),
1502
      )
1503
      .map((profile) =>
1504
        summarizeContextualReliability({
1505
          profile: {
1506
            ...profile,
1507
            effectiveSampleSize: profile.effectiveSampleSize ?? profile.sampleSize,
1508
          },
1509
          deltas: deltas.filter(
1510
            (delta) =>
1511
              delta.agentId === profile.agentId &&
1512
              delta.agentVersion === profile.agentVersion &&
1513
              delta.taskCategory === profile.taskCategory,
1514
          ),
1515
          currentAgentVersion: currentVersions.get(profile.agentId) ?? profile.agentVersion,
1516
        }),
1517
      )
1518
      .sort((left, right) => {
1519
        if (left.agentId !== right.agentId) return left.agentId.localeCompare(right.agentId);
1520
        if (left.versionStatus.status !== right.versionStatus.status)
1521
          return left.versionStatus.status === 'current' ? -1 : 1;
1522
        return right.confidence.value - left.confidence.value;
1523
      });
1524
  }
1525
1526
  async issueAgentAccessToken(
1527
    operatorId: string,
1528
    agentId: string,
1529
    input: { name: string; expiresInDays: number; scopes?: OpenClaspAuthScope[] },
1530
  ): Promise<AgentAccessTokenMetadata & { token: string }> {
1531
    await this.ensureSchema();
1532
    const agents = await this.sql`
1533
      SELECT payload FROM openclasp_records
1534
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1535
      LIMIT 1
1536
    `;
1537
    const agent = agents[0]?.payload as AgentProfile | undefined;
1538
    if (!agent) throw new Error('Owned agent not found');
1539
    if (agent.status !== 'active') throw new Error('Revoked agents cannot receive access tokens');
1540
    const name = input.name.trim();
1541
    if (!name) throw new Error('Token name is required');
1542
    const { tokenId, token, tokenHash } = createAgentAccessToken();
1543
    const createdAt = new Date();
1544
    const expiresAt = new Date(createdAt.getTime() + input.expiresInDays * 86_400_000);
1545
    // The credential is bound to one agent. Callers may further reduce its default
1546
    // scopes for a specific integration, but cannot grant unsupported scopes.
1547
    const scopes = input.scopes ?? DEFAULT_AGENT_AUTH_SCOPES;
1548
    if (!scopes.length || scopes.some((scope) => !DEFAULT_AGENT_AUTH_SCOPES.includes(scope)))
1549
      throw new Error('Agent access token contains an unsupported scope');
1550
    await this.sql`
1551
      INSERT INTO openclasp_agent_access_tokens(
1552
        token_id, operator_id, agent_id, name, token_hash, scopes, expires_at, created_at
1553
      ) VALUES (
1554
        ${tokenId}, ${operatorId}, ${agentId}, ${name}, ${tokenHash},
1555
        ${JSON.stringify(scopes)}::jsonb, ${expiresAt.toISOString()}, ${createdAt.toISOString()}
1556
      )
1557
    `;
1558
    const clientId = agentAccessTokenClientId(tokenId);
1559
    await this.upsert(operatorId, 'installation', clientId, {
1560
      installationId: `install_${tokenId}`,
1561
      clientId,
1562
      agentId,
1563
      projectId: agent.projectId,
1564
      connectedAt: createdAt.toISOString(),
1565
      updatedAt: createdAt.toISOString(),
1566
    } satisfies AgentInstallation);
1567
    return {
1568
      tokenId,
1569
      token,
1570
      agentId,
1571
      name,
1572
      scopes,
1573
      createdAt: createdAt.toISOString(),
1574
      expiresAt: expiresAt.toISOString(),
1575
    };
1576
  }
1577
1578
  async listAgentAccessTokens(
1579
    operatorId: string,
1580
    agentId?: string,
1581
  ): Promise<AgentAccessTokenMetadata[]> {
1582
    await this.ensureSchema();
1583
    const rows = agentId
1584
      ? await this.sql`
1585
          SELECT token_id, agent_id, name, scopes, expires_at, last_used_at, revoked_at, created_at
1586
          FROM openclasp_agent_access_tokens
1587
          WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1588
          ORDER BY created_at DESC
1589
        `
1590
      : await this.sql`
1591
          SELECT token_id, agent_id, name, scopes, expires_at, last_used_at, revoked_at, created_at
1592
          FROM openclasp_agent_access_tokens
1593
          WHERE operator_id = ${operatorId}
1594
          ORDER BY created_at DESC
1595
        `;
1596
    return rows.map((row) => ({
1597
      tokenId: String(row.token_id),
1598
      agentId: String(row.agent_id),
1599
      name: String(row.name),
1600
      scopes: Array.isArray(row.scopes)
1601
        ? row.scopes.filter((scope): scope is string => typeof scope === 'string')
1602
        : [],
1603
      createdAt: new Date(String(row.created_at)).toISOString(),
1604
      expiresAt: new Date(String(row.expires_at)).toISOString(),
1605
      ...(row.last_used_at ? { lastUsedAt: new Date(String(row.last_used_at)).toISOString() } : {}),
1606
      ...(row.revoked_at ? { revokedAt: new Date(String(row.revoked_at)).toISOString() } : {}),
1607
    }));
1608
  }
1609
1610
  async revokeAgentAccessToken(operatorId: string, agentId: string, tokenId: string) {
1611
    await this.ensureSchema();
1612
    const rows = await this.sql`
1613
      UPDATE openclasp_agent_access_tokens
1614
      SET revoked_at = COALESCE(revoked_at, NOW())
1615
      WHERE token_id = ${tokenId} AND operator_id = ${operatorId} AND agent_id = ${agentId}
1616
      RETURNING revoked_at
1617
    `;
1618
    if (!rows.length) throw new Error('Agent access token not found');
1619
    const clientId = agentAccessTokenClientId(tokenId);
1620
    await this.sql`
1621
      DELETE FROM openclasp_records
1622
      WHERE operator_id = ${operatorId} AND kind = 'installation' AND record_id = ${clientId}
1623
    `;
1624
    return {
1625
      tokenId,
1626
      agentId,
1627
      revokedAt: new Date(String(rows[0]?.revoked_at)).toISOString(),
1628
    };
1629
  }
1630
1631
  async verifyAgentAccessToken(token: string) {
1632
    await this.ensureSchema();
1633
    const tokenId = agentAccessTokenId(token);
1634
    if (!tokenId) throw new Error('Invalid agent access token');
1635
    const rows = await this.sql`
1636
      SELECT tokens.operator_id, tokens.agent_id, tokens.token_hash, tokens.scopes,
1637
        tokens.expires_at, profile.payload AS profile
1638
      FROM openclasp_agent_access_tokens tokens
1639
      INNER JOIN openclasp_records profile
1640
        ON profile.operator_id = tokens.operator_id
1641
        AND profile.kind = 'agent_profile'
1642
        AND profile.record_id = tokens.agent_id
1643
      WHERE tokens.token_id = ${tokenId} AND tokens.revoked_at IS NULL
1644
      LIMIT 1
1645
    `;
1646
    const row = rows[0];
1647
    if (!row || !matchesAgentAccessToken(token, String(row.token_hash)))
1648
      throw new Error('Invalid agent access token');
1649
    if (Date.parse(String(row.expires_at)) <= Date.now())
1650
      throw new Error('Agent access token has expired');
1651
    const profile = row.profile as Partial<AgentProfile>;
1652
    if (profile.status !== 'active') throw new Error('Agent access token is disabled');
1653
    await this.sql`
1654
      UPDATE openclasp_agent_access_tokens
1655
      SET last_used_at = NOW()
1656
      WHERE token_id = ${tokenId}
1657
        AND (last_used_at IS NULL OR last_used_at < NOW() - INTERVAL '5 minutes')
1658
    `;
1659
    return {
1660
      tokenId,
1661
      operatorId: String(row.operator_id),
1662
      agentId: String(row.agent_id),
1663
      clientId: agentAccessTokenClientId(tokenId),
1664
      scopes: Array.isArray(row.scopes)
1665
        ? row.scopes.filter((scope): scope is string => typeof scope === 'string')
1666
        : [],
1667
      expiresAt: new Date(String(row.expires_at)).toISOString(),
1668
    };
1669
  }
1670
1671
  async publishAgent(operatorId: string, card: PublicAgentCard): Promise<PublicAgentCard> {
1672
    await this.ensureSchema();
1673
    card = PublicAgentCardSchema.parse(card);
1674
    const runtimes = await this.sql`
1675
      SELECT a2a_endpoint, endpoint FROM openclasp_agent_runtimes
1676
      WHERE operator_id = ${operatorId} AND agent_id = ${card.agentId} AND status = 'verified'
1677
    `;
1678
    if (!runtimes[0]) throw new Error('Connect and verify the agent runtime before publishing');
1679
    card = PublicAgentCardSchema.parse({
1680
      ...card,
1681
      agentMode: 'persistent_runtime',
1682
      transports: [
1683
        {
1684
          protocol: 'A2A/1.0',
1685
          protocolBinding: 'JSONRPC',
1686
          endpoint: String(runtimes[0].a2a_endpoint ?? runtimes[0].endpoint),
1687
          managedBy: 'agent',
1688
        },
1689
      ],
1690
    });
1691
    card = this.attestPublicAgentCard(card);
1692
    const encoded = JSON.stringify(card);
1693
    const rows = await this.sql`
1694
      INSERT INTO openclasp_public_agents(agent_id, operator_id, card, published_at, updated_at)
1695
      VALUES (${card.agentId}, ${operatorId}, ${encoded}::jsonb, NOW(), NOW())
1696
      ON CONFLICT (agent_id) DO UPDATE SET
1697
        card = EXCLUDED.card,
1698
        updated_at = NOW()
1699
      WHERE openclasp_public_agents.operator_id = ${operatorId}
1700
      RETURNING card
1701
    `;
1702
    const published = rows[0]?.card as PublicAgentCard | undefined;
1703
    if (!published) throw new Error('Agent ID is already owned by another operator');
1704
    return PublicAgentCardSchema.parse(published);
1705
  }
1706
1707
  async unpublishAgent(operatorId: string, agentId: string): Promise<boolean> {
1708
    await this.ensureSchema();
1709
    const rows = await this.sql`
1710
      DELETE FROM openclasp_public_agents
1711
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
1712
      RETURNING agent_id
1713
    `;
1714
    return rows.length > 0;
1715
  }
1716
1717
  async getPublishedAgent(agentId: string): Promise<PublicAgentCard | undefined> {
1718
    await this.ensureSchema();
1719
    const rows = await this.sql`
1720
      SELECT agents.card, runtime.a2a_endpoint, runtime.endpoint, profile.payload AS profile
1721
      FROM openclasp_public_agents agents
1722
      INNER JOIN openclasp_agent_runtimes runtime
1723
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
1724
      LEFT JOIN openclasp_records profile
1725
        ON profile.operator_id = agents.operator_id AND profile.kind = 'agent_profile'
1726
        AND profile.record_id = agents.agent_id
1727
      WHERE agents.agent_id = ${agentId}
1728
    `;
1729
    if (!rows[0]?.card) return undefined;
1730
    const card = PublicAgentCardSchema.parse({
1731
      ...normalizePublicAgentCard(rows[0].card),
1732
      agentMode: 'persistent_runtime',
1733
      transports: [
1734
        {
1735
          protocol: 'A2A/1.0',
1736
          protocolBinding: 'JSONRPC',
1737
          endpoint: String(rows[0].a2a_endpoint ?? rows[0].endpoint),
1738
          managedBy: 'agent',
1739
        },
1740
      ],
1741
    });
1742
    return this.attestPublicAgentCard({ ...card, presence: await this.getAgentPresence(agentId) });
1743
  }
1744
1745
  async resolveAgentReference(reference: string): Promise<AgentResolution | undefined> {
1746
    await this.ensureSchema();
1747
    const normalizedReference = reference.trim().replace(/\/$/, '');
1748
    if (!normalizedReference || normalizedReference.length > 2048)
1749
      throw new Error('Agent reference is invalid');
1750
    const rows = await this.sql`
1751
      SELECT agent_id, card
1752
      FROM openclasp_public_agents
1753
      WHERE agent_id = ${normalizedReference}
1754
        OR card->>'slug' = ${normalizedReference}
1755
        OR RTRIM(card->>'profileUrl', '/') = ${normalizedReference}
1756
        OR RTRIM(card->>'cardUrl', '/') = ${normalizedReference}
1757
        OR RTRIM(card->>'a2aAgentCardUrl', '/') = ${normalizedReference}
1758
      LIMIT 1
1759
    `;
1760
    let row = rows[0];
1761
    if (!row) {
1762
      const legacyRows = await this.sql`
1763
        SELECT agent_id, card
1764
        FROM openclasp_public_agents
1765
        ORDER BY updated_at DESC
1766
        LIMIT 1000
1767
      `;
1768
      row = legacyRows.find((candidate) => {
1769
        const card = normalizePublicAgentCard(candidate.card);
1770
        return [card.slug, card.profileUrl, card.cardUrl, card.a2aAgentCardUrl]
1771
          .filter((value): value is string => typeof value === 'string')
1772
          .some((value) => value.replace(/\/$/, '') === normalizedReference);
1773
      });
1774
    }
1775
    if (!row) return undefined;
1776
    const stored = normalizePublicAgentCard(row.card);
1777
    const matchedBy =
1778
      String(row.agent_id) === normalizedReference
1779
        ? 'agent_id'
1780
        : stored.slug === normalizedReference
1781
          ? 'slug'
1782
          : stored.profileUrl?.replace(/\/$/, '') === normalizedReference
1783
            ? 'profile_url'
1784
            : stored.cardUrl.replace(/\/$/, '') === normalizedReference
1785
              ? 'card_url'
1786
              : 'a2a_card_url';
1787
    const card = await this.getPublishedAgent(String(row.agent_id));
1788
    if (!card) return undefined;
1789
    return AgentResolutionSchema.parse({
1790
      reference,
1791
      matchedBy,
1792
      verified: true,
1793
      card,
1794
      resolvedAt: new Date().toISOString(),
1795
    });
1796
  }
1797
1798
  async searchPublishedAgents(input: {
1799
    query?: string | undefined;
1800
    capability?: string | undefined;
1801
    limit?: number | undefined;
1802
  }): Promise<PublicAgentCard[]> {
1803
    await this.ensureSchema();
1804
    const rows = await this.sql`
1805
      SELECT agents.card, runtime.a2a_endpoint, runtime.endpoint, profile.payload AS profile
1806
      FROM openclasp_public_agents agents
1807
      INNER JOIN openclasp_agent_runtimes runtime
1808
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
1809
      LEFT JOIN openclasp_records profile
1810
        ON profile.operator_id = agents.operator_id AND profile.kind = 'agent_profile'
1811
        AND profile.record_id = agents.agent_id
1812
      ORDER BY agents.updated_at DESC LIMIT 100
1813
    `;
1814
    const query = input.query?.trim().toLowerCase();
1815
    const capability = input.capability?.trim().toLowerCase();
1816
    const presenceRows = await this.sql`
1817
      SELECT records.record_id, records.payload
1818
      FROM openclasp_records records
1819
      INNER JOIN openclasp_public_agents agents ON agents.agent_id = records.record_id
1820
      WHERE records.kind = 'presence'
1821
    `;
1822
    const presence = new Map(
1823
      presenceRows.map((row) => [String(row.record_id), String(row.payload.lastSeenAt)]),
1824
    );
1825
    return rows
1826
      .map((row) => {
1827
        const card = PublicAgentCardSchema.parse({
1828
          ...normalizePublicAgentCard(row.card),
1829
          agentMode: 'persistent_runtime',
1830
          transports: [
1831
            {
1832
              protocol: 'A2A/1.0',
1833
              protocolBinding: 'JSONRPC',
1834
              endpoint: String(row.a2a_endpoint ?? row.endpoint),
1835
              managedBy: 'agent',
1836
            },
1837
          ],
1838
        });
1839
        return this.attestPublicAgentCard({
1840
          ...card,
1841
          presence: resolveAgentPresence(presence.get(card.agentId)),
1842
        });
1843
      })
1844
      .filter(
1845
        (card) =>
1846
          (!query ||
1847
            card.agentId.toLowerCase().includes(query) ||
1848
            card.slug?.includes(query) ||
1849
            card.name.toLowerCase().includes(query) ||
1850
            card.description.toLowerCase().includes(query) ||
1851
            card.framework.toLowerCase().includes(query) ||
1852
            card.capabilities.some((value) => value.toLowerCase().includes(query)) ||
1853
            card.limitations.some((value) => value.toLowerCase().includes(query))) &&
1854
          (!capability ||
1855
            card.capabilities.some((value) => value.toLowerCase().includes(capability))),
1856
      )
1857
      .slice(0, Math.min(Math.max(input.limit ?? 20, 1), 50));
1858
  }
1859
1860
  async searchPersonalizedMarketplace(
1861
    operatorId: string,
1862
    input: {
1863
      agentId?: string;
1864
      taskCategory?: string;
1865
      query?: string;
1866
      limit?: number;
1867
    } = {},
1868
  ) {
1869
    const [cards, records, summaries] = await Promise.all([
1870
      this.searchPublishedAgents({ query: input.query, limit: input.limit }),
1871
      this.list(operatorId),
1872
      this.listContextualIntelligence(operatorId, {
1873
        ...(input.taskCategory ? { taskCategory: input.taskCategory } : {}),
1874
      }),
1875
    ]);
1876
    const ownedAgents = records
1877
      .filter((record) => record.kind === 'agent_profile')
1878
      .map((record) => record.payload as Partial<AgentProfile>);
1879
    const ownedIds = new Set(ownedAgents.map((agent) => agent.agentId));
1880
    const selected = ownedAgents.find((agent) => agent.agentId === input.agentId);
1881
    const taskCategory = input.taskCategory ?? selected?.capabilities?.[0] ?? 'general';
1882
    const normalizedCategory = taskCategory.toLowerCase();
1883
    const summaryFor = (agentId: string) =>
1884
      summaries.find(
1885
        (summary) =>
1886
          summary.agentId === agentId &&
1887
          summary.taskCategory.toLowerCase() === normalizedCategory &&
1888
          summary.versionStatus.status === 'current',
1889
      ) ?? summaries.find((summary) => summary.agentId === agentId);
1890
    return cards
1891
      .filter(
1892
        (card) =>
1893
          card.agentMode === 'persistent_runtime' &&
1894
          card.transports.some((transport) => transport.managedBy === 'agent'),
1895
      )
1896
      .filter((card) => !ownedIds.has(card.agentId))
1897
      .map((card) => {
1898
        const intelligence = summaryFor(card.agentId);
1899
        const capabilityMatch = card.capabilities.some((capability) =>
1900
          capability.toLowerCase().includes(normalizedCategory),
1901
        );
1902
        const online = card.presence?.status === 'online';
1903
        const fitScore = Math.min(
1904
          1,
1905
          0.25 +
1906
            (capabilityMatch ? 0.35 : 0) +
1907
            (online ? 0.1 : 0) +
1908
            (intelligence ? intelligence.score * intelligence.confidence.value * 0.3 : 0),
1909
        );
1910
        return {
1911
          card,
1912
          taskCategory,
1913
          ...(intelligence ? { contextualReliability: intelligence } : {}),
1914
          match: {
1915
            score: fitScore,
1916
            label: fitScore >= 0.72 ? 'strong' : fitScore >= 0.48 ? 'possible' : 'unproven',
1917
            reasons: [
1918
              capabilityMatch
1919
                ? `Published capability matches ${taskCategory}`
1920
                : `No explicit ${taskCategory} capability`,
1921
              intelligence
1922
                ? `${intelligence.confidence.evidenceCount} verified outcomes in private history`
1923
                : 'No verified private history for this task',
1924
              online ? 'Currently online' : 'Currently offline',
1925
            ],
1926
          },
1927
        };
1928
      })
1929
      .sort((left, right) => right.match.score - left.match.score);
1930
  }
1931
1932
  async touchAgentPresence(operatorId: string, agentId: string): Promise<AgentPresence> {
1933
    await this.ensureSchema();
1934
    const owned = await this.sql`
1935
      SELECT 1 FROM openclasp_records
1936
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1937
    `;
1938
    if (!owned.length) throw new Error('Agent is not owned by this account');
1939
    const lastSeenAt = new Date().toISOString();
1940
    await this.upsert(operatorId, 'presence', agentId, { lastSeenAt });
1941
    await this.sql`
1942
      UPDATE openclasp_agent_runtimes SET last_seen_at = ${lastSeenAt}, updated_at = NOW()
1943
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId} AND status = 'verified'
1944
    `;
1945
    return resolveAgentPresence(lastSeenAt);
1946
  }
1947
1948
  async getAgentPresence(agentId: string): Promise<AgentPresence> {
1949
    await this.ensureSchema();
1950
    const rows = await this.sql`
1951
      SELECT records.payload
1952
      FROM openclasp_public_agents agents
1953
      LEFT JOIN openclasp_records records
1954
        ON records.operator_id = agents.operator_id
1955
        AND records.kind = 'presence'
1956
        AND records.record_id = agents.agent_id
1957
      WHERE agents.agent_id = ${agentId}
1958
      LIMIT 1
1959
    `;
1960
    if (!rows.length) throw new Error('Published agent not found');
1961
    const lastSeenAt = rows[0]?.payload?.lastSeenAt;
1962
    return resolveAgentPresence(typeof lastSeenAt === 'string' ? lastSeenAt : undefined);
1963
  }
1964
1965
  async listAgentRuntimes(operatorId: string) {
1966
    await this.ensureSchema();
1967
    const rows = await this.sql`
1968
      SELECT agent_id, endpoint, a2a_endpoint, status, verified_at, last_seen_at, last_error, updated_at
1969
      FROM openclasp_agent_runtimes
1970
      WHERE operator_id = ${operatorId}
1971
      ORDER BY updated_at DESC
1972
    `;
1973
    return rows.map((row) => ({
1974
      agentId: String(row.agent_id),
1975
      endpoint: String(row.endpoint),
1976
      a2aEndpoint: String(row.a2a_endpoint ?? row.endpoint),
1977
      status: row.status as 'verified' | 'disabled',
1978
      verifiedAt: new Date(String(row.verified_at)).toISOString(),
1979
      ...(row.last_seen_at ? { lastSeenAt: new Date(String(row.last_seen_at)).toISOString() } : {}),
1980
      ...(row.last_error ? { lastError: String(row.last_error) } : {}),
1981
      updatedAt: new Date(String(row.updated_at)).toISOString(),
1982
    }));
1983
  }
1984
1985
  async registerAgentRuntime(operatorId: string, agentId: string, endpoint: string) {
1986
    await this.ensureSchema();
1987
    const owned = await this.sql`
1988
      SELECT 1 FROM openclasp_records
1989
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1990
    `;
1991
    if (!owned.length) throw new Error('Agent is not owned by this account');
1992
    const challenge = crypto.randomUUID();
1993
    const verification = await postRuntimeJson(endpoint, {
1994
      type: 'openclasp.runtime.verify',
1995
      version: '1',
1996
      agentId,
1997
      challenge,
1998
    });
1999
    if (
2000
      verification.status < 200 ||
2001
      verification.status >= 300 ||
2002
      !verification.body ||
2003
      typeof verification.body !== 'object' ||
2004
      (verification.body as { type?: unknown }).type !== 'openclasp.runtime.verified' ||
2005
      (verification.body as { version?: unknown }).version !== '1' ||
2006
      (verification.body as { agentId?: unknown }).agentId !== agentId ||
2007
      (verification.body as { challenge?: unknown }).challenge !== challenge
2008
    )
2009
      throw new Error('Runtime endpoint did not return the verification challenge');
2010
    const a2aEndpointValue = (verification.body as { a2aEndpoint?: unknown }).a2aEndpoint;
2011
    const a2aEndpoint =
2012
      typeof a2aEndpointValue === 'string' && a2aEndpointValue ? a2aEndpointValue : endpoint;
2013
    await resolvePublicRuntimeEndpoint(a2aEndpoint);
2014
    const runtimeSecret = randomBytes(32).toString('base64url');
2015
    const encrypted = encryptGatewayPayload(this.gatewaySecret(), runtimeSecret);
2016
    const verifiedAt = new Date().toISOString();
2017
    const saved = await this.sql`
2018
      INSERT INTO openclasp_agent_runtimes(
2019
        agent_id, operator_id, endpoint, a2a_endpoint, secret_ciphertext, secret_iv, secret_auth_tag,
2020
        status, verified_at, last_seen_at, updated_at
2021
      ) VALUES (
2022
        ${agentId}, ${operatorId}, ${endpoint}, ${a2aEndpoint}, ${encrypted.ciphertext},
2023
        ${encrypted.iv}, ${encrypted.authTag}, 'verified', ${verifiedAt}, ${verifiedAt}, NOW()
2024
      )
2025
      ON CONFLICT (agent_id) DO UPDATE SET
2026
        operator_id = EXCLUDED.operator_id,
2027
        endpoint = EXCLUDED.endpoint,
2028
        a2a_endpoint = EXCLUDED.a2a_endpoint,
2029
        secret_ciphertext = EXCLUDED.secret_ciphertext,
2030
        secret_iv = EXCLUDED.secret_iv,
2031
        secret_auth_tag = EXCLUDED.secret_auth_tag,
2032
        status = 'verified',
2033
        verified_at = EXCLUDED.verified_at,
2034
        last_seen_at = EXCLUDED.last_seen_at,
2035
        last_error = NULL,
2036
        updated_at = NOW()
2037
      WHERE openclasp_agent_runtimes.operator_id = ${operatorId}
2038
      RETURNING agent_id
2039
    `;
2040
    if (!saved.length) throw new Error('Agent runtime is owned by another account');
2041
    await this.sql`
2042
      UPDATE openclasp_public_agents
2043
      SET card = jsonb_set(
2044
          jsonb_set(
2045
            card,
2046
            '{transports}',
2047
            ${JSON.stringify([
2048
              {
2049
                protocol: 'A2A/1.0',
2050
                protocolBinding: 'JSONRPC',
2051
                endpoint: a2aEndpoint,
2052
                managedBy: 'agent',
2053
              },
2054
            ])}::jsonb
2055
          ),
2056
          '{agentMode}', '"persistent_runtime"'::jsonb
2057
        ), updated_at = NOW()
2058
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
2059
    `;
2060
    await this.sql`
2061
      UPDATE openclasp_records
2062
      SET payload = jsonb_set(
2063
          jsonb_set(payload, '{a2aEndpoint}', to_jsonb(${a2aEndpoint}::text)),
2064
          '{agentMode}', '"persistent_runtime"'::jsonb
2065
        ),
2066
        updated_at = NOW()
2067
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
2068
    `;
2069
    await this.touchAgentPresence(operatorId, agentId);
2070
    await this.completeConnectorClaim(agentId);
2071
    return {
2072
      agentId,
2073
      endpoint,
2074
      a2aEndpoint,
2075
      status: 'verified' as const,
2076
      verifiedAt,
2077
      verificationKey: getSessionVerificationKey(this.gatewaySecret()),
2078
    };
2079
  }
2080
2081
  async disableAgentRuntime(operatorId: string, agentId: string) {
2082
    await this.ensureSchema();
2083
    const rows = await this.sql`
2084
      UPDATE openclasp_agent_runtimes
2085
      SET status = 'disabled', updated_at = NOW()
2086
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
2087
      RETURNING agent_id
2088
    `;
2089
    if (!rows.length) throw new Error('Agent runtime not found');
2090
    await this.sql`
2091
      DELETE FROM openclasp_public_agents
2092
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
2093
    `;
2094
    await this.sql`
2095
      UPDATE openclasp_records
2096
      SET payload = jsonb_set(
2097
        jsonb_set(payload, '{published}', 'false'::jsonb),
2098
        '{updatedAt}', to_jsonb(${new Date().toISOString()}::text)
2099
      ), updated_at = NOW()
2100
      WHERE operator_id = ${operatorId} AND kind = 'publication' AND record_id = ${agentId}
2101
    `;
2102
    return { agentId, status: 'disabled' as const, unpublished: true as const };
2103
  }
2104
2105
  async deleteAgent(operatorId: string, agentId: string) {
2106
    await this.ensureSchema();
2107
    const owned = await this.sql`
2108
      SELECT 1 FROM openclasp_records
2109
      WHERE operator_id = ${operatorId}
2110
        AND kind = 'agent_profile'
2111
        AND record_id = ${agentId}
2112
      LIMIT 1
2113
    `;
2114
    if (!owned.length) throw new Error('Owned agent not found');
2115
    const openInteractions = await this.sql`
2116
      SELECT interaction_id FROM openclasp_federated_interactions
2117
      WHERE (initiator_agent_id = ${agentId} OR responder_agent_id = ${agentId})
2118
        AND status IN ('pending', 'active')
2119
      LIMIT 1
2120
    `;
2121
    if (openInteractions.length) {
2122
      const error = new Error('Finish or reject the agent’s open interactions before deleting it');
2123
      Object.assign(error, { statusCode: 409 });
2124
      throw error;
2125
    }
2126
    await this.sql`
2127
      DELETE FROM openclasp_public_agents
2128
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
2129
    `;
2130
    await this.sql`
2131
      DELETE FROM openclasp_agent_runtimes
2132
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
2133
    `;
2134
    await this.sql`
2135
      DELETE FROM openclasp_agent_access_tokens
2136
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
2137
    `;
2138
    await this.sql`
2139
      DELETE FROM openclasp_provider_connections
2140
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
2141
    `;
2142
    await this.sql`
2143
      DELETE FROM openclasp_connector_claims
2144
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
2145
    `;
2146
    await this.sql`
2147
      DELETE FROM openclasp_records
2148
      WHERE operator_id = ${operatorId}
2149
        AND (
2150
          (record_id = ${agentId} AND kind IN ('agent', 'agent_profile', 'publication', 'presence'))
2151
          OR (kind = 'installation' AND payload->>'agentId' = ${agentId})
2152
          OR (kind = 'setup_request' AND payload->>'existingAgentId' = ${agentId})
2153
        )
2154
    `;
2155
    return {
2156
      agentId,
2157
      deleted: true as const,
2158
      historyRetained: true as const,
2159
    };
2160
  }
2161
2162
  private async sessionParticipant(agentId: string) {
2163
    const rows = await this.sql`
2164
      SELECT agents.operator_id, agents.card, profile.payload AS profile,
2165
        runtime.endpoint, runtime.a2a_endpoint
2166
      FROM openclasp_public_agents agents
2167
      LEFT JOIN openclasp_records profile
2168
        ON profile.operator_id = agents.operator_id
2169
        AND profile.kind = 'agent_profile'
2170
        AND profile.record_id = agents.agent_id
2171
      INNER JOIN openclasp_agent_runtimes runtime
2172
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
2173
      WHERE agents.agent_id = ${agentId}
2174
      LIMIT 1
2175
    `;
2176
    const row = rows[0];
2177
    if (!row) throw new Error(`Published agent ${agentId} was not found`);
2178
    const card = normalizePublicAgentCard(row.card);
2179
    if (!row.endpoint) throw new Error(`Agent ${agentId} does not have a verified live runtime`);
2180
    return {
2181
      agentId,
2182
      operatorId: String(row.operator_id),
2183
      mode: 'persistent_runtime' as const,
2184
      callbackEndpoint: String(row.endpoint),
2185
      a2aEndpoint: String(row.a2a_endpoint ?? row.endpoint),
2186
      card: PublicAgentCardSchema.parse({ ...card, agentMode: 'persistent_runtime' }),
2187
    };
2188
  }
2189
2190
  private async signedRuntimeRequest(
2191
    runtime: { callbackEndpoint: string },
2192
    requestId: string,
2193
    value: unknown,
2194
  ) {
2195
    const body = JSON.stringify(value);
2196
    const timestamp = new Date().toISOString();
2197
    return postRuntimeJson(runtime.callbackEndpoint, value, {
2198
      'openclasp-request-id': requestId,
2199
      'openclasp-timestamp': timestamp,
2200
      'openclasp-signature': `v1=${signSessionControl(this.gatewaySecret(), requestId, timestamp, body)}`,
2201
    });
2202
  }
2203
2204
  private sessionGrant(
2205
    interaction: FederatedInteraction,
2206
    senderAgentId: string,
2207
    recipientAgentId: string,
2208
  ) {
2209
    return issueSessionGrant(this.gatewaySecret(), {
2210
      interactionId: interaction.interactionId,
2211
      senderAgentId,
2212
      recipientAgentId,
2213
      expiresAt: Math.min(Date.parse(interaction.expiresAt), Date.now() + 60 * 60_000),
2214
    });
2215
  }
2216
2217
  private async privateCounterpartyInsights(
2218
    viewerOperatorId: string,
2219
    counterparty: { agentId: string; card: PublicAgentCard },
2220
    taskCategory: string,
2221
  ): Promise<{
2222
    insights: LiveSessionInsight[];
2223
    relevantSampleSize: number;
2224
    historyConfidence: number;
2225
  }> {
2226
    const rows = await this.sql`
2227
      SELECT record_id, payload
2228
      FROM openclasp_records
2229
      WHERE operator_id = ${viewerOperatorId}
2230
        AND kind = 'profile'
2231
        AND payload->>'agentId' = ${counterparty.agentId}
2232
        AND payload->>'taskCategory' = ${taskCategory}
2233
      ORDER BY (payload->>'sampleSize')::integer DESC, updated_at DESC
2234
    `;
2235
    const profiles: ContextualProfile[] = rows
2236
      .map<Record<string, unknown>>((row) => ({
2237
        recordId: String(row.record_id),
2238
        ...(row.payload as Record<string, unknown>),
2239
      }))
2240
      .filter(
2241
        (profile) =>
2242
          typeof profile.agentVersion === 'string' &&
2243
          typeof profile.sampleSize === 'number' &&
2244
          typeof profile.updatedAt === 'string' &&
2245
          typeof profile.completion === 'number' &&
2246
          typeof profile.acceptance === 'number' &&
2247
          typeof profile.specification === 'number' &&
2248
          typeof profile.deadline === 'number' &&
2249
          typeof profile.communication === 'number' &&
2250
          typeof profile.evidence === 'number' &&
2251
          typeof profile.scope === 'number' &&
2252
          typeof profile.disputes === 'number',
2253
      ) as ContextualProfile[];
2254
    const exact = profiles.find(
2255
      (profile) => profile.agentVersion === counterparty.card.agentVersion,
2256
    );
2257
    const current = exact ?? profiles[0];
2258
    const insights: LiveSessionInsight[] = [];
2259
    let relevantSampleSize = 0;
2260
    let historyConfidence = 0;
2261
    if (!current) {
2262
      insights.push({
2263
        code: 'limited_verified_history',
2264
        severity: 'caution',
2265
        message: `No eligible ${taskCategory} history is available for this agent version. Ask for task-relevant evidence.`,
2266
        evidenceReferences: [],
2267
        requirementReferences: [],
2268
      });
2269
    } else {
2270
      const ageDays = Math.max(
2271
        0,
2272
        (Date.now() - Date.parse(String(current.updatedAt))) / 86_400_000,
2273
      );
2274
      const freshness = Math.exp(-ageDays / 180);
2275
      const sampleSize = Number(current.sampleSize);
2276
      const effectiveSampleSize = Number(current.effectiveSampleSize ?? sampleSize);
2277
      const versionChanged = current.agentVersion !== counterparty.card.agentVersion;
2278
      const confidence = Math.min(
2279
        0.95,
2280
        (effectiveSampleSize / (effectiveSampleSize + 5)) * freshness * (versionChanged ? 0.35 : 1),
2281
      );
2282
      relevantSampleSize = sampleSize;
2283
      historyConfidence = confidence;
2284
      const reference = `openclasp:profile:${current.recordId}`;
2285
      insights.push({
2286
        code: versionChanged ? 'version_history_only' : 'contextual_history',
2287
        severity: versionChanged || confidence < 0.25 ? 'caution' : 'info',
2288
        message: versionChanged
2289
          ? `Only prior-version ${taskCategory} history is available (${sampleSize} eligible interaction${sampleSize === 1 ? '' : 's'}); confidence is reduced for version ${counterparty.card.agentVersion}.`
2290
          : `Based on ${sampleSize} eligible ${taskCategory} interaction${sampleSize === 1 ? '' : 's'}; confidence ${(confidence * 100).toFixed(0)}%.`,
2291
        evidenceReferences: [reference],
2292
        requirementReferences: [],
2293
      });
2294
      const weakDimensions = [
2295
        ['completion', Number(current.completion)],
2296
        ['output acceptance', Number(current.acceptance)],
2297
        ['specification adherence', Number(current.specification)],
2298
        ['deadline reliability', Number(current.deadline)],
2299
        ['communication quality', Number(current.communication)],
2300
        ['evidence quality', Number(current.evidence)],
2301
        ['scope adherence', Number(current.scope)],
2302
      ].filter(([, score]) => Number(score) < 0.7);
2303
      if (weakDimensions.length)
2304
        insights.push({
2305
          code: 'historical_weaknesses',
2306
          severity: weakDimensions.some(([, score]) => Number(score) < 0.5) ? 'high' : 'caution',
2307
          message: `Observed weaker areas: ${weakDimensions
2308
            .map(([name, score]) => `${String(name)} ${(Number(score) * 100).toFixed(0)}%`)
2309
            .join(', ')}.`,
2310
          evidenceReferences: [reference],
2311
          requirementReferences: [],
2312
        });
2313
      if (Number(current.disputes) > 0.25)
2314
        insights.push({
2315
          code: 'elevated_dispute_history',
2316
          severity: Number(current.disputes) >= 0.5 ? 'high' : 'caution',
2317
          message: `Eligible feedback shows a ${(Number(current.disputes) * 100).toFixed(0)}% dispute rate in this task category.`,
2318
          evidenceReferences: [reference],
2319
          requirementReferences: [],
2320
        });
2321
    }
2322
    if (counterparty.card.limitations.length)
2323
      insights.push({
2324
        code: 'declared_limitations',
2325
        severity: 'info',
2326
        message: `Counterparty declares: ${counterparty.card.limitations.join('; ')}`,
2327
        evidenceReferences: [counterparty.card.cardUrl],
2328
        requirementReferences: [],
2329
      });
2330
    return { insights, relevantSampleSize, historyConfidence };
2331
  }
2332
2333
  async brokerLiveSession(interaction: FederatedInteraction) {
2334
    await this.ensureSchema();
2335
    const brokerStartedAt = Date.now();
2336
    a2aInfo('session.broker_started', {
2337
      interactionId: interaction.interactionId,
2338
      initiatorAgentId: interaction.initiatorAgentId,
2339
      responderAgentId: interaction.responderAgentId,
2340
      contractRevision: interaction.contractRevision,
2341
    });
2342
    const existing = await this.sql`
2343
      SELECT status FROM openclasp_live_sessions
2344
      WHERE interaction_id = ${interaction.interactionId}
2345
    `;
2346
    if (existing[0]?.status === 'active') return;
2347
    const [initiator, responder] = await Promise.all([
2348
      this.sessionParticipant(interaction.initiatorAgentId),
2349
      this.sessionParticipant(interaction.responderAgentId),
2350
    ]);
2351
    const [initiatorContext, responderContext] = await Promise.all([
2352
      this.privateCounterpartyInsights(
2353
        initiator.operatorId,
2354
        responder,
2355
        interaction.contract.taskCategory,
2356
      ),
2357
      this.privateCounterpartyInsights(
2358
        responder.operatorId,
2359
        initiator,
2360
        interaction.contract.taskCategory,
2361
      ),
2362
    ]);
2363
    const issuedAt = new Date().toISOString();
2364
    const initiatorBrief = buildCounterpartyBrief({
2365
      interactionId: interaction.interactionId,
2366
      contractHash: interaction.termsHash,
2367
      contract: interaction.contract,
2368
      recipientAgentId: initiator.agentId,
2369
      subject: responder.card,
2370
      historyInsights: initiatorContext.insights,
2371
      relevantSampleSize: initiatorContext.relevantSampleSize,
2372
      historyConfidence: initiatorContext.historyConfidence,
2373
      generatedAt: issuedAt,
2374
      expiresAt: interaction.expiresAt,
2375
    });
2376
    const responderBrief = buildCounterpartyBrief({
2377
      interactionId: interaction.interactionId,
2378
      contractHash: interaction.termsHash,
2379
      contract: interaction.contract,
2380
      recipientAgentId: responder.agentId,
2381
      subject: initiator.card,
2382
      historyInsights: responderContext.insights,
2383
      relevantSampleSize: responderContext.relevantSampleSize,
2384
      historyConfidence: responderContext.historyConfidence,
2385
      generatedAt: issuedAt,
2386
      expiresAt: interaction.expiresAt,
2387
    });
2388
    await Promise.all([
2389
      this.upsert(
2390
        initiator.operatorId,
2391
        'counterparty_brief',
2392
        initiatorBrief.briefId,
2393
        initiatorBrief,
2394
      ),
2395
      this.upsert(
2396
        responder.operatorId,
2397
        'counterparty_brief',
2398
        responderBrief.briefId,
2399
        responderBrief,
2400
      ),
2401
    ]);
2402
    const makeOffer = (
2403
      runtime: typeof initiator,
2404
      counterparty: typeof responder,
2405
      role: 'initiator' | 'responder',
2406
      privateInsights: LiveSessionInsight[],
2407
      counterpartyBrief: CounterpartyBrief,
2408
    ) =>
2409
      LiveSessionOfferSchema.parse({
2410
        type: 'openclasp.session.offer',
2411
        version: '1',
2412
        offerId: crypto.randomUUID(),
2413
        interactionId: interaction.interactionId,
2414
        agentId: runtime.agentId,
2415
        role,
2416
        counterparty: {
2417
          agentId: counterparty.agentId,
2418
          name: counterparty.card.name,
2419
          agentVersion: counterparty.card.agentVersion,
2420
          capabilities: counterparty.card.capabilities,
2421
        },
2422
        contract: interaction.contract,
2423
        contractHash: interaction.termsHash,
2424
        privateInsights,
2425
        counterpartyBrief,
2426
        issuedAt,
2427
        expiresAt: interaction.expiresAt,
2428
      });
2429
    const initiatorOffer = makeOffer(
2430
      initiator,
2431
      responder,
2432
      'initiator',
2433
      initiatorBrief.insights,
2434
      initiatorBrief,
2435
    );
2436
    const responderOffer = makeOffer(
2437
      responder,
2438
      initiator,
2439
      'responder',
2440
      responderBrief.insights,
2441
      responderBrief,
2442
    );
2443
    const prepare = async (
2444
      participant: typeof initiator,
2445
      offer: typeof initiatorOffer,
2446
      label: 'Initiator' | 'Responder',
2447
    ) => {
2448
      if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
2449
      const response = await this.signedRuntimeRequest(
2450
        { callbackEndpoint: participant.callbackEndpoint },
2451
        offer.offerId,
2452
        offer,
2453
      );
2454
      a2aInfo('runtime.offer_completed', {
2455
        interactionId: interaction.interactionId,
2456
        role: label.toLowerCase(),
2457
        statusCode: response.status,
2458
      });
2459
      if (response.status < 200 || response.status >= 300)
2460
        throw new Error(`${label} runtime is not live (HTTP ${response.status})`);
2461
      return LiveSessionAcceptanceSchema.parse(response.body);
2462
    };
2463
    const [initiatorAcceptance, responderAcceptance] = await Promise.all([
2464
      prepare(initiator, initiatorOffer, 'Initiator'),
2465
      prepare(responder, responderOffer, 'Responder'),
2466
    ]);
2467
    if (
2468
      initiatorAcceptance.offerId !== initiatorOffer.offerId ||
2469
      initiatorAcceptance.agentId !== initiator.agentId ||
2470
      responderAcceptance.offerId !== responderOffer.offerId ||
2471
      responderAcceptance.agentId !== responder.agentId
2472
    )
2473
      throw new Error('Runtime returned a mismatched live-session acceptance');
2474
    await Promise.all([
2475
      ...(initiator.mode === 'persistent_runtime'
2476
        ? [resolvePublicRuntimeEndpoint(initiatorAcceptance.a2aEndpoint)]
2477
        : []),
2478
      ...(responder.mode === 'persistent_runtime'
2479
        ? [resolvePublicRuntimeEndpoint(responderAcceptance.a2aEndpoint)]
2480
        : []),
2481
    ]);
2482
    await this.sql`
2483
      INSERT INTO openclasp_live_sessions(
2484
        interaction_id, initiator_agent_id, responder_agent_id,
2485
        initiator_session_id, responder_session_id,
2486
        initiator_endpoint, responder_endpoint, status, expires_at
2487
      ) VALUES (
2488
        ${interaction.interactionId}, ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
2489
        ${initiatorAcceptance.sessionId}, ${responderAcceptance.sessionId},
2490
        ${initiatorAcceptance.a2aEndpoint}, ${responderAcceptance.a2aEndpoint},
2491
        'preparing', ${interaction.expiresAt}
2492
      )
2493
      ON CONFLICT (interaction_id) DO UPDATE SET
2494
        initiator_session_id = EXCLUDED.initiator_session_id,
2495
        responder_session_id = EXCLUDED.responder_session_id,
2496
        initiator_endpoint = EXCLUDED.initiator_endpoint,
2497
        responder_endpoint = EXCLUDED.responder_endpoint,
2498
        status = 'preparing', expires_at = EXCLUDED.expires_at,
2499
        activated_at = NULL, completed_at = NULL, last_error = NULL
2500
    `;
2501
    await this.journalLiveSessionState(interaction.interactionId);
2502
    const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
2503
      /\/$/,
2504
      '',
2505
    );
2506
    const activatedAt = new Date().toISOString();
2507
    const verificationKey = getSessionVerificationKey(this.gatewaySecret());
2508
    const makeActivation = (
2509
      agentId: string,
2510
      sessionId: string,
2511
      role: 'initiator' | 'responder',
2512
      peerAgentId: string,
2513
      peerSessionId: string,
2514
      peerEndpoint: string,
2515
      privateInsights: LiveSessionInsight[],
2516
      counterpartyBrief: CounterpartyBrief,
2517
    ): LiveSessionActivation => {
2518
      const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
2519
      return LiveSessionActivationSchema.parse({
2520
        type: 'openclasp.session.activation',
2521
        version: '1',
2522
        activationId: crypto.randomUUID(),
2523
        interactionId: interaction.interactionId,
2524
        agentId,
2525
        sessionId,
2526
        role,
2527
        peer: {
2528
          agentId: peerAgentId,
2529
          sessionId: peerSessionId,
2530
          endpoint: peerEndpoint,
2531
          bearerToken,
2532
          verificationKey,
2533
        },
2534
        reporting: {
2535
          endpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/events`,
2536
          completionEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/completion-reports`,
2537
          feedbackEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/feedback`,
2538
          assuranceResponseEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/assurance-responses`,
2539
          bearerToken,
2540
        },
2541
        privateInsights,
2542
        counterpartyBrief,
2543
        contractHash: interaction.termsHash,
2544
        activatedAt,
2545
        expiresAt: interaction.expiresAt,
2546
      });
2547
    };
2548
    const initiatorActivation = makeActivation(
2549
      initiator.agentId,
2550
      initiatorAcceptance.sessionId,
2551
      'initiator',
2552
      responder.agentId,
2553
      responderAcceptance.sessionId,
2554
      responderAcceptance.a2aEndpoint,
2555
      initiatorBrief.insights,
2556
      initiatorBrief,
2557
    );
2558
    const responderActivation = makeActivation(
2559
      responder.agentId,
2560
      responderAcceptance.sessionId,
2561
      'responder',
2562
      initiator.agentId,
2563
      initiatorAcceptance.sessionId,
2564
      initiatorAcceptance.a2aEndpoint,
2565
      responderBrief.insights,
2566
      responderBrief,
2567
    );
2568
    try {
2569
      const activate = async (
2570
        participant: typeof initiator,
2571
        activation: LiveSessionActivation,
2572
        label: 'Initiator' | 'Responder',
2573
      ) => {
2574
        if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
2575
        const response = await this.signedRuntimeRequest(
2576
          { callbackEndpoint: participant.callbackEndpoint },
2577
          activation.activationId,
2578
          activation,
2579
        );
2580
        a2aInfo('runtime.activation_completed', {
2581
          interactionId: interaction.interactionId,
2582
          role: label.toLowerCase(),
2583
          statusCode: response.status,
2584
        });
2585
        if (response.status < 200 || response.status >= 300)
2586
          throw new Error(`${label} activation failed with HTTP ${response.status}`);
2587
      };
2588
      await activate(responder, responderActivation, 'Responder');
2589
      await activate(initiator, initiatorActivation, 'Initiator');
2590
      await this.sql`
2591
        UPDATE openclasp_live_sessions
2592
        SET status = 'active', activated_at = NOW(), last_error = NULL
2593
        WHERE interaction_id = ${interaction.interactionId}
2594
      `;
2595
      await this.journalLiveSessionState(interaction.interactionId);
2596
      await this.sql`
2597
        UPDATE openclasp_agent_runtimes
2598
        SET last_seen_at = NOW(), last_error = NULL, updated_at = NOW()
2599
        WHERE agent_id IN (${initiator.agentId}, ${responder.agentId})
2600
      `;
2601
      await Promise.all([
2602
        this.touchAgentPresence(initiator.operatorId, initiator.agentId),
2603
        this.touchAgentPresence(responder.operatorId, responder.agentId),
2604
      ]);
2605
      a2aInfo('session.activated', {
2606
        interactionId: interaction.interactionId,
2607
        initiatorAgentId: initiator.agentId,
2608
        responderAgentId: responder.agentId,
2609
        durationMs: Date.now() - brokerStartedAt,
2610
      });
2611
    } catch (error) {
2612
      const reason =
2613
        error instanceof Error ? error.message.slice(0, 500) : 'Session activation failed';
2614
      await this.sql`
2615
        UPDATE openclasp_live_sessions SET status = 'failed', last_error = ${reason}
2616
        WHERE interaction_id = ${interaction.interactionId}
2617
      `;
2618
      await this.journalLiveSessionState(interaction.interactionId);
2619
      a2aError('session.activation_failed', error, {
2620
        interactionId: interaction.interactionId,
2621
        durationMs: Date.now() - brokerStartedAt,
2622
      });
2623
      throw error;
2624
    }
2625
  }
2626
2627
  async getLiveSession(operatorId: string, interactionId: string, agentId: string) {
2628
    await this.ensureSchema();
2629
    const rows = await this.sql`
2630
      SELECT session.*, interaction.payload,
2631
        initiator.operator_id AS initiator_operator_id,
2632
        responder.operator_id AS responder_operator_id
2633
      FROM openclasp_live_sessions session
2634
      INNER JOIN openclasp_federated_interactions interaction
2635
        ON interaction.interaction_id = session.interaction_id
2636
      INNER JOIN openclasp_public_agents initiator
2637
        ON initiator.agent_id = session.initiator_agent_id
2638
      INNER JOIN openclasp_public_agents responder
2639
        ON responder.agent_id = session.responder_agent_id
2640
      WHERE session.interaction_id = ${interactionId} AND session.status = 'active'
2641
      LIMIT 1
2642
    `;
2643
    const row = rows[0];
2644
    if (!row) throw new Error('Active live session not found');
2645
    const isInitiator = row.initiator_agent_id === agentId;
2646
    const isResponder = row.responder_agent_id === agentId;
2647
    if (!isInitiator && !isResponder) throw new Error('Agent is not a session participant');
2648
    const owner = isInitiator ? row.initiator_operator_id : row.responder_operator_id;
2649
    if (owner !== operatorId) throw new Error('Agent is not owned by this account');
2650
    const interaction = FederatedInteractionSchema.parse(row.payload);
2651
    const peerAgentId = String(isInitiator ? row.responder_agent_id : row.initiator_agent_id);
2652
    const peer = await this.sessionParticipant(peerAgentId);
2653
    const privateContext = await this.privateCounterpartyInsights(
2654
      String(owner),
2655
      peer,
2656
      interaction.contract.taskCategory,
2657
    );
2658
    const briefRows = await this.sql`
2659
      SELECT payload FROM openclasp_records
2660
      WHERE operator_id = ${String(owner)}
2661
        AND kind = 'counterparty_brief'
2662
        AND payload->>'interactionId' = ${interactionId}
2663
        AND payload->>'recipientAgentId' = ${agentId}
2664
      ORDER BY updated_at DESC
2665
      LIMIT 1
2666
    `;
2667
    const storedBrief = CounterpartyBriefSchema.safeParse(briefRows[0]?.payload);
2668
    const counterpartyBrief = storedBrief.success
2669
      ? storedBrief.data
2670
      : buildCounterpartyBrief({
2671
          interactionId,
2672
          contractHash: interaction.termsHash,
2673
          contract: interaction.contract,
2674
          recipientAgentId: agentId,
2675
          subject: peer.card,
2676
          historyInsights: privateContext.insights,
2677
          relevantSampleSize: privateContext.relevantSampleSize,
2678
          historyConfidence: privateContext.historyConfidence,
2679
          expiresAt: new Date(String(row.expires_at)).toISOString(),
2680
        });
2681
    const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
2682
    return {
2683
      type: 'openclasp.session.activation' as const,
2684
      version: '1' as const,
2685
      activationId: crypto.randomUUID(),
2686
      interactionId,
2687
      agentId,
2688
      sessionId: String(isInitiator ? row.initiator_session_id : row.responder_session_id),
2689
      role: isInitiator ? ('initiator' as const) : ('responder' as const),
2690
      peer: {
2691
        agentId: peerAgentId,
2692
        sessionId: String(isInitiator ? row.responder_session_id : row.initiator_session_id),
2693
        endpoint: String(isInitiator ? row.responder_endpoint : row.initiator_endpoint),
2694
        bearerToken,
2695
        verificationKey: getSessionVerificationKey(this.gatewaySecret()),
2696
      },
2697
      reporting: {
2698
        endpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/events`,
2699
        completionEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/completion-reports`,
2700
        feedbackEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/feedback`,
2701
        assuranceResponseEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/assurance-responses`,
2702
        bearerToken,
2703
      },
2704
      privateInsights: privateContext.insights,
2705
      counterpartyBrief,
2706
      contractHash: interaction.termsHash,
2707
      activatedAt: row.activated_at
2708
        ? new Date(String(row.activated_at)).toISOString()
2709
        : new Date().toISOString(),
2710
      expiresAt: new Date(String(row.expires_at)).toISOString(),
2711
    };
2712
  }
2713
2714
  private async interactionParticipant(operatorId: string, interactionId: string, agentId: string) {
2715
    await this.ensureSchema();
2716
    const rows = await this.sql`
2717
      SELECT payload, initiator_operator_id, responder_operator_id,
2718
        initiator_agent_id, responder_agent_id
2719
      FROM openclasp_federated_interactions
2720
      WHERE interaction_id = ${interactionId}
2721
        AND (
2722
          (initiator_operator_id = ${operatorId} AND initiator_agent_id = ${agentId})
2723
          OR (responder_operator_id = ${operatorId} AND responder_agent_id = ${agentId})
2724
        )
2725
      LIMIT 1
2726
    `;
2727
    const row = rows[0];
2728
    if (!row) throw new Error('Interaction participant not found for this account');
2729
    const interaction = FederatedInteractionSchema.parse(row.payload);
2730
    const isInitiator = interaction.initiatorAgentId === agentId;
2731
    return {
2732
      interaction,
2733
      counterpartyAgentId: isInitiator
2734
        ? interaction.responderAgentId
2735
        : interaction.initiatorAgentId,
2736
      participantOperatorId: String(
2737
        isInitiator ? row.initiator_operator_id : row.responder_operator_id,
2738
      ),
2739
      counterpartyOperatorId: String(
2740
        isInitiator ? row.responder_operator_id : row.initiator_operator_id,
2741
      ),
2742
    };
2743
  }
2744
2745
  async getCounterpartyBrief(operatorId: string, interactionId: string, agentId: string) {
2746
    await this.interactionParticipant(operatorId, interactionId, agentId);
2747
    const rows = await this.sql`
2748
      SELECT payload FROM openclasp_records
2749
      WHERE operator_id = ${operatorId}
2750
        AND kind = 'counterparty_brief'
2751
        AND payload->>'interactionId' = ${interactionId}
2752
        AND payload->>'recipientAgentId' = ${agentId}
2753
      ORDER BY updated_at DESC
2754
      LIMIT 1
2755
    `;
2756
    const parsed = CounterpartyBriefSchema.safeParse(rows[0]?.payload);
2757
    if (!parsed.success) throw new Error('Counterparty brief is not available');
2758
    return parsed.data;
2759
  }
2760
2761
  private async getAssuranceLearning(
2762
    operatorId: string,
2763
    targetAgentId: string,
2764
    targetAgentVersion: string,
2765
    taskCategory: string,
2766
  ) {
2767
    const rows = await this.sql`
2768
      SELECT payload FROM openclasp_assurance_evaluations
2769
      WHERE operator_id = ${operatorId}
2770
        AND target_agent_id = ${targetAgentId}
2771
        AND target_agent_version = ${targetAgentVersion}
2772
        AND task_category = ${taskCategory}
2773
      ORDER BY created_at DESC
2774
      LIMIT 500
2775
    `;
2776
    const evaluations = rows
2777
      .map((row) => AssuranceEffectivenessEvaluationSchema.safeParse(row.payload))
2778
      .filter((parsed) => parsed.success)
2779
      .map((parsed) => parsed.data);
2780
    return summarizeAssuranceLearning(evaluations);
2781
  }
2782
2783
  async getAssuranceProbeContext(
2784
    operatorId: string,
2785
    interactionId: string,
2786
    generatedForAgentId: string,
2787
    targetAgentId: string,
2788
  ) {
2789
    const participant = await this.interactionParticipant(
2790
      operatorId,
2791
      interactionId,
2792
      generatedForAgentId,
2793
    );
2794
    if (participant.counterpartyAgentId !== targetAgentId)
2795
      throw new Error('Assurance probe target must be the authenticated agent’s counterparty');
2796
    if (participant.interaction.status !== 'active')
2797
      throw new Error('Assurance probes require an active interaction');
2798
    const targetCard = await this.getPublishedAgent(targetAgentId);
2799
    if (!targetCard) throw new Error('Assurance probe target is no longer published');
2800
    const brief = await this.getCounterpartyBrief(
2801
      operatorId,
2802
      interactionId,
2803
      generatedForAgentId,
2804
    ).catch(() => undefined);
2805
    const reportRows = await this.sql`
2806
      SELECT payload FROM openclasp_records
2807
      WHERE operator_id = ${operatorId}
2808
        AND kind = 'completion_report'
2809
        AND payload->>'interactionId' = ${interactionId}
2810
      ORDER BY updated_at DESC
2811
    `;
2812
    const completionReports = reportRows
2813
      .map((row) => InteractionCompletionReportSchema.safeParse(row.payload))
2814
      .filter((parsed) => parsed.success)
2815
      .map((parsed) => parsed.data);
2816
    const eventRows = await this.sql`
2817
      SELECT event FROM openclasp_live_session_events
2818
      WHERE interaction_id = ${interactionId}
2819
      ORDER BY created_at ASC
2820
      LIMIT 100
2821
    `;
2822
    const sessionEvents = eventRows
2823
      .map((row) => LiveSessionEventSchema.safeParse(row.event))
2824
      .filter((parsed) => parsed.success)
2825
      .map((parsed) => parsed.data);
2826
    const planRows = await this.sql`
2827
      SELECT payload FROM openclasp_assurance_probe_plans
2828
      WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
2829
        AND generated_for_agent_id = ${generatedForAgentId} AND target_agent_id = ${targetAgentId}
2830
      ORDER BY created_at ASC
2831
    `;
2832
    const previousPlans = planRows.map((row) => AssuranceProbePlanSchema.parse(row.payload));
2833
    const responseRows = await this.sql`
2834
      SELECT responses.payload FROM openclasp_assurance_probe_responses responses
2835
      INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id
2836
      WHERE plans.operator_id = ${operatorId} AND responses.interaction_id = ${interactionId}
2837
        AND responses.agent_id = ${targetAgentId}
2838
      ORDER BY responses.created_at ASC
2839
    `;
2840
    const previousResponses = responseRows.map((row) =>
2841
      AssuranceProbeResponseSchema.parse(row.payload),
2842
    );
2843
    const predictionRows = await this.sql`
2844
      SELECT payload FROM openclasp_assurance_predictions
2845
      WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
2846
        AND target_agent_id = ${targetAgentId}
2847
      ORDER BY created_at ASC
2848
    `;
2849
    const previousPredictions = predictionRows.map((row) =>
2850
      AssurancePredictionSnapshotSchema.parse(row.payload),
2851
    );
2852
    const assuranceLearning = await this.getAssuranceLearning(
2853
      operatorId,
2854
      targetAgentId,
2855
      targetCard.agentVersion,
2856
      participant.interaction.contract.taskCategory,
2857
    );
2858
    return {
2859
      interaction: participant.interaction,
2860
      targetCard,
2861
      brief,
2862
      completionReports,
2863
      sessionEvents,
2864
      previousPlans,
2865
      previousResponses,
2866
      previousPredictions,
2867
      assuranceLearning,
2868
    };
2869
  }
2870
2871
  async beginAssuranceGeneration(record: {
2872
    generationId: string;
2873
    operatorId: string;
2874
    interactionId: string;
2875
    phase: 'pre_task' | 'post_task';
2876
    model: string;
2877
    promptVersion: string;
2878
    input: Record<string, unknown>;
2879
  }) {
2880
    await this.ensureSchema();
2881
    const participants = await this.sql`
2882
      SELECT 1 FROM openclasp_federated_interactions
2883
      WHERE interaction_id = ${record.interactionId}
2884
        AND ${record.operatorId} IN (initiator_operator_id, responder_operator_id)
2885
      LIMIT 1
2886
    `;
2887
    if (!participants.length) throw new Error('Assurance generation interaction is not accessible');
2888
    await this.sql`
2889
      INSERT INTO openclasp_ai_generations(
2890
        generation_id, operator_id, interaction_id, phase, model, prompt_version,
2891
        status, input, input_digest
2892
      ) VALUES (
2893
        ${record.generationId}, ${record.operatorId}, ${record.interactionId}, ${record.phase},
2894
        ${record.model}, ${record.promptVersion}, 'pending', ${JSON.stringify(record.input)}::jsonb,
2895
        ${canonicalHash(record.input)}
2896
      )
2897
    `;
2898
  }
2899
2900
  async finishAssuranceGeneration(
2901
    operatorId: string,
2902
    generationId: string,
2903
    value: {
2904
      status: 'complete' | 'fallback' | 'error';
2905
      output: AssuranceDecision;
2906
      tokenUsage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number };
2907
      errorCode?: string;
2908
    },
2909
  ) {
2910
    const rows = await this.sql`
2911
      UPDATE openclasp_ai_generations
2912
      SET status = ${value.status}, output = ${JSON.stringify(value.output)}::jsonb,
2913
        token_usage = ${JSON.stringify(value.tokenUsage ?? {})}::jsonb,
2914
        error_code = ${value.errorCode ?? null}, completed_at = NOW()
2915
      WHERE generation_id = ${generationId} AND operator_id = ${operatorId} AND status = 'pending'
2916
      RETURNING generation_id
2917
    `;
2918
    if (!rows.length) throw new Error('Pending assurance generation not found');
2919
  }
2920
2921
  async saveAssuranceProbePlan(operatorId: string, value: AssuranceProbePlan) {
2922
    const plan = AssuranceProbePlanSchema.parse(value);
2923
    const participant = await this.interactionParticipant(
2924
      operatorId,
2925
      plan.interactionId,
2926
      plan.generatedForAgentId,
2927
    );
2928
    if (participant.counterpartyAgentId !== plan.targetAgentId)
2929
      throw new Error('Assurance probe target does not match the interaction counterparty');
2930
    if (participant.interaction.termsHash !== plan.contractHash)
2931
      throw new Error('Assurance probe contract hash is stale');
2932
    const rows = await this.sql`
2933
      INSERT INTO openclasp_assurance_probe_plans(
2934
        plan_id, generation_id, operator_id, interaction_id, contract_hash, phase,
2935
        generated_for_agent_id, target_agent_id, payload, expires_at
2936
      ) VALUES (
2937
        ${plan.planId}, ${plan.generation.generationId}, ${operatorId}, ${plan.interactionId},
2938
        ${plan.contractHash}, ${plan.phase}, ${plan.generatedForAgentId}, ${plan.targetAgentId},
2939
        ${JSON.stringify(plan)}::jsonb, ${plan.expiresAt}
2940
      )
2941
      ON CONFLICT (plan_id) DO NOTHING
2942
      RETURNING payload
2943
    `;
2944
    if (!rows.length) throw new Error('Assurance probe plan already exists');
2945
    return AssuranceProbePlanSchema.parse(rows[0]!.payload);
2946
  }
2947
2948
  async saveAssuranceDecision(
2949
    operatorId: string,
2950
    decisionValue: AssuranceDecision,
2951
    planValue: AssuranceProbePlan,
2952
  ) {
2953
    const decision = AssuranceDecisionSchema.parse(decisionValue);
2954
    const plan = AssuranceProbePlanSchema.parse(planValue);
2955
    if (
2956
      decision.assessmentId !== plan.assessmentId ||
2957
      decision.prediction.predictionId !== plan.predictionBeforeId ||
2958
      decision.generation.generationId !== plan.generation.generationId
2959
    )
2960
      throw new Error('Assurance decision and probe plan are not linked');
2961
    await this.interactionParticipant(
2962
      operatorId,
2963
      decision.interactionId,
2964
      decision.generatedForAgentId,
2965
    );
2966
    await this.sql`
2967
      INSERT INTO openclasp_assurance_assessments(
2968
        assessment_id, generation_id, operator_id, interaction_id, phase, round,
2969
        target_agent_id, target_agent_version, payload
2970
      ) VALUES (
2971
        ${decision.assessmentId}, ${decision.generation.generationId}, ${operatorId},
2972
        ${decision.interactionId}, ${decision.phase}, ${decision.round}, ${decision.targetAgentId},
2973
        ${decision.targetAgentVersion}, ${JSON.stringify(decision)}::jsonb
2974
      )
2975
    `;
2976
    await this.sql`
2977
      INSERT INTO openclasp_assurance_predictions(
2978
        prediction_id, assessment_id, operator_id, interaction_id, target_agent_id,
2979
        target_agent_version, task_category, stage, success_probability, payload
2980
      ) VALUES (
2981
        ${decision.prediction.predictionId}, ${decision.assessmentId}, ${operatorId},
2982
        ${decision.interactionId}, ${decision.targetAgentId}, ${decision.targetAgentVersion},
2983
        ${decision.prediction.taskCategory}, ${decision.prediction.stage},
2984
        ${decision.prediction.successProbability}, ${JSON.stringify(decision.prediction)}::jsonb
2985
      )
2986
    `;
2987
    for (const safeguard of decision.safeguards) {
2988
      await this.sql`
2989
        INSERT INTO openclasp_assurance_safeguards(
2990
          safeguard_id, assessment_id, operator_id, interaction_id, target_agent_id, status, payload
2991
        ) VALUES (
2992
          ${safeguard.safeguardId}, ${decision.assessmentId}, ${operatorId},
2993
          ${decision.interactionId}, ${decision.targetAgentId}, ${safeguard.status},
2994
          ${JSON.stringify(safeguard)}::jsonb
2995
        )
2996
      `;
2997
    }
2998
    const savedPlan = await this.saveAssuranceProbePlan(operatorId, plan);
2999
    return { decision, plan: savedPlan };
3000
  }
3001
3002
  async listAssuranceProbePlans(operatorId: string, interactionId: string, agentId: string) {
3003
    await this.interactionParticipant(operatorId, interactionId, agentId);
3004
    const rows = await this.sql`
3005
      SELECT payload FROM openclasp_assurance_probe_plans
3006
      WHERE interaction_id = ${interactionId}
3007
        AND (generated_for_agent_id = ${agentId} OR target_agent_id = ${agentId})
3008
      ORDER BY created_at DESC
3009
    `;
3010
    return rows.map((row) => AssuranceProbePlanSchema.parse(row.payload));
3011
  }
3012
3013
  async getAssuranceBrief(operatorId: string, interactionId: string, agentId: string) {
3014
    await this.interactionParticipant(operatorId, interactionId, agentId);
3015
    const [assessments, predictions, safeguards, evaluations, plans, responses] = await Promise.all(
3016
      [
3017
        this
3018
          .sql`SELECT payload FROM openclasp_assurance_assessments WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
3019
        this
3020
          .sql`SELECT payload FROM openclasp_assurance_predictions WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
3021
        this
3022
          .sql`SELECT payload FROM openclasp_assurance_safeguards WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
3023
        this
3024
          .sql`SELECT payload FROM openclasp_assurance_evaluations WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
3025
        this
3026
          .sql`SELECT payload FROM openclasp_assurance_probe_plans WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
3027
        this
3028
          .sql`SELECT responses.payload FROM openclasp_assurance_probe_responses responses INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id WHERE plans.operator_id = ${operatorId} AND responses.interaction_id = ${interactionId} ORDER BY responses.created_at ASC`,
3029
      ],
3030
    );
3031
    const parsedAssessments = assessments.map((row) => AssuranceDecisionSchema.parse(row.payload));
3032
    const latestAssessment = parsedAssessments.at(-1);
3033
    const assuranceLearning = latestAssessment
3034
      ? await this.getAssuranceLearning(
3035
          operatorId,
3036
          latestAssessment.targetAgentId,
3037
          latestAssessment.targetAgentVersion,
3038
          latestAssessment.prediction.taskCategory,
3039
        )
3040
      : summarizeAssuranceLearning([]);
3041
    return {
3042
      assessments: parsedAssessments,
3043
      predictions: predictions.map((row) => AssurancePredictionSnapshotSchema.parse(row.payload)),
3044
      safeguards: safeguards.map((row) => AssuranceSafeguardSchema.parse(row.payload)),
3045
      evaluations: evaluations.map((row) =>
3046
        AssuranceEffectivenessEvaluationSchema.parse(row.payload),
3047
      ),
3048
      plans: plans.map((row) => AssuranceProbePlanSchema.parse(row.payload)),
3049
      responses: responses.map((row) => AssuranceProbeResponseSchema.parse(row.payload)),
3050
      assuranceLearning,
3051
      advisoryNotice: 'experimental_estimate_not_a_guarantee' as const,
3052
    };
3053
  }
3054
3055
  private async predictionAfterProbe(
3056
    operatorId: string,
3057
    plan: AssuranceProbePlan,
3058
    response: AssuranceProbeResponse,
3059
  ) {
3060
    const rows = await this.sql`
3061
      SELECT payload FROM openclasp_assurance_predictions
3062
      WHERE operator_id = ${operatorId} AND interaction_id = ${plan.interactionId}
3063
        AND target_agent_id = ${plan.targetAgentId}
3064
      ORDER BY created_at DESC LIMIT 1
3065
    `;
3066
    const prior = AssurancePredictionSnapshotSchema.safeParse(rows[0]?.payload);
3067
    if (!prior.success) return undefined;
3068
    const question = plan.questions[0];
3069
    const answer = response.answers.find((candidate) => candidate.probeId === question?.probeId);
3070
    if (!question || !answer) return undefined;
3071
    const normalized = String(answer.answer).trim().toLowerCase();
3072
    const signal = question.expectedSignals.find(
3073
      (candidate) => candidate.answer.trim().toLowerCase() === normalized,
3074
    );
3075
    const delta = signal?.probabilityDelta ?? 0;
3076
    const prediction = AssurancePredictionSnapshotSchema.parse({
3077
      ...prior.data,
3078
      predictionId: crypto.randomUUID(),
3079
      stage: 'after_probe',
3080
      successProbability: Math.max(0.05, Math.min(0.95, prior.data.successProbability + delta)),
3081
      confidence: Math.min(0.9, prior.data.confidence + 0.05 * answer.confidence),
3082
      priorPredictionId: prior.data.predictionId,
3083
      triggerResponseId: response.responseId,
3084
      createdAt: new Date().toISOString(),
3085
    });
3086
    await this.sql`
3087
      INSERT INTO openclasp_assurance_predictions(
3088
        prediction_id, assessment_id, operator_id, interaction_id, target_agent_id,
3089
        target_agent_version, task_category, stage, success_probability, payload
3090
      ) VALUES (
3091
        ${prediction.predictionId}, ${plan.assessmentId}, ${operatorId}, ${prediction.interactionId},
3092
        ${prediction.targetAgentId}, ${prediction.targetAgentVersion}, ${prediction.taskCategory},
3093
        ${prediction.stage}, ${prediction.successProbability}, ${JSON.stringify(prediction)}::jsonb
3094
      )
3095
    `;
3096
    return prediction;
3097
  }
3098
3099
  async decideAssuranceSafeguard(
3100
    operatorId: string,
3101
    interactionId: string,
3102
    agentId: string,
3103
    safeguardId: string,
3104
    status: 'accepted' | 'rejected' | 'modified',
3105
    decisionReason?: string,
3106
  ) {
3107
    await this.interactionParticipant(operatorId, interactionId, agentId);
3108
    const rows = await this.sql`
3109
      SELECT safeguard.payload, safeguard.assessment_id, assessment.payload AS assessment
3110
      FROM openclasp_assurance_safeguards safeguard
3111
      INNER JOIN openclasp_assurance_assessments assessment
3112
        ON assessment.assessment_id = safeguard.assessment_id
3113
      WHERE safeguard.operator_id = ${operatorId} AND safeguard.interaction_id = ${interactionId}
3114
        AND safeguard.safeguard_id = ${safeguardId}
3115
      LIMIT 1
3116
    `;
3117
    const row = rows[0];
3118
    if (!row) throw new Error('Assurance safeguard not found');
3119
    const assessment = AssuranceDecisionSchema.parse(row.assessment);
3120
    if (assessment.generatedForAgentId !== agentId)
3121
      throw new Error('Only the agent that requested the assessment may decide its safeguard');
3122
    const current = AssuranceSafeguardSchema.parse(row.payload);
3123
    if (current.status !== 'recommended')
3124
      throw new Error('Assurance safeguard was already decided');
3125
    const safeguard = AssuranceSafeguardSchema.parse({
3126
      ...current,
3127
      status,
3128
      ...(decisionReason ? { decisionReason } : {}),
3129
      decidedAt: new Date().toISOString(),
3130
    });
3131
    await this.sql`
3132
      UPDATE openclasp_assurance_safeguards
3133
      SET status = ${status}, payload = ${JSON.stringify(safeguard)}::jsonb, updated_at = NOW()
3134
      WHERE safeguard_id = ${safeguardId} AND operator_id = ${operatorId}
3135
    `;
3136
    let prediction: AssurancePredictionSnapshot | undefined;
3137
    if (status !== 'rejected') {
3138
      const priorRows = await this.sql`
3139
        SELECT payload FROM openclasp_assurance_predictions
3140
        WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
3141
          AND target_agent_id = ${safeguard.targetAgentId}
3142
        ORDER BY created_at DESC LIMIT 1
3143
      `;
3144
      const prior = AssurancePredictionSnapshotSchema.safeParse(priorRows[0]?.payload);
3145
      if (prior.success) {
3146
        prediction = AssurancePredictionSnapshotSchema.parse({
3147
          ...prior.data,
3148
          predictionId: crypto.randomUUID(),
3149
          stage: 'after_safeguard',
3150
          successProbability: Math.min(
3151
            0.95,
3152
            prior.data.successProbability + Math.min(0.15, safeguard.expectedImpact),
3153
          ),
3154
          confidence: Math.min(0.9, prior.data.confidence + 0.03),
3155
          priorPredictionId: prior.data.predictionId,
3156
          createdAt: new Date().toISOString(),
3157
        });
3158
        await this.sql`
3159
          INSERT INTO openclasp_assurance_predictions(
3160
            prediction_id, assessment_id, operator_id, interaction_id, target_agent_id,
3161
            target_agent_version, task_category, stage, success_probability, payload
3162
          ) VALUES (
3163
            ${prediction.predictionId}, ${String(row.assessment_id)}, ${operatorId},
3164
            ${prediction.interactionId}, ${prediction.targetAgentId}, ${prediction.targetAgentVersion},
3165
            ${prediction.taskCategory}, ${prediction.stage}, ${prediction.successProbability},
3166
            ${JSON.stringify(prediction)}::jsonb
3167
          )
3168
        `;
3169
      }
3170
    }
3171
    return { safeguard, prediction, contractRevisionRequired: status !== 'rejected' };
3172
  }
3173
3174
  private async recalculateAssuranceComparison(
3175
    operatorId: string,
3176
    interactionId: string,
3177
    targetAgentId: string,
3178
  ): Promise<AssuranceClaimOutcomeComparison | undefined> {
3179
    const responseRows = await this.sql`
3180
      SELECT responses.response_id, responses.phase, responses.payload
3181
      FROM openclasp_assurance_probe_responses responses
3182
      INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id
3183
      WHERE responses.interaction_id = ${interactionId}
3184
        AND responses.agent_id = ${targetAgentId}
3185
        AND plans.operator_id = ${operatorId}
3186
      ORDER BY responses.created_at DESC
3187
    `;
3188
    const preRow = responseRows.find((row) => row.phase === 'pre_task');
3189
    if (!preRow) return undefined;
3190
    const pre = AssuranceProbeResponseSchema.parse(preRow.payload);
3191
    const postRow = responseRows.find((row) => row.phase === 'post_task');
3192
    const post = postRow ? AssuranceProbeResponseSchema.parse(postRow.payload) : undefined;
3193
    const reportRows = await this.sql`
3194
      SELECT payload FROM openclasp_records
3195
      WHERE operator_id = ${operatorId}
3196
        AND kind = 'completion_report'
3197
        AND payload->>'interactionId' = ${interactionId}
3198
        AND payload->>'reportingAgentId' = ${targetAgentId}
3199
      ORDER BY updated_at DESC
3200
      LIMIT 1
3201
    `;
3202
    const report = InteractionCompletionReportSchema.safeParse(reportRows[0]?.payload);
3203
    if (!report.success) return undefined;
3204
    const comparison = AssuranceClaimOutcomeComparisonSchema.parse({
3205
      protocolVersion: '0.1',
3206
      comparisonId: deterministicUuid(`assurance-comparison:${operatorId}:${pre.responseId}`),
3207
      interactionId,
3208
      contractHash: pre.contractHash,
3209
      targetAgentId,
3210
      preTaskResponseId: pre.responseId,
3211
      ...(post ? { postTaskResponseId: post.responseId } : {}),
3212
      completionReportIds: [report.data.reportId],
3213
      comparisons: pre.answers.map((answer) => assuranceClaimComparison(answer, report.data)),
3214
      calculatedAt: new Date().toISOString(),
3215
    });
3216
    await this.sql`
3217
      INSERT INTO openclasp_assurance_claim_comparisons(
3218
        comparison_id, operator_id, interaction_id, target_agent_id,
3219
        pre_task_response_id, post_task_response_id, payload
3220
      ) VALUES (
3221
        ${comparison.comparisonId}, ${operatorId}, ${interactionId}, ${targetAgentId},
3222
        ${comparison.preTaskResponseId}, ${comparison.postTaskResponseId ?? null},
3223
        ${JSON.stringify(comparison)}::jsonb
3224
      )
3225
      ON CONFLICT (operator_id, interaction_id, target_agent_id, pre_task_response_id)
3226
      DO UPDATE SET post_task_response_id = EXCLUDED.post_task_response_id,
3227
        payload = EXCLUDED.payload, created_at = NOW()
3228
    `;
3229
    return comparison;
3230
  }
3231
3232
  async submitAssuranceProbeResponse(
3233
    operatorId: string,
3234
    agentId: string,
3235
    value: AssuranceProbeResponse,
3236
  ) {
3237
    const response = AssuranceProbeResponseSchema.parse(value);
3238
    const participant = await this.interactionParticipant(
3239
      operatorId,
3240
      response.interactionId,
3241
      agentId,
3242
    );
3243
    if (response.agentId !== agentId)
3244
      throw new Error('Assurance response identity does not match the authenticated agent');
3245
    if (participant.interaction.termsHash !== response.contractHash)
3246
      throw new Error('Assurance response contract hash is stale');
3247
    const planRows = await this.sql`
3248
      SELECT operator_id, target_agent_id, phase, payload, expires_at
3249
      FROM openclasp_assurance_probe_plans
3250
      WHERE plan_id = ${response.planId} AND interaction_id = ${response.interactionId}
3251
      LIMIT 1
3252
    `;
3253
    const row = planRows[0];
3254
    if (!row) throw new Error('Assurance probe plan not found');
3255
    const plan = AssuranceProbePlanSchema.parse(row.payload);
3256
    if (row.target_agent_id !== agentId || response.phase !== row.phase)
3257
      throw new Error('Assurance response does not match the targeted probe plan');
3258
    if (response.agentVersion !== plan.targetAgentVersion)
3259
      throw new Error('Assurance response agent version does not match the probe plan');
3260
    if (Date.parse(String(row.expires_at)) <= Date.now())
3261
      throw new Error('Assurance probe plan expired');
3262
    const answerIds = new Set(response.answers.map((answer) => answer.probeId));
3263
    for (const question of plan.questions) {
3264
      const answer = response.answers.find((candidate) => candidate.probeId === question.probeId);
3265
      if (question.required && !answer)
3266
        throw new Error(`Required assurance probe ${question.questionCode} is unanswered`);
3267
      if (!answer) continue;
3268
      if (
3269
        answer.questionCode !== question.questionCode ||
3270
        answer.responseType !== question.responseType ||
3271
        (question.responseType === 'enum' && !question.choices?.includes(String(answer.answer)))
3272
      )
3273
        throw new Error(`Assurance answer does not match probe ${question.questionCode}`);
3274
    }
3275
    if (
3276
      answerIds.size !== response.answers.length ||
3277
      response.answers.some(
3278
        (answer) => !plan.questions.some((question) => question.probeId === answer.probeId),
3279
      )
3280
    )
3281
      throw new Error('Assurance response contains duplicate or unknown probes');
3282
    const rows = await this.sql`
3283
      INSERT INTO openclasp_assurance_probe_responses(
3284
        response_id, plan_id, operator_id, interaction_id, contract_hash, phase, agent_id, payload
3285
      ) VALUES (
3286
        ${response.responseId}, ${response.planId}, ${operatorId}, ${response.interactionId},
3287
        ${response.contractHash}, ${response.phase}, ${agentId}, ${JSON.stringify(response)}::jsonb
3288
      )
3289
      ON CONFLICT (plan_id, agent_id) DO NOTHING
3290
      RETURNING payload
3291
    `;
3292
    if (!rows.length) throw new Error('This agent already answered the assurance probe plan');
3293
    const storedResponse = AssuranceProbeResponseSchema.parse(rows[0]!.payload);
3294
    const prediction = await this.predictionAfterProbe(
3295
      String(row.operator_id),
3296
      plan,
3297
      storedResponse,
3298
    );
3299
    const comparison = await this.recalculateAssuranceComparison(
3300
      String(row.operator_id),
3301
      response.interactionId,
3302
      agentId,
3303
    );
3304
    return { response: storedResponse, prediction, comparison };
3305
  }
3306
3307
  async recordSessionAssuranceResponse(token: string, value: AssuranceProbeResponse) {
3308
    const response = AssuranceProbeResponseSchema.parse(value);
3309
    const grant = this.verifySessionGrant(token);
3310
    if (grant.interactionId !== response.interactionId || grant.senderAgentId !== response.agentId)
3311
      throw new Error('Session credential does not match the assurance response');
3312
    const owners = await this.sql`
3313
      SELECT initiator_operator_id, responder_operator_id, initiator_agent_id
3314
      FROM openclasp_federated_interactions
3315
      WHERE interaction_id = ${response.interactionId}
3316
      LIMIT 1
3317
    `;
3318
    const row = owners[0];
3319
    if (!row) throw new Error('Interaction not found');
3320
    const operatorId = String(
3321
      row.initiator_agent_id === response.agentId
3322
        ? row.initiator_operator_id
3323
        : row.responder_operator_id,
3324
    );
3325
    return this.submitAssuranceProbeResponse(operatorId, response.agentId, response);
3326
  }
3327
3328
  async listAssuranceComparisons(operatorId: string, interactionId: string, agentId: string) {
3329
    await this.interactionParticipant(operatorId, interactionId, agentId);
3330
    const rows = await this.sql`
3331
      SELECT payload FROM openclasp_assurance_claim_comparisons
3332
      WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
3333
      ORDER BY created_at DESC
3334
    `;
3335
    return rows.map((row) => AssuranceClaimOutcomeComparisonSchema.parse(row.payload));
3336
  }
3337
3338
  private async evaluateAssuranceOutcome(
3339
    operatorId: string,
3340
    report: InteractionCompletionReport,
3341
  ): Promise<AssuranceEffectivenessEvaluation | undefined> {
3342
    const [predictionRows, planRows, responseRows, safeguardRows] = await Promise.all([
3343
      this
3344
        .sql`SELECT payload FROM openclasp_assurance_predictions WHERE operator_id = ${operatorId} AND interaction_id = ${report.interactionId} AND target_agent_id = ${report.reportingAgentId} ORDER BY created_at ASC`,
3345
      this
3346
        .sql`SELECT payload FROM openclasp_assurance_probe_plans WHERE operator_id = ${operatorId} AND interaction_id = ${report.interactionId} AND target_agent_id = ${report.reportingAgentId} ORDER BY created_at ASC`,
3347
      this
3348
        .sql`SELECT responses.payload FROM openclasp_assurance_probe_responses responses INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id WHERE plans.operator_id = ${operatorId} AND responses.interaction_id = ${report.interactionId} AND responses.agent_id = ${report.reportingAgentId} ORDER BY responses.created_at ASC`,
3349
      this
3350
        .sql`SELECT payload FROM openclasp_assurance_safeguards WHERE operator_id = ${operatorId} AND interaction_id = ${report.interactionId} AND target_agent_id = ${report.reportingAgentId} ORDER BY created_at ASC`,
3351
    ]);
3352
    if (!predictionRows.length && !planRows.length) return undefined;
3353
    const predictions = predictionRows.map((row) =>
3354
      AssurancePredictionSnapshotSchema.parse(row.payload),
3355
    );
3356
    const plans = planRows.map((row) => AssuranceProbePlanSchema.parse(row.payload));
3357
    const responses = responseRows.map((row) => AssuranceProbeResponseSchema.parse(row.payload));
3358
    const safeguards = safeguardRows.map((row) => AssuranceSafeguardSchema.parse(row.payload));
3359
    const outcomeValue = report.outcome === 'success' ? 1 : report.outcome === 'partial' ? 0.5 : 0;
3360
    const evaluation = AssuranceEffectivenessEvaluationSchema.parse({
3361
      protocolVersion: '0.1',
3362
      evaluationId: deterministicUuid(`assurance-evaluation:${operatorId}:${report.reportId}`),
3363
      interactionId: report.interactionId,
3364
      contractHash: report.contractHash,
3365
      targetAgentId: report.reportingAgentId,
3366
      targetAgentVersion: report.agentVersion,
3367
      taskCategory: predictions[0]?.taskCategory ?? 'general',
3368
      completionReportId: report.reportId,
3369
      outcomeValue,
3370
      predictionScores: predictions.map((prediction) => ({
3371
        predictionId: prediction.predictionId,
3372
        stage: prediction.stage,
3373
        probability: prediction.successProbability,
3374
        brierScore: (prediction.successProbability - outcomeValue) ** 2,
3375
      })),
3376
      questionScores: plans.flatMap((plan) =>
3377
        plan.questions.map((question) => {
3378
          const response = responses.find((candidate) => candidate.planId === plan.planId);
3379
          const answer = response?.answers.find(
3380
            (candidate) => candidate.probeId === question.probeId,
3381
          );
3382
          const signal = answer
3383
            ? question.expectedSignals.find(
3384
                (candidate) =>
3385
                  candidate.answer.toLowerCase() === String(answer.answer).toLowerCase(),
3386
              )
3387
            : undefined;
3388
          const before = predictions.find(
3389
            (prediction) => prediction.predictionId === plan.predictionBeforeId,
3390
          );
3391
          const after = response
3392
            ? predictions.find((prediction) => prediction.triggerResponseId === response.responseId)
3393
            : undefined;
3394
          return {
3395
            probeId: question.probeId,
3396
            questionCode: question.questionCode,
3397
            questionFamily: question.questionFamily,
3398
            answered: Boolean(answer),
3399
            exposedMaterialRisk:
3400
              signal?.effect === 'reduce_success' ||
3401
              Boolean(answer?.limitations.length) ||
3402
              report.blockers.length > 0,
3403
            predictionDelta:
3404
              before && after ? after.successProbability - before.successProbability : 0,
3405
          };
3406
        }),
3407
      ),
3408
      safeguardScores: safeguards.map((safeguard) => ({
3409
        safeguardId: safeguard.safeguardId,
3410
        type: safeguard.type,
3411
        status: safeguard.status,
3412
        outcomeAssociation:
3413
          safeguard.status === 'accepted' || safeguard.status === 'modified'
3414
            ? outcomeValue >= 0.75
3415
              ? 'positive'
3416
              : outcomeValue <= 0.25
3417
                ? 'negative'
3418
                : 'unclear'
3419
            : 'unclear',
3420
        causalClaim: false,
3421
      })),
3422
      evaluatedAt: new Date().toISOString(),
3423
    });
3424
    await this.sql`
3425
      INSERT INTO openclasp_assurance_evaluations(
3426
        evaluation_id, operator_id, interaction_id, target_agent_id, target_agent_version,
3427
        task_category, completion_report_id, payload
3428
      ) VALUES (
3429
        ${evaluation.evaluationId}, ${operatorId}, ${evaluation.interactionId},
3430
        ${evaluation.targetAgentId}, ${evaluation.targetAgentVersion}, ${evaluation.taskCategory},
3431
        ${evaluation.completionReportId}, ${JSON.stringify(evaluation)}::jsonb
3432
      )
3433
      ON CONFLICT (operator_id, completion_report_id)
3434
      DO UPDATE SET payload = EXCLUDED.payload, created_at = NOW()
3435
    `;
3436
    return evaluation;
3437
  }
3438
3439
  private async ensureFeedbackRequest(
3440
    operatorId: string,
3441
    interactionId: string,
3442
    reviewerAgentId: string,
3443
    subjectAgentId: string,
3444
  ) {
3445
    const existing = await this.sql`
3446
      SELECT payload FROM openclasp_records
3447
      WHERE operator_id = ${operatorId}
3448
        AND kind = 'feedback_request'
3449
        AND payload->>'interactionId' = ${interactionId}
3450
        AND payload->>'reviewerAgentId' = ${reviewerAgentId}
3451
      ORDER BY updated_at DESC
3452
      LIMIT 1
3453
    `;
3454
    const parsed = FeedbackRequestSchema.safeParse(existing[0]?.payload);
3455
    if (parsed.success) return parsed.data;
3456
    const requestedAt = new Date();
3457
    const base = FeedbackRequestSchema.parse({
3458
      requestId: deterministicUuid(`feedback-request:${interactionId}:${reviewerAgentId}`),
3459
      interactionId,
3460
      reviewerAgentId,
3461
      subjectAgentId,
3462
      status: 'pending',
3463
      requestedDimensions: FEEDBACK_DIMENSIONS,
3464
      requestedAt: requestedAt.toISOString(),
3465
      dueAt: new Date(requestedAt.getTime() + feedbackWindowMilliseconds()).toISOString(),
3466
    });
3467
    const request = FeedbackRequestSchema.parse({
3468
      ...base,
3469
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
3470
    });
3471
    await this.upsert(operatorId, 'feedback_request', request.requestId, request);
3472
    return request;
3473
  }
3474
3475
  private async requestRuntimeFinalization(
3476
    interactionId: string,
3477
    reportingAgentId: string,
3478
    counterpartyAgentId: string,
3479
    contractHash: string,
3480
  ) {
3481
    const rows = await this.sql`
3482
      SELECT endpoint FROM openclasp_agent_runtimes
3483
      WHERE agent_id = ${counterpartyAgentId} AND status = 'verified'
3484
      LIMIT 1
3485
    `;
3486
    if (!rows[0]?.endpoint) return false;
3487
    const requestId = crypto.randomUUID();
3488
    const value = {
3489
      type: 'openclasp.session.finalization_request',
3490
      version: '1',
3491
      requestId,
3492
      interactionId,
3493
      agentId: counterpartyAgentId,
3494
      peerAgentId: reportingAgentId,
3495
      contractHash,
3496
      requestedAt: new Date().toISOString(),
3497
    };
3498
    const response = await this.signedRuntimeRequest(
3499
      { callbackEndpoint: String(rows[0].endpoint) },
3500
      requestId,
3501
      value,
3502
    );
3503
    return response.status >= 200 && response.status < 300;
3504
  }
3505
3506
  async listFeedbackRequests(operatorId: string, agentId: string) {
3507
    const rows = await this.sql`
3508
      SELECT payload FROM openclasp_records
3509
      WHERE operator_id = ${operatorId}
3510
        AND kind = 'feedback_request'
3511
        AND payload->>'reviewerAgentId' = ${agentId}
3512
      ORDER BY updated_at DESC
3513
    `;
3514
    return rows.map((row) => FeedbackRequestSchema.parse(row.payload));
3515
  }
3516
3517
  async submitCompletionReport(
3518
    operatorId: string,
3519
    agentId: string,
3520
    value: InteractionCompletionReport,
3521
    submissionMethod:
3522
      'oauth_account' | 'oauth_installation' | 'agent_access_token' | 'runtime_session',
3523
  ) {
3524
    const participant = await this.interactionParticipant(operatorId, value.interactionId, agentId);
3525
    const report = InteractionCompletionReportSchema.parse(value);
3526
    if (report.platformAttestation)
3527
      throw new Error('Platform attestation is assigned by OpenClasp');
3528
    if (report.reportingAgentId !== agentId)
3529
      throw new Error('Completion report identity does not match the authenticated agent');
3530
    if (report.counterpartyAgentId !== participant.counterpartyAgentId)
3531
      throw new Error('Completion report counterparty does not match the interaction');
3532
    if (report.contractHash !== participant.interaction.termsHash)
3533
      throw new Error('Completion report contract hash does not match the interaction');
3534
    if (report.requestedOutcome !== participant.interaction.contract.requestedOutcome)
3535
      throw new Error('Completion report requested outcome does not match the contract');
3536
    if (
3537
      report.criteria.some(
3538
        (criterion) =>
3539
          !participant.interaction.contract.successCriteria.includes(criterion.criterion),
3540
      )
3541
    )
3542
      throw new Error('Completion report contains a criterion outside the contract');
3543
    if (!['active', 'completed'].includes(participant.interaction.status))
3544
      throw new Error('Only active or completed interactions accept completion reports');
3545
    const profiles = await this.sql`
3546
      SELECT payload FROM openclasp_records
3547
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
3548
      LIMIT 1
3549
    `;
3550
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
3551
    if (!profile || profile.agentVersion !== report.agentVersion)
3552
      throw new Error('Completion report agent version does not match the registered agent');
3553
    let verifiedSubmissionMethod: InteractionCompletionReport['submissionMethod'] =
3554
      submissionMethod;
3555
    if (report.signature) {
3556
      if (report.submissionMethod !== 'agent_signature')
3557
        throw new Error('Signed completion reports must declare agent_signature submission');
3558
      const identities = await this.sql`
3559
        SELECT payload FROM openclasp_records
3560
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
3561
        LIMIT 1
3562
      `;
3563
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
3564
      if (
3565
        !identity.success ||
3566
        !verifyObject(report as unknown as Record<string, unknown>, identity.data.publicKey)
3567
      )
3568
        throw new Error('Completion report agent signature is invalid or unverifiable');
3569
      verifiedSubmissionMethod = 'agent_signature';
3570
    }
3571
    const unsigned = InteractionCompletionReportSchema.parse({
3572
      ...report,
3573
      submissionMethod: verifiedSubmissionMethod,
3574
    });
3575
    const stored = InteractionCompletionReportSchema.parse({
3576
      ...unsigned,
3577
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unsigned),
3578
    });
3579
    const existing = await this.sql`
3580
      SELECT payload FROM openclasp_records
3581
      WHERE kind = 'completion_report' AND record_id = ${stored.reportId}
3582
        AND payload->>'interactionId' = ${stored.interactionId}
3583
      LIMIT 1
3584
    `;
3585
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
3586
      throw new Error('Conflicting completion report ID');
3587
    await Promise.all([
3588
      this.upsert(participant.participantOperatorId, 'completion_report', stored.reportId, stored),
3589
      this.upsert(participant.counterpartyOperatorId, 'completion_report', stored.reportId, stored),
3590
    ]);
3591
    a2aInfo('completion.report_recorded', {
3592
      interactionId: stored.interactionId,
3593
      reportId: stored.reportId,
3594
      reportingAgentId: stored.reportingAgentId,
3595
      outcome: stored.outcome,
3596
      submissionMethod: verifiedSubmissionMethod,
3597
      criteriaCount: stored.criteria.length,
3598
      evidenceReferenceCount: stored.evidenceReferences.length,
3599
      confidence: stored.confidence,
3600
    });
3601
    const assuranceComparisons = (
3602
      await Promise.all([
3603
        this.recalculateAssuranceComparison(
3604
          participant.participantOperatorId,
3605
          stored.interactionId,
3606
          stored.reportingAgentId,
3607
        ).catch(() => undefined),
3608
        this.recalculateAssuranceComparison(
3609
          participant.counterpartyOperatorId,
3610
          stored.interactionId,
3611
          stored.reportingAgentId,
3612
        ).catch(() => undefined),
3613
      ])
3614
    ).filter((comparison) => comparison !== undefined);
3615
    const assuranceEvaluations = (
3616
      await Promise.all([
3617
        this.evaluateAssuranceOutcome(participant.participantOperatorId, stored).catch(
3618
          () => undefined,
3619
        ),
3620
        this.evaluateAssuranceOutcome(participant.counterpartyOperatorId, stored).catch(
3621
          () => undefined,
3622
        ),
3623
      ])
3624
    ).filter((evaluation) => evaluation !== undefined);
3625
    const [feedbackRequest] = await Promise.all([
3626
      this.ensureFeedbackRequest(
3627
        participant.participantOperatorId,
3628
        stored.interactionId,
3629
        stored.reportingAgentId,
3630
        stored.counterpartyAgentId,
3631
      ),
3632
      this.ensureFeedbackRequest(
3633
        participant.counterpartyOperatorId,
3634
        stored.interactionId,
3635
        stored.counterpartyAgentId,
3636
        stored.reportingAgentId,
3637
      ),
3638
    ]);
3639
    const completed = await this.sql`
3640
      SELECT COUNT(DISTINCT payload->>'reportingAgentId') AS count
3641
      FROM openclasp_records
3642
      WHERE kind = 'completion_report'
3643
        AND payload->>'interactionId' = ${stored.interactionId}
3644
    `;
3645
    let peerReportStatus: InteractionConclusion['peerReportStatus'] = 'received';
3646
    if (Number(completed[0]?.count ?? 0) >= 2) {
3647
      const completedAt = new Date().toISOString();
3648
      await Promise.all([
3649
        this.sql`
3650
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
3651
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
3652
        `,
3653
        this.sql`
3654
          UPDATE openclasp_federated_interactions
3655
          SET status = 'completed',
3656
            payload = jsonb_set(
3657
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
3658
              '{updatedAt}', to_jsonb(${completedAt}::text)
3659
            ),
3660
            updated_at = NOW()
3661
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
3662
        `,
3663
      ]);
3664
      await Promise.all([
3665
        this.journalLiveSessionState(stored.interactionId),
3666
        this.journalFederatedInteraction(stored.interactionId),
3667
      ]);
3668
    } else {
3669
      const requested = await this.requestRuntimeFinalization(
3670
        stored.interactionId,
3671
        stored.reportingAgentId,
3672
        stored.counterpartyAgentId,
3673
        stored.contractHash,
3674
      ).catch(() => false);
3675
      peerReportStatus = requested ? 'awaiting' : 'unreachable';
3676
      a2aInfo('completion.peer_finalization_requested', {
3677
        interactionId: stored.interactionId,
3678
        reportingAgentId: stored.reportingAgentId,
3679
        counterpartyAgentId: stored.counterpartyAgentId,
3680
        accepted: requested,
3681
      });
3682
    }
3683
    const conclusion = await this.finalizeInteractionConclusion(stored.interactionId, {
3684
      peerReportStatus,
3685
    });
3686
    return {
3687
      report: stored,
3688
      feedbackRequest,
3689
      peerReportRequested: peerReportStatus === 'awaiting',
3690
      assuranceComparisons,
3691
      assuranceEvaluations,
3692
      ...(conclusion.released ? { conclusion: conclusion.conclusion } : {}),
3693
    };
3694
  }
3695
3696
  async recordSessionCompletionReport(token: string, value: InteractionCompletionReport) {
3697
    const report = InteractionCompletionReportSchema.parse(value);
3698
    const grant = this.verifySessionGrant(token);
3699
    if (
3700
      grant.interactionId !== report.interactionId ||
3701
      grant.senderAgentId !== report.reportingAgentId ||
3702
      grant.recipientAgentId !== report.counterpartyAgentId
3703
    )
3704
      throw new Error('Session credential does not match the completion report');
3705
    const owners = await this.sql`
3706
      SELECT initiator_operator_id, responder_operator_id,
3707
        initiator_agent_id, responder_agent_id
3708
      FROM openclasp_federated_interactions
3709
      WHERE interaction_id = ${report.interactionId}
3710
      LIMIT 1
3711
    `;
3712
    const row = owners[0];
3713
    if (!row) throw new Error('Interaction not found');
3714
    const operatorId = String(
3715
      row.initiator_agent_id === report.reportingAgentId
3716
        ? row.initiator_operator_id
3717
        : row.responder_operator_id,
3718
    );
3719
    return this.submitCompletionReport(
3720
      operatorId,
3721
      report.reportingAgentId,
3722
      report,
3723
      'runtime_session',
3724
    );
3725
  }
3726
3727
  private async applyInteractionLearning(input: {
3728
    interaction: FederatedInteraction;
3729
    initiatorOperatorId: string;
3730
    responderOperatorId: string;
3731
    reports: InteractionCompletionReport[];
3732
    feedback: InteractionFeedback[];
3733
    conclusion: InteractionConclusion;
3734
  }) {
3735
    const { interaction, reports, feedback, conclusion } = input;
3736
    const [initiatorSettings, responderSettings, cards, pairActivity] = await Promise.all([
3737
      this.getSettings(input.initiatorOperatorId),
3738
      this.getSettings(input.responderOperatorId),
3739
      this.sql`
3740
        SELECT agent_id, card FROM openclasp_public_agents
3741
        WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
3742
      `,
3743
      this.sql`
3744
        SELECT COUNT(*)::integer AS count
3745
        FROM openclasp_federated_interactions
3746
        WHERE created_at >= NOW() - INTERVAL '24 hours'
3747
          AND (
3748
            (initiator_agent_id = ${interaction.initiatorAgentId}
3749
              AND responder_agent_id = ${interaction.responderAgentId})
3750
            OR
3751
            (initiator_agent_id = ${interaction.responderAgentId}
3752
              AND responder_agent_id = ${interaction.initiatorAgentId})
3753
          )
3754
      `,
3755
    ]);
3756
    const contributionMode =
3757
      initiatorSettings.contributionEnabled && responderSettings.contributionEnabled
3758
        ? ('network_aggregate' as const)
3759
        : ('local_only' as const);
3760
    const eligibilityBase = evaluateLearningEligibility({
3761
      interactionId: interaction.interactionId,
3762
      reports,
3763
      feedback,
3764
      consensus: conclusion.consensus,
3765
      contributionMode,
3766
      reviewerCredibility: Object.fromEntries(
3767
        feedback.map((item) => [item.reviewerAgentId, reviewerCredibility(item)]),
3768
      ),
3769
      manipulationSignals: [
3770
        ...(input.initiatorOperatorId === input.responderOperatorId ? ['same_operator_pair'] : []),
3771
        ...(Number(pairActivity[0]?.count ?? 0) > 10 ? ['rapid_reciprocal_pair_activity'] : []),
3772
      ],
3773
      decisionId: deterministicUuid(`learning-eligibility:${interaction.interactionId}`),
3774
      decidedAt: conclusion.generatedAt,
3775
    });
3776
    const eligibility = LearningEligibilityDecisionSchema.parse({
3777
      ...eligibilityBase,
3778
      platformAttestation: attestSessionRecord(this.gatewaySecret(), eligibilityBase),
3779
    });
3780
    await Promise.all([
3781
      this.upsert(
3782
        input.initiatorOperatorId,
3783
        'learning_eligibility',
3784
        eligibility.decisionId,
3785
        eligibility,
3786
      ),
3787
      this.upsert(
3788
        input.responderOperatorId,
3789
        'learning_eligibility',
3790
        eligibility.decisionId,
3791
        eligibility,
3792
      ),
3793
    ]);
3794
    if (!eligibility.eligible || eligibility.sampleWeight <= 0)
3795
      return { eligibility, profileDeltas: [] as BehaviouralProfileDelta[] };
3796
3797
    const appliedAt = new Date().toISOString();
3798
    const versions = new Map<string, string>();
3799
    for (const report of reports) versions.set(report.reportingAgentId, report.agentVersion);
3800
    for (const row of cards) {
3801
      const card = PublicAgentCardSchema.safeParse(row.card);
3802
      if (card.success && !versions.has(String(row.agent_id)))
3803
        versions.set(String(row.agent_id), card.data.agentVersion);
3804
    }
3805
    const participants = [
3806
      {
3807
        viewerOperatorId: input.initiatorOperatorId,
3808
        reviewerAgentId: interaction.initiatorAgentId,
3809
        subjectAgentId: interaction.responderAgentId,
3810
      },
3811
      {
3812
        viewerOperatorId: input.responderOperatorId,
3813
        reviewerAgentId: interaction.responderAgentId,
3814
        subjectAgentId: interaction.initiatorAgentId,
3815
      },
3816
      {
3817
        viewerOperatorId: input.initiatorOperatorId,
3818
        reviewerAgentId: interaction.responderAgentId,
3819
        subjectAgentId: interaction.initiatorAgentId,
3820
      },
3821
      {
3822
        viewerOperatorId: input.responderOperatorId,
3823
        reviewerAgentId: interaction.initiatorAgentId,
3824
        subjectAgentId: interaction.responderAgentId,
3825
      },
3826
    ];
3827
    const profileDeltas: Array<BehaviouralProfileDelta | undefined> = [];
3828
    for (const participant of participants) {
3829
      const agentVersion = versions.get(participant.subjectAgentId);
3830
      if (!agentVersion) {
3831
        profileDeltas.push(undefined);
3832
        continue;
3833
      }
3834
      const deltaId = deterministicUuid(
3835
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.reviewerAgentId}:${participant.subjectAgentId}`,
3836
      );
3837
      const legacyDeltaId = deterministicUuid(
3838
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.subjectAgentId}`,
3839
      );
3840
      const existingDelta = await this.sql`
3841
          SELECT payload FROM openclasp_records
3842
          WHERE operator_id = ${participant.viewerOperatorId}
3843
            AND kind = 'profile_delta'
3844
            AND (record_id = ${deltaId} OR record_id = ${legacyDeltaId})
3845
          LIMIT 1
3846
        `;
3847
      const parsedDelta = BehaviouralProfileDeltaSchema.safeParse(existingDelta[0]?.payload);
3848
      if (parsedDelta.success) {
3849
        profileDeltas.push(parsedDelta.data);
3850
        continue;
3851
      }
3852
      const profileId = deterministicUuid(
3853
        `contextual-profile:${participant.subjectAgentId}:${agentVersion}:${interaction.contract.taskCategory}`,
3854
      );
3855
      const currentRows = await this.sql`
3856
          SELECT payload FROM openclasp_records
3857
          WHERE operator_id = ${participant.viewerOperatorId}
3858
            AND kind = 'profile' AND record_id = ${profileId}
3859
          LIMIT 1
3860
        `;
3861
      const current = currentRows[0]?.payload as Partial<ContextualProfile> | undefined;
3862
      const reviewerFeedback = feedback.find(
3863
        (item) =>
3864
          item.reviewerAgentId === participant.reviewerAgentId &&
3865
          item.subjectAgentId === participant.subjectAgentId,
3866
      );
3867
      const observations = deriveBehaviouralObservations({
3868
        subjectAgentId: participant.subjectAgentId,
3869
        reviewerAgentId: participant.reviewerAgentId,
3870
        reports,
3871
        ...(reviewerFeedback ? { reviewerFeedback } : {}),
3872
        conclusion,
3873
      });
3874
      if (!Object.keys(observations).length) {
3875
        profileDeltas.push(undefined);
3876
        continue;
3877
      }
3878
      const applied = updateContextualBehaviouralProfile({
3879
        ...(current ? { current } : {}),
3880
        observations,
3881
        sampleWeight: eligibility.sampleWeight,
3882
        appliedAt,
3883
      });
3884
      const profile: ContextualProfile = {
3885
        recordId: profileId,
3886
        agentId: participant.subjectAgentId,
3887
        agentVersion,
3888
        taskCategory: interaction.contract.taskCategory,
3889
        ...applied.profile,
3890
      };
3891
      const deltaBase = BehaviouralProfileDeltaSchema.parse({
3892
        deltaId,
3893
        interactionId: interaction.interactionId,
3894
        agentId: participant.subjectAgentId,
3895
        agentVersion,
3896
        taskCategory: interaction.contract.taskCategory,
3897
        sampleWeight: eligibility.sampleWeight,
3898
        dimensionDeltas: applied.dimensionDeltas,
3899
        explanation:
3900
          'Applied platform-attested structured outcome and eligible reviewer signals; private comments and raw conversation content were excluded.',
3901
        appliedAt,
3902
      });
3903
      const delta = BehaviouralProfileDeltaSchema.parse({
3904
        ...deltaBase,
3905
        platformAttestation: attestSessionRecord(this.gatewaySecret(), deltaBase),
3906
      });
3907
      await Promise.all([
3908
        this.upsert(participant.viewerOperatorId, 'profile', profileId, profile),
3909
        this.upsert(participant.viewerOperatorId, 'profile_delta', deltaId, delta),
3910
      ]);
3911
      profileDeltas.push(delta);
3912
    }
3913
    return {
3914
      eligibility,
3915
      profileDeltas: profileDeltas.filter(
3916
        (delta): delta is BehaviouralProfileDelta => delta !== undefined,
3917
      ),
3918
    };
3919
  }
3920
3921
  private async finalizeInteractionConclusion(
3922
    interactionId: string,
3923
    options: { peerReportStatus?: InteractionConclusion['peerReportStatus'] } = {},
3924
  ) {
3925
    const [interactionRows, reportRows, feedbackRows, requestRows, existingRows] =
3926
      await Promise.all([
3927
        this.sql`
3928
        SELECT payload, initiator_operator_id, responder_operator_id
3929
        FROM openclasp_federated_interactions
3930
        WHERE interaction_id = ${interactionId}
3931
        LIMIT 1
3932
      `,
3933
        this.sql`
3934
        SELECT record_id, payload FROM openclasp_records
3935
        WHERE kind = 'completion_report' AND payload->>'interactionId' = ${interactionId}
3936
      `,
3937
        this.sql`
3938
        SELECT record_id, payload FROM openclasp_records
3939
        WHERE kind = 'interaction_feedback' AND payload->>'interactionId' = ${interactionId}
3940
      `,
3941
        this.sql`
3942
        SELECT record_id, payload FROM openclasp_records
3943
        WHERE kind = 'feedback_request' AND payload->>'interactionId' = ${interactionId}
3944
      `,
3945
        this.sql`
3946
        SELECT payload FROM openclasp_records
3947
        WHERE kind = 'interaction_conclusion' AND payload->>'interactionId' = ${interactionId}
3948
        LIMIT 1
3949
      `,
3950
      ]);
3951
    const interactionRow = interactionRows[0];
3952
    if (!interactionRow) throw new Error('Interaction not found');
3953
    const interaction = FederatedInteractionSchema.parse(interactionRow.payload);
3954
    const reports = [
3955
      ...new Map(
3956
        reportRows
3957
          .map((row) => InteractionCompletionReportSchema.parse(row.payload))
3958
          .map((report) => [report.reportId, report]),
3959
      ).values(),
3960
    ];
3961
    const feedback = [
3962
      ...new Map(
3963
        feedbackRows
3964
          .map((row) => InteractionFeedbackSchema.parse(row.payload))
3965
          .map((item) => [item.feedbackId, item]),
3966
      ).values(),
3967
    ];
3968
    const requests = [
3969
      ...new Map(
3970
        requestRows
3971
          .map((row) => FeedbackRequestSchema.parse(row.payload))
3972
          .map((request) => [request.requestId, request]),
3973
      ).values(),
3974
    ];
3975
    if (!reports.length) return { released: false as const, feedbackRevealed: false as const };
3976
    const existing = InteractionConclusionSchema.safeParse(existingRows[0]?.payload);
3977
    const reportingAgentIds = new Set(reports.map((report) => report.reportingAgentId));
3978
    const missingReportAgentIds = [
3979
      interaction.initiatorAgentId,
3980
      interaction.responderAgentId,
3981
    ].filter((agentId) => !reportingAgentIds.has(agentId));
3982
    const feedbackWindowClosed =
3983
      requests.length >= 2 && requests.every((request) => request.status !== 'pending');
3984
    const lifecycle =
3985
      missingReportAgentIds.length === 0 || feedbackWindowClosed
3986
        ? ('final' as const)
3987
        : ('provisional' as const);
3988
    const visibleFeedback = feedbackWindowClosed ? feedback : [];
3989
    const peerReportStatus: InteractionConclusion['peerReportStatus'] =
3990
      missingReportAgentIds.length === 0
3991
        ? 'received'
3992
        : feedbackWindowClosed
3993
          ? 'timed_out'
3994
          : (options.peerReportStatus ?? existing.data?.peerReportStatus ?? 'awaiting');
3995
    const pendingFeedbackAgentIds = requests
3996
      .filter((request) => request.status === 'pending')
3997
      .map((request) => request.reviewerAgentId);
3998
    if (requests.length < 2) return { released: false as const };
3999
    const base = buildInteractionConclusion({
4000
      interaction,
4001
      reports,
4002
      feedback: visibleFeedback,
4003
      conclusionId: deterministicUuid(`interaction-conclusion:${interactionId}`),
4004
      lifecycle,
4005
      pendingFeedbackAgentIds,
4006
      peerReportStatus,
4007
    });
4008
    const conclusion = InteractionConclusionSchema.parse({
4009
      ...base,
4010
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
4011
    });
4012
    const startedAt = reports
4013
      .map((report) => report.startedAt)
4014
      .filter((value): value is string => typeof value === 'string')
4015
      .sort()[0];
4016
    const receiptBase = {
4017
      receiptId: deterministicUuid(`interaction-receipt:${interactionId}`),
4018
      interactionId,
4019
      participants: [interaction.initiatorAgentId, interaction.responderAgentId],
4020
      agentVersions: Object.fromEntries(
4021
        reports.map((report) => [report.reportingAgentId, report.agentVersion]),
4022
      ),
4023
      contractHash: interaction.termsHash,
4024
      startedAt: startedAt ?? interaction.createdAt,
4025
      completedAt:
4026
        reports
4027
          .map((report) => report.completedAt)
4028
          .sort()
4029
          .at(-1) ?? conclusion.generatedAt,
4030
      outcome: conclusion.outcome,
4031
      commitmentsFulfilled: conclusion.criteria
4032
        .filter((criterion) => criterion.status === 'met')
4033
        .map((criterion) => criterion.criterion),
4034
      commitmentsMissed: conclusion.criteria
4035
        .filter((criterion) => criterion.status !== 'met')
4036
        .map((criterion) => criterion.criterion),
4037
      evidenceHashes: conclusion.evidenceReferences.map((reference) => canonicalHash(reference)),
4038
      policyWarnings: reports.flatMap((report) => report.blockers),
4039
      policyViolations: [],
4040
      disputeStatus: conclusion.consensus === 'conflicting' ? ('open' as const) : ('none' as const),
4041
      delegationChainHash: canonicalHash(interaction.contract.delegationRules),
4042
      unilateral: reports.length < 2,
4043
      provisional: lifecycle === 'provisional',
4044
      confidence: conclusion.confidence,
4045
      signatures: {},
4046
      completionReportIds: conclusion.reportIds,
4047
      conclusionId: conclusion.conclusionId,
4048
    };
4049
    const receipt = ReceiptSchema.parse({
4050
      ...receiptBase,
4051
      platformAttestation: attestSessionRecord(this.gatewaySecret(), receiptBase),
4052
    });
4053
    await Promise.all([
4054
      this.upsert(
4055
        String(interactionRow.initiator_operator_id),
4056
        'interaction_conclusion',
4057
        conclusion.conclusionId,
4058
        conclusion,
4059
      ),
4060
      this.upsert(
4061
        String(interactionRow.responder_operator_id),
4062
        'interaction_conclusion',
4063
        conclusion.conclusionId,
4064
        conclusion,
4065
      ),
4066
      this.upsert(
4067
        String(interactionRow.initiator_operator_id),
4068
        'receipt',
4069
        receipt.receiptId,
4070
        receipt,
4071
      ),
4072
      this.upsert(
4073
        String(interactionRow.responder_operator_id),
4074
        'receipt',
4075
        receipt.receiptId,
4076
        receipt,
4077
      ),
4078
    ]);
4079
    const learning =
4080
      lifecycle === 'final' && feedbackWindowClosed
4081
        ? await this.applyInteractionLearning({
4082
            interaction,
4083
            initiatorOperatorId: String(interactionRow.initiator_operator_id),
4084
            responderOperatorId: String(interactionRow.responder_operator_id),
4085
            reports,
4086
            feedback: visibleFeedback,
4087
            conclusion,
4088
          })
4089
        : undefined;
4090
    a2aInfo('conclusion.released', {
4091
      interactionId,
4092
      conclusionId: conclusion.conclusionId,
4093
      lifecycle,
4094
      outcome: conclusion.outcome,
4095
      consensus: conclusion.consensus,
4096
      reportCount: reports.length,
4097
      feedbackCount: visibleFeedback.length,
4098
      feedbackRevealed: feedbackWindowClosed,
4099
      learningEligible: learning?.eligibility.eligible,
4100
      learningSampleWeight: learning?.eligibility.sampleWeight,
4101
    });
4102
    return {
4103
      released: true as const,
4104
      feedbackRevealed: feedbackWindowClosed,
4105
      conclusion,
4106
      receipt,
4107
      ...(learning ? { learning } : {}),
4108
    };
4109
  }
4110
4111
  async submitInteractionFeedback(
4112
    operatorId: string,
4113
    agentId: string,
4114
    value: InteractionFeedback,
4115
    submissionMethod:
4116
      'oauth_account' | 'oauth_installation' | 'agent_access_token' | 'runtime_session',
4117
  ) {
4118
    const feedback = InteractionFeedbackSchema.parse(value);
4119
    if (feedback.platformAttestation)
4120
      throw new Error('Platform attestation is assigned by OpenClasp');
4121
    if (feedback.reviewerAgentId !== agentId)
4122
      throw new Error('Feedback reviewer does not match the authenticated agent');
4123
    const participant = await this.interactionParticipant(
4124
      operatorId,
4125
      feedback.interactionId,
4126
      agentId,
4127
    );
4128
    if (feedback.subjectAgentId !== participant.counterpartyAgentId)
4129
      throw new Error('Feedback subject does not match the interaction counterparty');
4130
    const requestRows = await this.sql`
4131
      SELECT payload FROM openclasp_records
4132
      WHERE operator_id = ${operatorId}
4133
        AND kind = 'feedback_request'
4134
        AND record_id = ${feedback.requestId}
4135
      LIMIT 1
4136
    `;
4137
    const request = FeedbackRequestSchema.parse(requestRows[0]?.payload);
4138
    if (
4139
      request.interactionId !== feedback.interactionId ||
4140
      request.reviewerAgentId !== feedback.reviewerAgentId ||
4141
      request.subjectAgentId !== feedback.subjectAgentId
4142
    )
4143
      throw new Error('Feedback does not match its request');
4144
    if (request.status !== 'pending') throw new Error('Feedback request is no longer pending');
4145
    if (Date.parse(request.dueAt) <= Date.now()) throw new Error('Feedback request has expired');
4146
    if (
4147
      request.requestedDimensions.some(
4148
        (dimension) => typeof feedback.ratings[dimension] !== 'number',
4149
      )
4150
    )
4151
      throw new Error('Feedback must rate every requested dimension');
4152
    const profiles = await this.sql`
4153
      SELECT payload FROM openclasp_records
4154
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
4155
      LIMIT 1
4156
    `;
4157
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
4158
    if (!profile || profile.agentVersion !== feedback.reviewerAgentVersion)
4159
      throw new Error('Feedback reviewer version does not match the registered agent');
4160
    let verifiedSubmissionMethod: InteractionFeedback['submissionMethod'] = submissionMethod;
4161
    if (feedback.signature) {
4162
      if (feedback.submissionMethod !== 'agent_signature')
4163
        throw new Error('Signed feedback must declare agent_signature submission');
4164
      const identities = await this.sql`
4165
        SELECT payload FROM openclasp_records
4166
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
4167
        LIMIT 1
4168
      `;
4169
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
4170
      if (
4171
        !identity.success ||
4172
        !verifyObject(feedback as unknown as Record<string, unknown>, identity.data.publicKey)
4173
      )
4174
        throw new Error('Feedback agent signature is invalid or unverifiable');
4175
      verifiedSubmissionMethod = 'agent_signature';
4176
    }
4177
    const unattested = InteractionFeedbackSchema.parse({
4178
      ...feedback,
4179
      submissionMethod: verifiedSubmissionMethod,
4180
    });
4181
    const stored = InteractionFeedbackSchema.parse({
4182
      ...unattested,
4183
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unattested),
4184
    });
4185
    const existing = await this.sql`
4186
      SELECT payload FROM openclasp_records
4187
      WHERE operator_id = ${operatorId}
4188
        AND kind = 'interaction_feedback'
4189
        AND record_id = ${stored.feedbackId}
4190
      LIMIT 1
4191
    `;
4192
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
4193
      throw new Error('Conflicting feedback ID');
4194
    await this.upsert(operatorId, 'interaction_feedback', stored.feedbackId, stored);
4195
    a2aInfo('feedback.recorded', {
4196
      interactionId: stored.interactionId,
4197
      feedbackId: stored.feedbackId,
4198
      reviewerAgentId: stored.reviewerAgentId,
4199
      subjectAgentId: stored.subjectAgentId,
4200
      submissionMethod: verifiedSubmissionMethod,
4201
      ratingDimensionCount: Object.keys(stored.ratings).length,
4202
      reasonCodeCount: stored.reasonCodes.length,
4203
      evidenceReferenceCount: stored.evidenceReferences.length,
4204
      confidence: stored.confidence,
4205
      hasPrivateComment: Boolean(stored.privateComment),
4206
    });
4207
    const requestBase = { ...request, status: 'submitted' as const };
4208
    delete requestBase.platformAttestation;
4209
    const updatedRequest = FeedbackRequestSchema.parse({
4210
      ...requestBase,
4211
      platformAttestation: attestSessionRecord(this.gatewaySecret(), requestBase),
4212
    });
4213
    await this.upsert(operatorId, 'feedback_request', request.requestId, updatedRequest);
4214
    const release = await this.finalizeInteractionConclusion(feedback.interactionId);
4215
    return {
4216
      feedbackId: stored.feedbackId,
4217
      status: 'submitted' as const,
4218
      revealed: release.released && release.feedbackRevealed,
4219
      ...(release.released ? { conclusion: release.conclusion } : {}),
4220
    };
4221
  }
4222
4223
  async processDueFeedback(now = new Date()) {
4224
    await this.ensureSchema();
4225
    const expiredInteractions = await this.sql`
4226
      UPDATE openclasp_federated_interactions
4227
      SET status = 'expired',
4228
        payload = jsonb_set(
4229
          jsonb_set(payload, '{status}', '"expired"'::jsonb),
4230
          '{updatedAt}', to_jsonb(${now.toISOString()}::text)
4231
        ),
4232
        updated_at = NOW()
4233
      WHERE status IN ('pending', 'active') AND expires_at <= ${now.toISOString()}
4234
      RETURNING interaction_id
4235
    `;
4236
    if (expiredInteractions.length) {
4237
      await this.sql`
4238
        UPDATE openclasp_live_sessions
4239
        SET status = 'failed', completed_at = COALESCE(completed_at, NOW()),
4240
          last_error = COALESCE(last_error, 'Interaction expired before bilateral completion')
4241
        WHERE status IN ('preparing', 'active')
4242
          AND interaction_id IN (
4243
            SELECT interaction_id FROM openclasp_federated_interactions
4244
            WHERE status = 'expired' AND expires_at <= ${now.toISOString()}
4245
          )
4246
      `;
4247
      await Promise.all(
4248
        expiredInteractions.flatMap((row) => {
4249
          const interactionId = String(row.interaction_id);
4250
          return [
4251
            this.journalFederatedInteraction(interactionId),
4252
            this.journalLiveSessionState(interactionId),
4253
          ];
4254
        }),
4255
      );
4256
    }
4257
    const [rows, backfillRows] = await Promise.all([
4258
      this.sql`
4259
        SELECT operator_id, record_id, payload FROM openclasp_records
4260
        WHERE kind = 'feedback_request'
4261
          AND payload->>'status' = 'pending'
4262
          AND (payload->>'dueAt')::timestamptz <= ${now.toISOString()}
4263
        LIMIT 500
4264
      `,
4265
      this.sql`
4266
        SELECT source.interaction_id
4267
        FROM (
4268
          SELECT conclusion.payload->>'interactionId' AS interaction_id
4269
          FROM openclasp_records conclusion
4270
          WHERE conclusion.kind = 'interaction_conclusion'
4271
            AND NOT EXISTS (
4272
              SELECT 1 FROM openclasp_records eligibility
4273
              WHERE eligibility.operator_id = conclusion.operator_id
4274
                AND eligibility.kind = 'learning_eligibility'
4275
                AND eligibility.payload->>'interactionId' = conclusion.payload->>'interactionId'
4276
            )
4277
          UNION
4278
          SELECT report.payload->>'interactionId' AS interaction_id
4279
          FROM openclasp_records report
4280
          WHERE report.kind = 'completion_report'
4281
            AND NOT EXISTS (
4282
              SELECT 1 FROM openclasp_records conclusion
4283
              WHERE conclusion.kind = 'interaction_conclusion'
4284
                AND conclusion.payload->>'interactionId' = report.payload->>'interactionId'
4285
            )
4286
        ) source
4287
        WHERE source.interaction_id IS NOT NULL
4288
        GROUP BY source.interaction_id
4289
        ORDER BY source.interaction_id ASC
4290
        LIMIT 200
4291
      `,
4292
    ]);
4293
    const interactions = new Set<string>();
4294
    for (const row of rows) {
4295
      const request = FeedbackRequestSchema.parse(row.payload);
4296
      const base = { ...request, status: 'expired' as const };
4297
      delete base.platformAttestation;
4298
      const expired = FeedbackRequestSchema.parse({
4299
        ...base,
4300
        platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
4301
      });
4302
      await this.upsert(
4303
        String(row.operator_id),
4304
        'feedback_request',
4305
        String(row.record_id),
4306
        expired,
4307
      );
4308
      interactions.add(request.interactionId);
4309
    }
4310
    for (const row of backfillRows) interactions.add(String(row.interaction_id));
4311
    let released = 0;
4312
    for (const interactionId of interactions) {
4313
      const result = await this.finalizeInteractionConclusion(interactionId);
4314
      if (result.released) released += 1;
4315
    }
4316
    return {
4317
      expired: rows.length,
4318
      interactionsExpired: expiredInteractions.length,
4319
      released,
4320
      backfilled: backfillRows.length,
4321
    };
4322
  }
4323
4324
  async recordSessionFeedback(token: string, value: InteractionFeedback) {
4325
    const feedback = InteractionFeedbackSchema.parse(value);
4326
    const grant = this.verifySessionGrant(token);
4327
    if (
4328
      grant.interactionId !== feedback.interactionId ||
4329
      grant.senderAgentId !== feedback.reviewerAgentId ||
4330
      grant.recipientAgentId !== feedback.subjectAgentId
4331
    )
4332
      throw new Error('Session credential does not match the feedback');
4333
    const owners = await this.sql`
4334
      SELECT initiator_operator_id, responder_operator_id,
4335
        initiator_agent_id, responder_agent_id
4336
      FROM openclasp_federated_interactions
4337
      WHERE interaction_id = ${feedback.interactionId}
4338
      LIMIT 1
4339
    `;
4340
    const row = owners[0];
4341
    if (!row) throw new Error('Interaction not found');
4342
    const operatorId = String(
4343
      row.initiator_agent_id === feedback.reviewerAgentId
4344
        ? row.initiator_operator_id
4345
        : row.responder_operator_id,
4346
    );
4347
    return this.submitInteractionFeedback(
4348
      operatorId,
4349
      feedback.reviewerAgentId,
4350
      feedback,
4351
      'runtime_session',
4352
    );
4353
  }
4354
4355
  async recordLiveSessionEvent(token: string, value: LiveSessionEvent) {
4356
    await this.ensureSchema();
4357
    const event = LiveSessionEventSchema.parse(value);
4358
    const grant = this.verifySessionGrant(token);
4359
    if (grant.interactionId !== event.interactionId || grant.senderAgentId !== event.agentId)
4360
      throw new Error('Session credential does not match the event');
4361
    const sessions = await this.sql`
4362
      SELECT 1 FROM openclasp_live_sessions
4363
      WHERE interaction_id = ${event.interactionId}
4364
        AND status = 'active'
4365
        AND ${event.agentId} IN (initiator_agent_id, responder_agent_id)
4366
    `;
4367
    if (!sessions.length) throw new Error('Active live session not found');
4368
    const attestation = attestSessionRecord(this.gatewaySecret(), event);
4369
    const encoded = JSON.stringify({ ...event, attestation });
4370
    const rows = await this.sql`
4371
      INSERT INTO openclasp_live_session_events(
4372
        interaction_id, event_id, agent_id, sequence, event
4373
      ) SELECT ${event.interactionId}, ${event.eventId}, ${event.agentId}, ${event.sequence}, ${encoded}::jsonb
4374
      WHERE (
4375
        SELECT COUNT(*) FROM openclasp_live_session_events
4376
        WHERE interaction_id = ${event.interactionId}
4377
      ) < 1000
4378
      ON CONFLICT DO NOTHING
4379
      RETURNING event_id
4380
    `;
4381
    if (rows.length)
4382
      await this.journalLiveSessionEvent({
4383
        ...event,
4384
        attestation,
4385
      });
4386
    a2aInfo('session.event_recorded', {
4387
      interactionId: event.interactionId,
4388
      agentId: event.agentId,
4389
      eventId: event.eventId,
4390
      eventType: event.type,
4391
      sequence: event.sequence,
4392
      recorded: rows.length > 0,
4393
      evidenceReferenceCount: event.evidenceReferences.length,
4394
    });
4395
    if (event.type === 'session_completed' || event.type === 'session_failed') {
4396
      const terminal = await this.sql`
4397
        SELECT COUNT(DISTINCT agent_id) AS count
4398
        FROM openclasp_live_session_events
4399
        WHERE interaction_id = ${event.interactionId}
4400
          AND event->>'type' IN ('session_completed', 'session_failed')
4401
      `;
4402
      if (Number(terminal[0]?.count ?? 0) >= 2) {
4403
        await this.sql`
4404
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
4405
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
4406
        `;
4407
        const completedAt = new Date().toISOString();
4408
        await this.sql`
4409
          UPDATE openclasp_federated_interactions
4410
          SET status = 'completed',
4411
            payload = jsonb_set(
4412
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
4413
              '{updatedAt}', to_jsonb(${completedAt}::text)
4414
            ),
4415
            updated_at = NOW()
4416
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
4417
        `;
4418
        await Promise.all([
4419
          this.journalLiveSessionState(event.interactionId),
4420
          this.journalFederatedInteraction(event.interactionId),
4421
        ]);
4422
        a2aInfo('session.terminal_events_complete', {
4423
          interactionId: event.interactionId,
4424
          terminalReporterCount: Number(terminal[0]?.count ?? 0),
4425
        });
4426
      }
4427
    }
4428
    return {
4429
      recorded: rows.length > 0,
4430
      deduplicated: rows.length === 0,
4431
      eventId: event.eventId,
4432
      attestation,
4433
    };
4434
  }
4435
4436
  async createFederatedInteraction(
4437
    operatorId: string,
4438
    value: FederatedInteraction,
4439
  ): Promise<FederatedInteraction> {
4440
    await this.ensureSchema();
4441
    let interaction = FederatedInteractionSchema.parse(value);
4442
    if (interaction.status !== 'pending') throw new Error('New interactions must be pending');
4443
    if (interaction.contractRevision !== 1 || interaction.contractRevisions.length)
4444
      throw new Error('New interactions cannot supply contract revision history');
4445
    if (interaction.contract.interactionId !== interaction.interactionId)
4446
      throw new Error('Contract interaction ID does not match');
4447
    if (canonicalHash(interaction.contract) !== interaction.termsHash)
4448
      throw new Error('Contract hash does not match the immutable terms');
4449
    if (
4450
      interaction.contract.parties.length !== 2 ||
4451
      interaction.contract.parties[0] !== interaction.initiatorAgentId ||
4452
      interaction.contract.parties[1] !== interaction.responderAgentId
4453
    )
4454
      throw new Error('Contract parties do not match the interaction participants');
4455
    const acceptanceEntries = Object.entries(interaction.acceptances);
4456
    if (
4457
      acceptanceEntries.length !== 1 ||
4458
      acceptanceEntries[0]?.[0] !== interaction.initiatorAgentId ||
4459
      acceptanceEntries[0]?.[1].agentId !== interaction.initiatorAgentId ||
4460
      acceptanceEntries[0]?.[1].termsHash !== interaction.termsHash ||
4461
      !['oauth_installation', 'oauth_account'].includes(acceptanceEntries[0]?.[1].method ?? '')
4462
    )
4463
      throw new Error('A pending interaction requires only the initiator acceptance');
4464
    if (Date.parse(interaction.expiresAt) <= Date.now())
4465
      throw new Error('Interaction expiry must be in the future');
4466
    interaction = withContractRevisionHistory(interaction);
4467
    const owners = await this.sql`
4468
      SELECT agent_id, operator_id, card FROM openclasp_public_agents
4469
      WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
4470
    `;
4471
    const initiator = owners.find((row) => row.agent_id === interaction.initiatorAgentId);
4472
    const responder = owners.find((row) => row.agent_id === interaction.responderAgentId);
4473
    if (!initiator || initiator.operator_id !== operatorId)
4474
      throw new Error('The initiating agent must be published and owned by this account');
4475
    if (!responder) throw new Error('Responder is not a published OpenClasp agent');
4476
    if (responder.operator_id === operatorId)
4477
      throw new Error('Federated interactions require agents from different accounts');
4478
    const responderCard = normalizePublicAgentCard(responder.card);
4479
    if (
4480
      !responderCard.transports.some(
4481
        (transport) =>
4482
          transport.endpoint === interaction.responderTransport.endpoint &&
4483
          transport.protocolBinding === interaction.responderTransport.protocolBinding,
4484
      )
4485
    )
4486
      throw new Error('Responder transport does not match its published Agent Card');
4487
    const encoded = JSON.stringify(interaction);
4488
    const rows = await this.sql`
4489
      INSERT INTO openclasp_federated_interactions(
4490
        interaction_id, initiator_operator_id, responder_operator_id,
4491
        initiator_agent_id, responder_agent_id, status, payload, expires_at
4492
      ) VALUES (
4493
        ${interaction.interactionId}, ${operatorId}, ${responder.operator_id},
4494
        ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
4495
        ${interaction.status}, ${encoded}::jsonb, ${interaction.expiresAt}
4496
      )
4497
      RETURNING payload
4498
    `;
4499
    const stored = FederatedInteractionSchema.parse(rows[0]?.payload);
4500
    await this.journalFederatedInteraction(stored.interactionId);
4501
    a2aInfo('interaction.created', {
4502
      interactionId: stored.interactionId,
4503
      initiatorAgentId: stored.initiatorAgentId,
4504
      responderAgentId: stored.responderAgentId,
4505
      contractRevision: stored.contractRevision,
4506
      successCriteriaCount: stored.contract.successCriteria.length,
4507
      expiresAt: stored.expiresAt,
4508
    });
4509
    const profiles = await this.sql`
4510
      SELECT payload FROM openclasp_records
4511
      WHERE operator_id = ${responder.operator_id}
4512
        AND kind = 'agent_profile'
4513
        AND record_id = ${interaction.responderAgentId}
4514
      LIMIT 1
4515
    `;
4516
    const responderProfile = profiles[0]?.payload as AgentProfile | undefined;
4517
    if (responderProfile && canAutoAcceptInteraction(responderProfile, stored))
4518
      return this.respondToFederatedInteraction(
4519
        String(responder.operator_id),
4520
        stored.interactionId,
4521
        stored.responderAgentId,
4522
        'accept',
4523
        'policy_auto_accept',
4524
      );
4525
    return stored;
4526
  }
4527
4528
  async listFederatedInteractions(operatorId: string): Promise<FederatedInteraction[]> {
4529
    await this.ensureSchema();
4530
    const expired = await this.sql`
4531
      UPDATE openclasp_federated_interactions
4532
      SET status = 'expired', payload = jsonb_set(payload, '{status}', '"expired"'::jsonb), updated_at = NOW()
4533
      WHERE status = 'pending' AND expires_at <= NOW()
4534
      RETURNING interaction_id
4535
    `;
4536
    await Promise.all(
4537
      expired.map((row) => this.journalFederatedInteraction(String(row.interaction_id))),
4538
    );
4539
    const rows = await this.sql`
4540
      SELECT payload FROM openclasp_federated_interactions
4541
      WHERE initiator_operator_id = ${operatorId} OR responder_operator_id = ${operatorId}
4542
      ORDER BY updated_at DESC
4543
    `;
4544
    return rows.map((row) =>
4545
      withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload)),
4546
    );
4547
  }
4548
4549
  async getFederatedInteraction(
4550
    operatorId: string,
4551
    interactionId: string,
4552
  ): Promise<FederatedInteraction | undefined> {
4553
    const values = await this.listFederatedInteractions(operatorId);
4554
    return values.find((value) => value.interactionId === interactionId);
4555
  }
4556
4557
  async respondToFederatedInteraction(
4558
    operatorId: string,
4559
    interactionId: string,
4560
    agentId: string,
4561
    decision: 'accept' | 'reject',
4562
    method: 'oauth_installation' | 'oauth_account' | 'policy_auto_accept' = 'oauth_account',
4563
  ): Promise<FederatedInteraction> {
4564
    await this.ensureSchema();
4565
    a2aInfo('invitation.response_started', {
4566
      interactionId,
4567
      agentId,
4568
      decision,
4569
      method,
4570
    });
4571
    const rows = await this.sql`
4572
      SELECT payload, responder_operator_id, responder_agent_id
4573
      FROM openclasp_federated_interactions WHERE interaction_id = ${interactionId}
4574
    `;
4575
    const row = rows[0];
4576
    if (!row) throw new Error('Interaction invitation not found');
4577
    if (row.responder_operator_id !== operatorId || row.responder_agent_id !== agentId)
4578
      throw new Error('Only the invited agent may respond');
4579
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
4580
    if (current.status !== 'pending') throw new Error('Invitation is no longer pending');
4581
    const now = new Date().toISOString();
4582
    if (Date.parse(current.expiresAt) <= Date.now()) {
4583
      const expired = { ...current, status: 'expired' as const, updatedAt: now };
4584
      await this.updateFederatedInteraction(
4585
        interactionId,
4586
        current.status,
4587
        expired,
4588
        current.updatedAt,
4589
      );
4590
      throw new Error('Invitation has expired');
4591
    }
4592
    const proposed = openContractRevision(current);
4593
    if (!proposed || proposed.termsHash !== current.termsHash)
4594
      throw new Error('No current contract proposal is available');
4595
    const acceptance = {
4596
      agentId,
4597
      method,
4598
      termsHash: current.termsHash,
4599
      acceptedAt: now,
4600
    };
4601
    const proposalBase: ContractRevision = {
4602
      ...proposed,
4603
      status: decision === 'accept' ? 'accepted' : 'rejected',
4604
      acceptances:
4605
        decision === 'accept'
4606
          ? { ...proposed.acceptances, [agentId]: acceptance }
4607
          : proposed.acceptances,
4608
      updatedAt: now,
4609
    };
4610
    const resolvedProposal =
4611
      decision === 'accept'
4612
        ? ContractRevisionSchema.parse({
4613
            ...proposalBase,
4614
            platformAttestation: attestSessionRecord(this.gatewaySecret(), proposalBase),
4615
          })
4616
        : proposalBase;
4617
    const next = FederatedInteractionSchema.parse({
4618
      ...current,
4619
      status: decision === 'accept' ? 'active' : 'rejected',
4620
      updatedAt: now,
4621
      acceptances: decision === 'accept' ? resolvedProposal.acceptances : current.acceptances,
4622
      contractRevisions: current.contractRevisions.map((revision) =>
4623
        revision.revisionId === resolvedProposal.revisionId ? resolvedProposal : revision,
4624
      ),
4625
    });
4626
    if (decision === 'accept') {
4627
      try {
4628
        await this.brokerLiveSession(next);
4629
      } catch (error) {
4630
        const reason = error instanceof Error ? error.message.slice(0, 500) : 'Live session failed';
4631
        await this.sql`
4632
          UPDATE openclasp_agent_runtimes SET last_error = ${reason}, updated_at = NOW()
4633
          WHERE agent_id IN (${current.initiatorAgentId}, ${current.responderAgentId})
4634
        `;
4635
        a2aError('interaction.activation_failed', error, {
4636
          interactionId,
4637
          initiatorAgentId: current.initiatorAgentId,
4638
          responderAgentId: current.responderAgentId,
4639
        });
4640
        throw error;
4641
      }
4642
    }
4643
    const updated = await this.updateFederatedInteraction(
4644
      interactionId,
4645
      current.status,
4646
      next,
4647
      current.updatedAt,
4648
    );
4649
    if (!updated) throw new Error('Invitation was already handled');
4650
    a2aInfo('interaction.status_changed', {
4651
      interactionId,
4652
      agentId,
4653
      status: next.status,
4654
      contractRevision: next.contractRevision,
4655
    });
4656
    return next;
4657
  }
4658
4659
  async proposeContractRevision(
4660
    operatorId: string,
4661
    interactionId: string,
4662
    agentId: string,
4663
    contract: InteractionContract,
4664
    expectedTermsHash?: string,
4665
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
4666
  ): Promise<FederatedInteraction> {
4667
    await this.ensureSchema();
4668
    const rows = await this.sql`
4669
      SELECT payload, initiator_operator_id, responder_operator_id,
4670
        initiator_agent_id, responder_agent_id
4671
      FROM openclasp_federated_interactions
4672
      WHERE interaction_id = ${interactionId}
4673
    `;
4674
    const row = rows[0];
4675
    if (!row) throw new Error('Interaction not found');
4676
    const ownsParticipant =
4677
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
4678
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
4679
    if (!ownsParticipant) throw new Error('Only a participating agent may propose contract terms');
4680
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
4681
    if (!['pending', 'active'].includes(current.status))
4682
      throw new Error('This interaction no longer accepts contract proposals');
4683
    const parsedContract = InteractionContractSchema.parse(contract);
4684
    if (parsedContract.interactionId !== interactionId)
4685
      throw new Error('Contract interaction ID does not match');
4686
    if (
4687
      parsedContract.parties.length !== 2 ||
4688
      parsedContract.parties[0] !== current.initiatorAgentId ||
4689
      parsedContract.parties[1] !== current.responderAgentId
4690
    )
4691
      throw new Error('Contract parties cannot change during negotiation');
4692
    const open = openContractRevision(current);
4693
    const expected = open?.termsHash ?? current.termsHash;
4694
    if (expectedTermsHash && expectedTermsHash !== expected)
4695
      throw new Error('Contract terms changed; fetch the interaction before countering');
4696
    const termsHash = canonicalHash(parsedContract);
4697
    if (termsHash === expected) throw new Error('Proposed terms are unchanged');
4698
    const now = new Date().toISOString();
4699
    const revisionNumber =
4700
      Math.max(
4701
        current.contractRevision,
4702
        ...current.contractRevisions.map((item) => item.revision),
4703
      ) + 1;
4704
    const acceptance = {
4705
      agentId,
4706
      method,
4707
      termsHash,
4708
      acceptedAt: now,
4709
    } as const;
4710
    const revision = ContractRevisionSchema.parse({
4711
      revisionId: crypto.randomUUID(),
4712
      interactionId,
4713
      revision: revisionNumber,
4714
      previousTermsHash: expected,
4715
      termsHash,
4716
      contract: parsedContract,
4717
      proposedByAgentId: agentId,
4718
      status: 'proposed',
4719
      acceptances: { [agentId]: acceptance },
4720
      createdAt: now,
4721
      updatedAt: now,
4722
    });
4723
    const revisions = [
4724
      ...current.contractRevisions.map((item) =>
4725
        item.status === 'proposed'
4726
          ? ContractRevisionSchema.parse({ ...item, status: 'superseded', updatedAt: now })
4727
          : item,
4728
      ),
4729
      revision,
4730
    ];
4731
    const next = FederatedInteractionSchema.parse({
4732
      ...current,
4733
      ...(current.status === 'pending'
4734
        ? {
4735
            contract: parsedContract,
4736
            termsHash,
4737
            acceptances: revision.acceptances,
4738
            contractRevision: revisionNumber,
4739
          }
4740
        : {}),
4741
      contractRevisions: revisions,
4742
      updatedAt: now,
4743
    });
4744
    const updated = await this.updateFederatedInteraction(
4745
      interactionId,
4746
      current.status,
4747
      next,
4748
      current.updatedAt,
4749
    );
4750
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
4751
    return next;
4752
  }
4753
4754
  async respondToContractRevision(
4755
    operatorId: string,
4756
    interactionId: string,
4757
    agentId: string,
4758
    revisionId: string,
4759
    decision: 'accept' | 'reject',
4760
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
4761
  ): Promise<FederatedInteraction> {
4762
    await this.ensureSchema();
4763
    const rows = await this.sql`
4764
      SELECT payload, initiator_operator_id, responder_operator_id,
4765
        initiator_agent_id, responder_agent_id
4766
      FROM openclasp_federated_interactions
4767
      WHERE interaction_id = ${interactionId}
4768
    `;
4769
    const row = rows[0];
4770
    if (!row) throw new Error('Interaction not found');
4771
    const ownsParticipant =
4772
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
4773
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
4774
    if (!ownsParticipant) throw new Error('Only a participating agent may respond to terms');
4775
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
4776
    if (!['pending', 'active'].includes(current.status))
4777
      throw new Error('This interaction no longer accepts contract responses');
4778
    const proposal = openContractRevision(current);
4779
    if (!proposal || proposal.revisionId !== revisionId)
4780
      throw new Error('Contract proposal is no longer current');
4781
    if (proposal.acceptances[agentId]) throw new Error('Agent already accepted this proposal');
4782
    const now = new Date().toISOString();
4783
    const acceptances =
4784
      decision === 'accept'
4785
        ? {
4786
            ...proposal.acceptances,
4787
            [agentId]: { agentId, method, termsHash: proposal.termsHash, acceptedAt: now },
4788
          }
4789
        : proposal.acceptances;
4790
    const fullyAccepted = current.contract.parties.every((party) => party in acceptances);
4791
    const revisionBase = ContractRevisionSchema.parse({
4792
      ...proposal,
4793
      status: decision === 'reject' ? 'rejected' : fullyAccepted ? 'accepted' : 'proposed',
4794
      acceptances,
4795
      updatedAt: now,
4796
    });
4797
    const revision =
4798
      fullyAccepted && decision === 'accept'
4799
        ? ContractRevisionSchema.parse({
4800
            ...revisionBase,
4801
            platformAttestation: attestSessionRecord(this.gatewaySecret(), revisionBase),
4802
          })
4803
        : revisionBase;
4804
    const next = FederatedInteractionSchema.parse({
4805
      ...current,
4806
      status:
4807
        current.status === 'pending'
4808
          ? decision === 'reject'
4809
            ? 'rejected'
4810
            : fullyAccepted
4811
              ? 'active'
4812
              : 'pending'
4813
          : 'active',
4814
      ...(fullyAccepted
4815
        ? {
4816
            contract: revision.contract,
4817
            termsHash: revision.termsHash,
4818
            acceptances: revision.acceptances,
4819
            contractRevision: revision.revision,
4820
          }
4821
        : {}),
4822
      contractRevisions: current.contractRevisions.map((item) =>
4823
        item.revisionId === revision.revisionId ? revision : item,
4824
      ),
4825
      updatedAt: now,
4826
    });
4827
    if (current.status === 'pending' && decision === 'accept' && fullyAccepted)
4828
      await this.brokerLiveSession(next);
4829
    const updated = await this.updateFederatedInteraction(
4830
      interactionId,
4831
      current.status,
4832
      next,
4833
      current.updatedAt,
4834
    );
4835
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
4836
    return next;
4837
  }
4838
4839
  private async updateFederatedInteraction(
4840
    interactionId: string,
4841
    expectedStatus: string,
4842
    value: FederatedInteraction,
4843
    expectedUpdatedAt?: string,
4844
  ): Promise<boolean> {
4845
    const encoded = JSON.stringify(FederatedInteractionSchema.parse(value));
4846
    const rows = await this.sql`
4847
      UPDATE openclasp_federated_interactions
4848
      SET status = ${value.status}, payload = ${encoded}::jsonb, updated_at = NOW()
4849
      WHERE interaction_id = ${interactionId} AND status = ${expectedStatus}
4850
        AND (${expectedUpdatedAt ?? null}::text IS NULL OR payload->>'updatedAt' = ${expectedUpdatedAt ?? null})
4851
      RETURNING interaction_id
4852
    `;
4853
    if (rows.length) await this.journalFederatedInteraction(interactionId);
4854
    return rows.length > 0;
4855
  }
4856
4857
  async getSettings(operatorId: string): Promise<AccountSettings> {
4858
    await this.ensureSchema();
4859
    const rows = await this.sql`
4860
      SELECT display_name, contribution_enabled
4861
      FROM openclasp_account_settings
4862
      WHERE operator_id = ${operatorId}
4863
    `;
4864
    const row = rows[0] as
4865
      | {
4866
          display_name: string;
4867
          contribution_enabled: boolean;
4868
        }
4869
      | undefined;
4870
    return row
4871
      ? {
4872
          displayName: row.display_name,
4873
          contributionEnabled: row.contribution_enabled,
4874
          rawConversationsStored: false,
4875
        }
4876
      : defaults;
4877
  }
4878
4879
  async saveSettings(
4880
    operatorId: string,
4881
    settings: Omit<AccountSettings, 'rawConversationsStored'>,
4882
  ): Promise<AccountSettings> {
4883
    await this.ensureSchema();
4884
    await this.sql`
4885
      INSERT INTO openclasp_account_settings(
4886
        operator_id, display_name, contribution_enabled
4887
      ) VALUES (
4888
        ${operatorId}, ${settings.displayName}, ${settings.contributionEnabled}
4889
      )
4890
      ON CONFLICT (operator_id) DO UPDATE SET
4891
        display_name = EXCLUDED.display_name,
4892
        contribution_enabled = EXCLUDED.contribution_enabled,
4893
        updated_at = NOW()
4894
    `;
4895
    return { ...settings, rawConversationsStored: false };
4896
  }
4897
}