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

Viewing files at commit 92498f6892e8Commit detailsView latest

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