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

Viewing files at commit 68f53f7907caCommit detailsView latest

packages/persistence/src/hosted.ts 4666 lines · 182.7 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_records
2022
      WHERE operator_id = ${operatorId}
2023
        AND (
2024
          (record_id = ${agentId} AND kind IN ('agent', 'agent_profile', 'publication', 'presence'))
2025
          OR (kind = 'installation' AND payload->>'agentId' = ${agentId})
2026
          OR (kind = 'setup_request' AND payload->>'existingAgentId' = ${agentId})
2027
        )
2028
    `;
2029
    return {
2030
      agentId,
2031
      deleted: true as const,
2032
      historyRetained: true as const,
2033
    };
2034
  }
2035
2036
  private async sessionParticipant(agentId: string) {
2037
    const rows = await this.sql`
2038
      SELECT agents.operator_id, agents.card, profile.payload AS profile,
2039
        runtime.endpoint, runtime.a2a_endpoint
2040
      FROM openclasp_public_agents agents
2041
      LEFT JOIN openclasp_records profile
2042
        ON profile.operator_id = agents.operator_id
2043
        AND profile.kind = 'agent_profile'
2044
        AND profile.record_id = agents.agent_id
2045
      INNER JOIN openclasp_agent_runtimes runtime
2046
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
2047
      WHERE agents.agent_id = ${agentId}
2048
      LIMIT 1
2049
    `;
2050
    const row = rows[0];
2051
    if (!row) throw new Error(`Published agent ${agentId} was not found`);
2052
    const card = normalizePublicAgentCard(row.card);
2053
    if (!row.endpoint) throw new Error(`Agent ${agentId} does not have a verified live runtime`);
2054
    return {
2055
      agentId,
2056
      operatorId: String(row.operator_id),
2057
      mode: 'persistent_runtime' as const,
2058
      callbackEndpoint: String(row.endpoint),
2059
      a2aEndpoint: String(row.a2a_endpoint ?? row.endpoint),
2060
      card: PublicAgentCardSchema.parse({ ...card, agentMode: 'persistent_runtime' }),
2061
    };
2062
  }
2063
2064
  private async signedRuntimeRequest(
2065
    runtime: { callbackEndpoint: string },
2066
    requestId: string,
2067
    value: unknown,
2068
  ) {
2069
    const body = JSON.stringify(value);
2070
    const timestamp = new Date().toISOString();
2071
    return postRuntimeJson(runtime.callbackEndpoint, value, {
2072
      'openclasp-request-id': requestId,
2073
      'openclasp-timestamp': timestamp,
2074
      'openclasp-signature': `v1=${signSessionControl(this.gatewaySecret(), requestId, timestamp, body)}`,
2075
    });
2076
  }
2077
2078
  private sessionGrant(
2079
    interaction: FederatedInteraction,
2080
    senderAgentId: string,
2081
    recipientAgentId: string,
2082
  ) {
2083
    return issueSessionGrant(this.gatewaySecret(), {
2084
      interactionId: interaction.interactionId,
2085
      senderAgentId,
2086
      recipientAgentId,
2087
      expiresAt: Math.min(Date.parse(interaction.expiresAt), Date.now() + 60 * 60_000),
2088
    });
2089
  }
2090
2091
  private async privateCounterpartyInsights(
2092
    viewerOperatorId: string,
2093
    counterparty: { agentId: string; card: PublicAgentCard },
2094
    taskCategory: string,
2095
  ): Promise<{
2096
    insights: LiveSessionInsight[];
2097
    relevantSampleSize: number;
2098
    historyConfidence: number;
2099
  }> {
2100
    const rows = await this.sql`
2101
      SELECT record_id, payload
2102
      FROM openclasp_records
2103
      WHERE operator_id = ${viewerOperatorId}
2104
        AND kind = 'profile'
2105
        AND payload->>'agentId' = ${counterparty.agentId}
2106
        AND payload->>'taskCategory' = ${taskCategory}
2107
      ORDER BY (payload->>'sampleSize')::integer DESC, updated_at DESC
2108
    `;
2109
    const profiles: ContextualProfile[] = rows
2110
      .map<Record<string, unknown>>((row) => ({
2111
        recordId: String(row.record_id),
2112
        ...(row.payload as Record<string, unknown>),
2113
      }))
2114
      .filter(
2115
        (profile) =>
2116
          typeof profile.agentVersion === 'string' &&
2117
          typeof profile.sampleSize === 'number' &&
2118
          typeof profile.updatedAt === 'string' &&
2119
          typeof profile.completion === 'number' &&
2120
          typeof profile.acceptance === 'number' &&
2121
          typeof profile.specification === 'number' &&
2122
          typeof profile.deadline === 'number' &&
2123
          typeof profile.communication === 'number' &&
2124
          typeof profile.evidence === 'number' &&
2125
          typeof profile.scope === 'number' &&
2126
          typeof profile.disputes === 'number',
2127
      ) as ContextualProfile[];
2128
    const exact = profiles.find(
2129
      (profile) => profile.agentVersion === counterparty.card.agentVersion,
2130
    );
2131
    const current = exact ?? profiles[0];
2132
    const insights: LiveSessionInsight[] = [];
2133
    let relevantSampleSize = 0;
2134
    let historyConfidence = 0;
2135
    if (!current) {
2136
      insights.push({
2137
        code: 'limited_verified_history',
2138
        severity: 'caution',
2139
        message: `No eligible ${taskCategory} history is available for this agent version. Ask for task-relevant evidence.`,
2140
        evidenceReferences: [],
2141
        requirementReferences: [],
2142
      });
2143
    } else {
2144
      const ageDays = Math.max(
2145
        0,
2146
        (Date.now() - Date.parse(String(current.updatedAt))) / 86_400_000,
2147
      );
2148
      const freshness = Math.exp(-ageDays / 180);
2149
      const sampleSize = Number(current.sampleSize);
2150
      const effectiveSampleSize = Number(current.effectiveSampleSize ?? sampleSize);
2151
      const versionChanged = current.agentVersion !== counterparty.card.agentVersion;
2152
      const confidence = Math.min(
2153
        0.95,
2154
        (effectiveSampleSize / (effectiveSampleSize + 5)) * freshness * (versionChanged ? 0.35 : 1),
2155
      );
2156
      relevantSampleSize = sampleSize;
2157
      historyConfidence = confidence;
2158
      const reference = `openclasp:profile:${current.recordId}`;
2159
      insights.push({
2160
        code: versionChanged ? 'version_history_only' : 'contextual_history',
2161
        severity: versionChanged || confidence < 0.25 ? 'caution' : 'info',
2162
        message: versionChanged
2163
          ? `Only prior-version ${taskCategory} history is available (${sampleSize} eligible interaction${sampleSize === 1 ? '' : 's'}); confidence is reduced for version ${counterparty.card.agentVersion}.`
2164
          : `Based on ${sampleSize} eligible ${taskCategory} interaction${sampleSize === 1 ? '' : 's'}; confidence ${(confidence * 100).toFixed(0)}%.`,
2165
        evidenceReferences: [reference],
2166
        requirementReferences: [],
2167
      });
2168
      const weakDimensions = [
2169
        ['completion', Number(current.completion)],
2170
        ['output acceptance', Number(current.acceptance)],
2171
        ['specification adherence', Number(current.specification)],
2172
        ['deadline reliability', Number(current.deadline)],
2173
        ['communication quality', Number(current.communication)],
2174
        ['evidence quality', Number(current.evidence)],
2175
        ['scope adherence', Number(current.scope)],
2176
      ].filter(([, score]) => Number(score) < 0.7);
2177
      if (weakDimensions.length)
2178
        insights.push({
2179
          code: 'historical_weaknesses',
2180
          severity: weakDimensions.some(([, score]) => Number(score) < 0.5) ? 'high' : 'caution',
2181
          message: `Observed weaker areas: ${weakDimensions
2182
            .map(([name, score]) => `${String(name)} ${(Number(score) * 100).toFixed(0)}%`)
2183
            .join(', ')}.`,
2184
          evidenceReferences: [reference],
2185
          requirementReferences: [],
2186
        });
2187
      if (Number(current.disputes) > 0.25)
2188
        insights.push({
2189
          code: 'elevated_dispute_history',
2190
          severity: Number(current.disputes) >= 0.5 ? 'high' : 'caution',
2191
          message: `Eligible feedback shows a ${(Number(current.disputes) * 100).toFixed(0)}% dispute rate in this task category.`,
2192
          evidenceReferences: [reference],
2193
          requirementReferences: [],
2194
        });
2195
    }
2196
    if (counterparty.card.limitations.length)
2197
      insights.push({
2198
        code: 'declared_limitations',
2199
        severity: 'info',
2200
        message: `Counterparty declares: ${counterparty.card.limitations.join('; ')}`,
2201
        evidenceReferences: [counterparty.card.cardUrl],
2202
        requirementReferences: [],
2203
      });
2204
    return { insights, relevantSampleSize, historyConfidence };
2205
  }
2206
2207
  async brokerLiveSession(interaction: FederatedInteraction) {
2208
    await this.ensureSchema();
2209
    const existing = await this.sql`
2210
      SELECT status FROM openclasp_live_sessions
2211
      WHERE interaction_id = ${interaction.interactionId}
2212
    `;
2213
    if (existing[0]?.status === 'active') return;
2214
    const [initiator, responder] = await Promise.all([
2215
      this.sessionParticipant(interaction.initiatorAgentId),
2216
      this.sessionParticipant(interaction.responderAgentId),
2217
    ]);
2218
    const [initiatorContext, responderContext] = await Promise.all([
2219
      this.privateCounterpartyInsights(
2220
        initiator.operatorId,
2221
        responder,
2222
        interaction.contract.taskCategory,
2223
      ),
2224
      this.privateCounterpartyInsights(
2225
        responder.operatorId,
2226
        initiator,
2227
        interaction.contract.taskCategory,
2228
      ),
2229
    ]);
2230
    const issuedAt = new Date().toISOString();
2231
    const initiatorBrief = buildCounterpartyBrief({
2232
      interactionId: interaction.interactionId,
2233
      contractHash: interaction.termsHash,
2234
      contract: interaction.contract,
2235
      recipientAgentId: initiator.agentId,
2236
      subject: responder.card,
2237
      historyInsights: initiatorContext.insights,
2238
      relevantSampleSize: initiatorContext.relevantSampleSize,
2239
      historyConfidence: initiatorContext.historyConfidence,
2240
      generatedAt: issuedAt,
2241
      expiresAt: interaction.expiresAt,
2242
    });
2243
    const responderBrief = buildCounterpartyBrief({
2244
      interactionId: interaction.interactionId,
2245
      contractHash: interaction.termsHash,
2246
      contract: interaction.contract,
2247
      recipientAgentId: responder.agentId,
2248
      subject: initiator.card,
2249
      historyInsights: responderContext.insights,
2250
      relevantSampleSize: responderContext.relevantSampleSize,
2251
      historyConfidence: responderContext.historyConfidence,
2252
      generatedAt: issuedAt,
2253
      expiresAt: interaction.expiresAt,
2254
    });
2255
    await Promise.all([
2256
      this.upsert(
2257
        initiator.operatorId,
2258
        'counterparty_brief',
2259
        initiatorBrief.briefId,
2260
        initiatorBrief,
2261
      ),
2262
      this.upsert(
2263
        responder.operatorId,
2264
        'counterparty_brief',
2265
        responderBrief.briefId,
2266
        responderBrief,
2267
      ),
2268
    ]);
2269
    const makeOffer = (
2270
      runtime: typeof initiator,
2271
      counterparty: typeof responder,
2272
      role: 'initiator' | 'responder',
2273
      privateInsights: LiveSessionInsight[],
2274
      counterpartyBrief: CounterpartyBrief,
2275
    ) =>
2276
      LiveSessionOfferSchema.parse({
2277
        type: 'openclasp.session.offer',
2278
        version: '1',
2279
        offerId: crypto.randomUUID(),
2280
        interactionId: interaction.interactionId,
2281
        agentId: runtime.agentId,
2282
        role,
2283
        counterparty: {
2284
          agentId: counterparty.agentId,
2285
          name: counterparty.card.name,
2286
          agentVersion: counterparty.card.agentVersion,
2287
          capabilities: counterparty.card.capabilities,
2288
        },
2289
        contract: interaction.contract,
2290
        contractHash: interaction.termsHash,
2291
        privateInsights,
2292
        counterpartyBrief,
2293
        issuedAt,
2294
        expiresAt: interaction.expiresAt,
2295
      });
2296
    const initiatorOffer = makeOffer(
2297
      initiator,
2298
      responder,
2299
      'initiator',
2300
      initiatorBrief.insights,
2301
      initiatorBrief,
2302
    );
2303
    const responderOffer = makeOffer(
2304
      responder,
2305
      initiator,
2306
      'responder',
2307
      responderBrief.insights,
2308
      responderBrief,
2309
    );
2310
    const prepare = async (
2311
      participant: typeof initiator,
2312
      offer: typeof initiatorOffer,
2313
      label: 'Initiator' | 'Responder',
2314
    ) => {
2315
      if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
2316
      const response = await this.signedRuntimeRequest(
2317
        { callbackEndpoint: participant.callbackEndpoint },
2318
        offer.offerId,
2319
        offer,
2320
      );
2321
      if (response.status < 200 || response.status >= 300)
2322
        throw new Error(`${label} runtime is not live (HTTP ${response.status})`);
2323
      return LiveSessionAcceptanceSchema.parse(response.body);
2324
    };
2325
    const [initiatorAcceptance, responderAcceptance] = await Promise.all([
2326
      prepare(initiator, initiatorOffer, 'Initiator'),
2327
      prepare(responder, responderOffer, 'Responder'),
2328
    ]);
2329
    if (
2330
      initiatorAcceptance.offerId !== initiatorOffer.offerId ||
2331
      initiatorAcceptance.agentId !== initiator.agentId ||
2332
      responderAcceptance.offerId !== responderOffer.offerId ||
2333
      responderAcceptance.agentId !== responder.agentId
2334
    )
2335
      throw new Error('Runtime returned a mismatched live-session acceptance');
2336
    await Promise.all([
2337
      ...(initiator.mode === 'persistent_runtime'
2338
        ? [resolvePublicRuntimeEndpoint(initiatorAcceptance.a2aEndpoint)]
2339
        : []),
2340
      ...(responder.mode === 'persistent_runtime'
2341
        ? [resolvePublicRuntimeEndpoint(responderAcceptance.a2aEndpoint)]
2342
        : []),
2343
    ]);
2344
    await this.sql`
2345
      INSERT INTO openclasp_live_sessions(
2346
        interaction_id, initiator_agent_id, responder_agent_id,
2347
        initiator_session_id, responder_session_id,
2348
        initiator_endpoint, responder_endpoint, status, expires_at
2349
      ) VALUES (
2350
        ${interaction.interactionId}, ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
2351
        ${initiatorAcceptance.sessionId}, ${responderAcceptance.sessionId},
2352
        ${initiatorAcceptance.a2aEndpoint}, ${responderAcceptance.a2aEndpoint},
2353
        'preparing', ${interaction.expiresAt}
2354
      )
2355
      ON CONFLICT (interaction_id) DO UPDATE SET
2356
        initiator_session_id = EXCLUDED.initiator_session_id,
2357
        responder_session_id = EXCLUDED.responder_session_id,
2358
        initiator_endpoint = EXCLUDED.initiator_endpoint,
2359
        responder_endpoint = EXCLUDED.responder_endpoint,
2360
        status = 'preparing', expires_at = EXCLUDED.expires_at,
2361
        activated_at = NULL, completed_at = NULL, last_error = NULL
2362
    `;
2363
    await this.journalLiveSessionState(interaction.interactionId);
2364
    const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
2365
      /\/$/,
2366
      '',
2367
    );
2368
    const activatedAt = new Date().toISOString();
2369
    const verificationKey = getSessionVerificationKey(this.gatewaySecret());
2370
    const makeActivation = (
2371
      agentId: string,
2372
      sessionId: string,
2373
      role: 'initiator' | 'responder',
2374
      peerAgentId: string,
2375
      peerSessionId: string,
2376
      peerEndpoint: string,
2377
      privateInsights: LiveSessionInsight[],
2378
      counterpartyBrief: CounterpartyBrief,
2379
    ): LiveSessionActivation => {
2380
      const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
2381
      return LiveSessionActivationSchema.parse({
2382
        type: 'openclasp.session.activation',
2383
        version: '1',
2384
        activationId: crypto.randomUUID(),
2385
        interactionId: interaction.interactionId,
2386
        agentId,
2387
        sessionId,
2388
        role,
2389
        peer: {
2390
          agentId: peerAgentId,
2391
          sessionId: peerSessionId,
2392
          endpoint: peerEndpoint,
2393
          bearerToken,
2394
          verificationKey,
2395
        },
2396
        reporting: {
2397
          endpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/events`,
2398
          completionEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/completion-reports`,
2399
          feedbackEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/feedback`,
2400
          assuranceResponseEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/assurance-responses`,
2401
          bearerToken,
2402
        },
2403
        privateInsights,
2404
        counterpartyBrief,
2405
        contractHash: interaction.termsHash,
2406
        activatedAt,
2407
        expiresAt: interaction.expiresAt,
2408
      });
2409
    };
2410
    const initiatorActivation = makeActivation(
2411
      initiator.agentId,
2412
      initiatorAcceptance.sessionId,
2413
      'initiator',
2414
      responder.agentId,
2415
      responderAcceptance.sessionId,
2416
      responderAcceptance.a2aEndpoint,
2417
      initiatorBrief.insights,
2418
      initiatorBrief,
2419
    );
2420
    const responderActivation = makeActivation(
2421
      responder.agentId,
2422
      responderAcceptance.sessionId,
2423
      'responder',
2424
      initiator.agentId,
2425
      initiatorAcceptance.sessionId,
2426
      initiatorAcceptance.a2aEndpoint,
2427
      responderBrief.insights,
2428
      responderBrief,
2429
    );
2430
    try {
2431
      const activate = async (
2432
        participant: typeof initiator,
2433
        activation: LiveSessionActivation,
2434
        label: 'Initiator' | 'Responder',
2435
      ) => {
2436
        if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
2437
        const response = await this.signedRuntimeRequest(
2438
          { callbackEndpoint: participant.callbackEndpoint },
2439
          activation.activationId,
2440
          activation,
2441
        );
2442
        if (response.status < 200 || response.status >= 300)
2443
          throw new Error(`${label} activation failed with HTTP ${response.status}`);
2444
      };
2445
      await activate(responder, responderActivation, 'Responder');
2446
      await activate(initiator, initiatorActivation, 'Initiator');
2447
      await this.sql`
2448
        UPDATE openclasp_live_sessions
2449
        SET status = 'active', activated_at = NOW(), last_error = NULL
2450
        WHERE interaction_id = ${interaction.interactionId}
2451
      `;
2452
      await this.journalLiveSessionState(interaction.interactionId);
2453
      await this.sql`
2454
        UPDATE openclasp_agent_runtimes
2455
        SET last_seen_at = NOW(), last_error = NULL, updated_at = NOW()
2456
        WHERE agent_id IN (${initiator.agentId}, ${responder.agentId})
2457
      `;
2458
      await Promise.all([
2459
        this.touchAgentPresence(initiator.operatorId, initiator.agentId),
2460
        this.touchAgentPresence(responder.operatorId, responder.agentId),
2461
      ]);
2462
    } catch (error) {
2463
      const reason =
2464
        error instanceof Error ? error.message.slice(0, 500) : 'Session activation failed';
2465
      await this.sql`
2466
        UPDATE openclasp_live_sessions SET status = 'failed', last_error = ${reason}
2467
        WHERE interaction_id = ${interaction.interactionId}
2468
      `;
2469
      await this.journalLiveSessionState(interaction.interactionId);
2470
      throw error;
2471
    }
2472
  }
2473
2474
  async getLiveSession(operatorId: string, interactionId: string, agentId: string) {
2475
    await this.ensureSchema();
2476
    const rows = await this.sql`
2477
      SELECT session.*, interaction.payload,
2478
        initiator.operator_id AS initiator_operator_id,
2479
        responder.operator_id AS responder_operator_id
2480
      FROM openclasp_live_sessions session
2481
      INNER JOIN openclasp_federated_interactions interaction
2482
        ON interaction.interaction_id = session.interaction_id
2483
      INNER JOIN openclasp_public_agents initiator
2484
        ON initiator.agent_id = session.initiator_agent_id
2485
      INNER JOIN openclasp_public_agents responder
2486
        ON responder.agent_id = session.responder_agent_id
2487
      WHERE session.interaction_id = ${interactionId} AND session.status = 'active'
2488
      LIMIT 1
2489
    `;
2490
    const row = rows[0];
2491
    if (!row) throw new Error('Active live session not found');
2492
    const isInitiator = row.initiator_agent_id === agentId;
2493
    const isResponder = row.responder_agent_id === agentId;
2494
    if (!isInitiator && !isResponder) throw new Error('Agent is not a session participant');
2495
    const owner = isInitiator ? row.initiator_operator_id : row.responder_operator_id;
2496
    if (owner !== operatorId) throw new Error('Agent is not owned by this account');
2497
    const interaction = FederatedInteractionSchema.parse(row.payload);
2498
    const peerAgentId = String(isInitiator ? row.responder_agent_id : row.initiator_agent_id);
2499
    const peer = await this.sessionParticipant(peerAgentId);
2500
    const privateContext = await this.privateCounterpartyInsights(
2501
      String(owner),
2502
      peer,
2503
      interaction.contract.taskCategory,
2504
    );
2505
    const briefRows = await this.sql`
2506
      SELECT payload FROM openclasp_records
2507
      WHERE operator_id = ${String(owner)}
2508
        AND kind = 'counterparty_brief'
2509
        AND payload->>'interactionId' = ${interactionId}
2510
        AND payload->>'recipientAgentId' = ${agentId}
2511
      ORDER BY updated_at DESC
2512
      LIMIT 1
2513
    `;
2514
    const storedBrief = CounterpartyBriefSchema.safeParse(briefRows[0]?.payload);
2515
    const counterpartyBrief = storedBrief.success
2516
      ? storedBrief.data
2517
      : buildCounterpartyBrief({
2518
          interactionId,
2519
          contractHash: interaction.termsHash,
2520
          contract: interaction.contract,
2521
          recipientAgentId: agentId,
2522
          subject: peer.card,
2523
          historyInsights: privateContext.insights,
2524
          relevantSampleSize: privateContext.relevantSampleSize,
2525
          historyConfidence: privateContext.historyConfidence,
2526
          expiresAt: new Date(String(row.expires_at)).toISOString(),
2527
        });
2528
    const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
2529
    return {
2530
      type: 'openclasp.session.activation' as const,
2531
      version: '1' as const,
2532
      activationId: crypto.randomUUID(),
2533
      interactionId,
2534
      agentId,
2535
      sessionId: String(isInitiator ? row.initiator_session_id : row.responder_session_id),
2536
      role: isInitiator ? ('initiator' as const) : ('responder' as const),
2537
      peer: {
2538
        agentId: peerAgentId,
2539
        sessionId: String(isInitiator ? row.responder_session_id : row.initiator_session_id),
2540
        endpoint: String(isInitiator ? row.responder_endpoint : row.initiator_endpoint),
2541
        bearerToken,
2542
        verificationKey: getSessionVerificationKey(this.gatewaySecret()),
2543
      },
2544
      reporting: {
2545
        endpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/events`,
2546
        completionEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/completion-reports`,
2547
        feedbackEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/feedback`,
2548
        assuranceResponseEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/assurance-responses`,
2549
        bearerToken,
2550
      },
2551
      privateInsights: privateContext.insights,
2552
      counterpartyBrief,
2553
      contractHash: interaction.termsHash,
2554
      activatedAt: row.activated_at
2555
        ? new Date(String(row.activated_at)).toISOString()
2556
        : new Date().toISOString(),
2557
      expiresAt: new Date(String(row.expires_at)).toISOString(),
2558
    };
2559
  }
2560
2561
  private async interactionParticipant(operatorId: string, interactionId: string, agentId: string) {
2562
    await this.ensureSchema();
2563
    const rows = await this.sql`
2564
      SELECT payload, initiator_operator_id, responder_operator_id,
2565
        initiator_agent_id, responder_agent_id
2566
      FROM openclasp_federated_interactions
2567
      WHERE interaction_id = ${interactionId}
2568
        AND (
2569
          (initiator_operator_id = ${operatorId} AND initiator_agent_id = ${agentId})
2570
          OR (responder_operator_id = ${operatorId} AND responder_agent_id = ${agentId})
2571
        )
2572
      LIMIT 1
2573
    `;
2574
    const row = rows[0];
2575
    if (!row) throw new Error('Interaction participant not found for this account');
2576
    const interaction = FederatedInteractionSchema.parse(row.payload);
2577
    const isInitiator = interaction.initiatorAgentId === agentId;
2578
    return {
2579
      interaction,
2580
      counterpartyAgentId: isInitiator
2581
        ? interaction.responderAgentId
2582
        : interaction.initiatorAgentId,
2583
      participantOperatorId: String(
2584
        isInitiator ? row.initiator_operator_id : row.responder_operator_id,
2585
      ),
2586
      counterpartyOperatorId: String(
2587
        isInitiator ? row.responder_operator_id : row.initiator_operator_id,
2588
      ),
2589
    };
2590
  }
2591
2592
  async getCounterpartyBrief(operatorId: string, interactionId: string, agentId: string) {
2593
    await this.interactionParticipant(operatorId, interactionId, agentId);
2594
    const rows = await this.sql`
2595
      SELECT payload FROM openclasp_records
2596
      WHERE operator_id = ${operatorId}
2597
        AND kind = 'counterparty_brief'
2598
        AND payload->>'interactionId' = ${interactionId}
2599
        AND payload->>'recipientAgentId' = ${agentId}
2600
      ORDER BY updated_at DESC
2601
      LIMIT 1
2602
    `;
2603
    const parsed = CounterpartyBriefSchema.safeParse(rows[0]?.payload);
2604
    if (!parsed.success) throw new Error('Counterparty brief is not available');
2605
    return parsed.data;
2606
  }
2607
2608
  private async getAssuranceLearning(
2609
    operatorId: string,
2610
    targetAgentId: string,
2611
    targetAgentVersion: string,
2612
    taskCategory: string,
2613
  ) {
2614
    const rows = await this.sql`
2615
      SELECT payload FROM openclasp_assurance_evaluations
2616
      WHERE operator_id = ${operatorId}
2617
        AND target_agent_id = ${targetAgentId}
2618
        AND target_agent_version = ${targetAgentVersion}
2619
        AND task_category = ${taskCategory}
2620
      ORDER BY created_at DESC
2621
      LIMIT 500
2622
    `;
2623
    const evaluations = rows
2624
      .map((row) => AssuranceEffectivenessEvaluationSchema.safeParse(row.payload))
2625
      .filter((parsed) => parsed.success)
2626
      .map((parsed) => parsed.data);
2627
    return summarizeAssuranceLearning(evaluations);
2628
  }
2629
2630
  async getAssuranceProbeContext(
2631
    operatorId: string,
2632
    interactionId: string,
2633
    generatedForAgentId: string,
2634
    targetAgentId: string,
2635
  ) {
2636
    const participant = await this.interactionParticipant(
2637
      operatorId,
2638
      interactionId,
2639
      generatedForAgentId,
2640
    );
2641
    if (participant.counterpartyAgentId !== targetAgentId)
2642
      throw new Error('Assurance probe target must be the authenticated agent’s counterparty');
2643
    if (participant.interaction.status !== 'active')
2644
      throw new Error('Assurance probes require an active interaction');
2645
    const targetCard = await this.getPublishedAgent(targetAgentId);
2646
    if (!targetCard) throw new Error('Assurance probe target is no longer published');
2647
    const brief = await this.getCounterpartyBrief(
2648
      operatorId,
2649
      interactionId,
2650
      generatedForAgentId,
2651
    ).catch(() => undefined);
2652
    const reportRows = await this.sql`
2653
      SELECT payload FROM openclasp_records
2654
      WHERE operator_id = ${operatorId}
2655
        AND kind = 'completion_report'
2656
        AND payload->>'interactionId' = ${interactionId}
2657
      ORDER BY updated_at DESC
2658
    `;
2659
    const completionReports = reportRows
2660
      .map((row) => InteractionCompletionReportSchema.safeParse(row.payload))
2661
      .filter((parsed) => parsed.success)
2662
      .map((parsed) => parsed.data);
2663
    const eventRows = await this.sql`
2664
      SELECT event FROM openclasp_live_session_events
2665
      WHERE interaction_id = ${interactionId}
2666
      ORDER BY created_at ASC
2667
      LIMIT 100
2668
    `;
2669
    const sessionEvents = eventRows
2670
      .map((row) => LiveSessionEventSchema.safeParse(row.event))
2671
      .filter((parsed) => parsed.success)
2672
      .map((parsed) => parsed.data);
2673
    const planRows = await this.sql`
2674
      SELECT payload FROM openclasp_assurance_probe_plans
2675
      WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
2676
        AND generated_for_agent_id = ${generatedForAgentId} AND target_agent_id = ${targetAgentId}
2677
      ORDER BY created_at ASC
2678
    `;
2679
    const previousPlans = planRows.map((row) => AssuranceProbePlanSchema.parse(row.payload));
2680
    const responseRows = await this.sql`
2681
      SELECT responses.payload FROM openclasp_assurance_probe_responses responses
2682
      INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id
2683
      WHERE plans.operator_id = ${operatorId} AND responses.interaction_id = ${interactionId}
2684
        AND responses.agent_id = ${targetAgentId}
2685
      ORDER BY responses.created_at ASC
2686
    `;
2687
    const previousResponses = responseRows.map((row) =>
2688
      AssuranceProbeResponseSchema.parse(row.payload),
2689
    );
2690
    const predictionRows = await this.sql`
2691
      SELECT payload FROM openclasp_assurance_predictions
2692
      WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
2693
        AND target_agent_id = ${targetAgentId}
2694
      ORDER BY created_at ASC
2695
    `;
2696
    const previousPredictions = predictionRows.map((row) =>
2697
      AssurancePredictionSnapshotSchema.parse(row.payload),
2698
    );
2699
    const assuranceLearning = await this.getAssuranceLearning(
2700
      operatorId,
2701
      targetAgentId,
2702
      targetCard.agentVersion,
2703
      participant.interaction.contract.taskCategory,
2704
    );
2705
    return {
2706
      interaction: participant.interaction,
2707
      targetCard,
2708
      brief,
2709
      completionReports,
2710
      sessionEvents,
2711
      previousPlans,
2712
      previousResponses,
2713
      previousPredictions,
2714
      assuranceLearning,
2715
    };
2716
  }
2717
2718
  async beginAssuranceGeneration(record: {
2719
    generationId: string;
2720
    operatorId: string;
2721
    interactionId: string;
2722
    phase: 'pre_task' | 'post_task';
2723
    model: string;
2724
    promptVersion: string;
2725
    input: Record<string, unknown>;
2726
  }) {
2727
    await this.ensureSchema();
2728
    const participants = await this.sql`
2729
      SELECT 1 FROM openclasp_federated_interactions
2730
      WHERE interaction_id = ${record.interactionId}
2731
        AND ${record.operatorId} IN (initiator_operator_id, responder_operator_id)
2732
      LIMIT 1
2733
    `;
2734
    if (!participants.length) throw new Error('Assurance generation interaction is not accessible');
2735
    await this.sql`
2736
      INSERT INTO openclasp_ai_generations(
2737
        generation_id, operator_id, interaction_id, phase, model, prompt_version,
2738
        status, input, input_digest
2739
      ) VALUES (
2740
        ${record.generationId}, ${record.operatorId}, ${record.interactionId}, ${record.phase},
2741
        ${record.model}, ${record.promptVersion}, 'pending', ${JSON.stringify(record.input)}::jsonb,
2742
        ${canonicalHash(record.input)}
2743
      )
2744
    `;
2745
  }
2746
2747
  async finishAssuranceGeneration(
2748
    operatorId: string,
2749
    generationId: string,
2750
    value: {
2751
      status: 'complete' | 'fallback' | 'error';
2752
      output: AssuranceDecision;
2753
      tokenUsage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number };
2754
      errorCode?: string;
2755
    },
2756
  ) {
2757
    const rows = await this.sql`
2758
      UPDATE openclasp_ai_generations
2759
      SET status = ${value.status}, output = ${JSON.stringify(value.output)}::jsonb,
2760
        token_usage = ${JSON.stringify(value.tokenUsage ?? {})}::jsonb,
2761
        error_code = ${value.errorCode ?? null}, completed_at = NOW()
2762
      WHERE generation_id = ${generationId} AND operator_id = ${operatorId} AND status = 'pending'
2763
      RETURNING generation_id
2764
    `;
2765
    if (!rows.length) throw new Error('Pending assurance generation not found');
2766
  }
2767
2768
  async saveAssuranceProbePlan(operatorId: string, value: AssuranceProbePlan) {
2769
    const plan = AssuranceProbePlanSchema.parse(value);
2770
    const participant = await this.interactionParticipant(
2771
      operatorId,
2772
      plan.interactionId,
2773
      plan.generatedForAgentId,
2774
    );
2775
    if (participant.counterpartyAgentId !== plan.targetAgentId)
2776
      throw new Error('Assurance probe target does not match the interaction counterparty');
2777
    if (participant.interaction.termsHash !== plan.contractHash)
2778
      throw new Error('Assurance probe contract hash is stale');
2779
    const rows = await this.sql`
2780
      INSERT INTO openclasp_assurance_probe_plans(
2781
        plan_id, generation_id, operator_id, interaction_id, contract_hash, phase,
2782
        generated_for_agent_id, target_agent_id, payload, expires_at
2783
      ) VALUES (
2784
        ${plan.planId}, ${plan.generation.generationId}, ${operatorId}, ${plan.interactionId},
2785
        ${plan.contractHash}, ${plan.phase}, ${plan.generatedForAgentId}, ${plan.targetAgentId},
2786
        ${JSON.stringify(plan)}::jsonb, ${plan.expiresAt}
2787
      )
2788
      ON CONFLICT (plan_id) DO NOTHING
2789
      RETURNING payload
2790
    `;
2791
    if (!rows.length) throw new Error('Assurance probe plan already exists');
2792
    return AssuranceProbePlanSchema.parse(rows[0]!.payload);
2793
  }
2794
2795
  async saveAssuranceDecision(
2796
    operatorId: string,
2797
    decisionValue: AssuranceDecision,
2798
    planValue: AssuranceProbePlan,
2799
  ) {
2800
    const decision = AssuranceDecisionSchema.parse(decisionValue);
2801
    const plan = AssuranceProbePlanSchema.parse(planValue);
2802
    if (
2803
      decision.assessmentId !== plan.assessmentId ||
2804
      decision.prediction.predictionId !== plan.predictionBeforeId ||
2805
      decision.generation.generationId !== plan.generation.generationId
2806
    )
2807
      throw new Error('Assurance decision and probe plan are not linked');
2808
    await this.interactionParticipant(
2809
      operatorId,
2810
      decision.interactionId,
2811
      decision.generatedForAgentId,
2812
    );
2813
    await this.sql`
2814
      INSERT INTO openclasp_assurance_assessments(
2815
        assessment_id, generation_id, operator_id, interaction_id, phase, round,
2816
        target_agent_id, target_agent_version, payload
2817
      ) VALUES (
2818
        ${decision.assessmentId}, ${decision.generation.generationId}, ${operatorId},
2819
        ${decision.interactionId}, ${decision.phase}, ${decision.round}, ${decision.targetAgentId},
2820
        ${decision.targetAgentVersion}, ${JSON.stringify(decision)}::jsonb
2821
      )
2822
    `;
2823
    await this.sql`
2824
      INSERT INTO openclasp_assurance_predictions(
2825
        prediction_id, assessment_id, operator_id, interaction_id, target_agent_id,
2826
        target_agent_version, task_category, stage, success_probability, payload
2827
      ) VALUES (
2828
        ${decision.prediction.predictionId}, ${decision.assessmentId}, ${operatorId},
2829
        ${decision.interactionId}, ${decision.targetAgentId}, ${decision.targetAgentVersion},
2830
        ${decision.prediction.taskCategory}, ${decision.prediction.stage},
2831
        ${decision.prediction.successProbability}, ${JSON.stringify(decision.prediction)}::jsonb
2832
      )
2833
    `;
2834
    for (const safeguard of decision.safeguards) {
2835
      await this.sql`
2836
        INSERT INTO openclasp_assurance_safeguards(
2837
          safeguard_id, assessment_id, operator_id, interaction_id, target_agent_id, status, payload
2838
        ) VALUES (
2839
          ${safeguard.safeguardId}, ${decision.assessmentId}, ${operatorId},
2840
          ${decision.interactionId}, ${decision.targetAgentId}, ${safeguard.status},
2841
          ${JSON.stringify(safeguard)}::jsonb
2842
        )
2843
      `;
2844
    }
2845
    const savedPlan = await this.saveAssuranceProbePlan(operatorId, plan);
2846
    return { decision, plan: savedPlan };
2847
  }
2848
2849
  async listAssuranceProbePlans(operatorId: string, interactionId: string, agentId: string) {
2850
    await this.interactionParticipant(operatorId, interactionId, agentId);
2851
    const rows = await this.sql`
2852
      SELECT payload FROM openclasp_assurance_probe_plans
2853
      WHERE interaction_id = ${interactionId}
2854
        AND (generated_for_agent_id = ${agentId} OR target_agent_id = ${agentId})
2855
      ORDER BY created_at DESC
2856
    `;
2857
    return rows.map((row) => AssuranceProbePlanSchema.parse(row.payload));
2858
  }
2859
2860
  async getAssuranceBrief(operatorId: string, interactionId: string, agentId: string) {
2861
    await this.interactionParticipant(operatorId, interactionId, agentId);
2862
    const [assessments, predictions, safeguards, evaluations, plans, responses] = await Promise.all(
2863
      [
2864
        this
2865
          .sql`SELECT payload FROM openclasp_assurance_assessments WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2866
        this
2867
          .sql`SELECT payload FROM openclasp_assurance_predictions WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2868
        this
2869
          .sql`SELECT payload FROM openclasp_assurance_safeguards WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2870
        this
2871
          .sql`SELECT payload FROM openclasp_assurance_evaluations WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2872
        this
2873
          .sql`SELECT payload FROM openclasp_assurance_probe_plans WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2874
        this
2875
          .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`,
2876
      ],
2877
    );
2878
    const parsedAssessments = assessments.map((row) => AssuranceDecisionSchema.parse(row.payload));
2879
    const latestAssessment = parsedAssessments.at(-1);
2880
    const assuranceLearning = latestAssessment
2881
      ? await this.getAssuranceLearning(
2882
          operatorId,
2883
          latestAssessment.targetAgentId,
2884
          latestAssessment.targetAgentVersion,
2885
          latestAssessment.prediction.taskCategory,
2886
        )
2887
      : summarizeAssuranceLearning([]);
2888
    return {
2889
      assessments: parsedAssessments,
2890
      predictions: predictions.map((row) => AssurancePredictionSnapshotSchema.parse(row.payload)),
2891
      safeguards: safeguards.map((row) => AssuranceSafeguardSchema.parse(row.payload)),
2892
      evaluations: evaluations.map((row) =>
2893
        AssuranceEffectivenessEvaluationSchema.parse(row.payload),
2894
      ),
2895
      plans: plans.map((row) => AssuranceProbePlanSchema.parse(row.payload)),
2896
      responses: responses.map((row) => AssuranceProbeResponseSchema.parse(row.payload)),
2897
      assuranceLearning,
2898
      advisoryNotice: 'experimental_estimate_not_a_guarantee' as const,
2899
    };
2900
  }
2901
2902
  private async predictionAfterProbe(
2903
    operatorId: string,
2904
    plan: AssuranceProbePlan,
2905
    response: AssuranceProbeResponse,
2906
  ) {
2907
    const rows = await this.sql`
2908
      SELECT payload FROM openclasp_assurance_predictions
2909
      WHERE operator_id = ${operatorId} AND interaction_id = ${plan.interactionId}
2910
        AND target_agent_id = ${plan.targetAgentId}
2911
      ORDER BY created_at DESC LIMIT 1
2912
    `;
2913
    const prior = AssurancePredictionSnapshotSchema.safeParse(rows[0]?.payload);
2914
    if (!prior.success) return undefined;
2915
    const question = plan.questions[0];
2916
    const answer = response.answers.find((candidate) => candidate.probeId === question?.probeId);
2917
    if (!question || !answer) return undefined;
2918
    const normalized = String(answer.answer).trim().toLowerCase();
2919
    const signal = question.expectedSignals.find(
2920
      (candidate) => candidate.answer.trim().toLowerCase() === normalized,
2921
    );
2922
    const delta = signal?.probabilityDelta ?? 0;
2923
    const prediction = AssurancePredictionSnapshotSchema.parse({
2924
      ...prior.data,
2925
      predictionId: crypto.randomUUID(),
2926
      stage: 'after_probe',
2927
      successProbability: Math.max(0.05, Math.min(0.95, prior.data.successProbability + delta)),
2928
      confidence: Math.min(0.9, prior.data.confidence + 0.05 * answer.confidence),
2929
      priorPredictionId: prior.data.predictionId,
2930
      triggerResponseId: response.responseId,
2931
      createdAt: new Date().toISOString(),
2932
    });
2933
    await this.sql`
2934
      INSERT INTO openclasp_assurance_predictions(
2935
        prediction_id, assessment_id, operator_id, interaction_id, target_agent_id,
2936
        target_agent_version, task_category, stage, success_probability, payload
2937
      ) VALUES (
2938
        ${prediction.predictionId}, ${plan.assessmentId}, ${operatorId}, ${prediction.interactionId},
2939
        ${prediction.targetAgentId}, ${prediction.targetAgentVersion}, ${prediction.taskCategory},
2940
        ${prediction.stage}, ${prediction.successProbability}, ${JSON.stringify(prediction)}::jsonb
2941
      )
2942
    `;
2943
    return prediction;
2944
  }
2945
2946
  async decideAssuranceSafeguard(
2947
    operatorId: string,
2948
    interactionId: string,
2949
    agentId: string,
2950
    safeguardId: string,
2951
    status: 'accepted' | 'rejected' | 'modified',
2952
    decisionReason?: string,
2953
  ) {
2954
    await this.interactionParticipant(operatorId, interactionId, agentId);
2955
    const rows = await this.sql`
2956
      SELECT safeguard.payload, safeguard.assessment_id, assessment.payload AS assessment
2957
      FROM openclasp_assurance_safeguards safeguard
2958
      INNER JOIN openclasp_assurance_assessments assessment
2959
        ON assessment.assessment_id = safeguard.assessment_id
2960
      WHERE safeguard.operator_id = ${operatorId} AND safeguard.interaction_id = ${interactionId}
2961
        AND safeguard.safeguard_id = ${safeguardId}
2962
      LIMIT 1
2963
    `;
2964
    const row = rows[0];
2965
    if (!row) throw new Error('Assurance safeguard not found');
2966
    const assessment = AssuranceDecisionSchema.parse(row.assessment);
2967
    if (assessment.generatedForAgentId !== agentId)
2968
      throw new Error('Only the agent that requested the assessment may decide its safeguard');
2969
    const current = AssuranceSafeguardSchema.parse(row.payload);
2970
    if (current.status !== 'recommended')
2971
      throw new Error('Assurance safeguard was already decided');
2972
    const safeguard = AssuranceSafeguardSchema.parse({
2973
      ...current,
2974
      status,
2975
      ...(decisionReason ? { decisionReason } : {}),
2976
      decidedAt: new Date().toISOString(),
2977
    });
2978
    await this.sql`
2979
      UPDATE openclasp_assurance_safeguards
2980
      SET status = ${status}, payload = ${JSON.stringify(safeguard)}::jsonb, updated_at = NOW()
2981
      WHERE safeguard_id = ${safeguardId} AND operator_id = ${operatorId}
2982
    `;
2983
    let prediction: AssurancePredictionSnapshot | undefined;
2984
    if (status !== 'rejected') {
2985
      const priorRows = await this.sql`
2986
        SELECT payload FROM openclasp_assurance_predictions
2987
        WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
2988
          AND target_agent_id = ${safeguard.targetAgentId}
2989
        ORDER BY created_at DESC LIMIT 1
2990
      `;
2991
      const prior = AssurancePredictionSnapshotSchema.safeParse(priorRows[0]?.payload);
2992
      if (prior.success) {
2993
        prediction = AssurancePredictionSnapshotSchema.parse({
2994
          ...prior.data,
2995
          predictionId: crypto.randomUUID(),
2996
          stage: 'after_safeguard',
2997
          successProbability: Math.min(
2998
            0.95,
2999
            prior.data.successProbability + Math.min(0.15, safeguard.expectedImpact),
3000
          ),
3001
          confidence: Math.min(0.9, prior.data.confidence + 0.03),
3002
          priorPredictionId: prior.data.predictionId,
3003
          createdAt: new Date().toISOString(),
3004
        });
3005
        await this.sql`
3006
          INSERT INTO openclasp_assurance_predictions(
3007
            prediction_id, assessment_id, operator_id, interaction_id, target_agent_id,
3008
            target_agent_version, task_category, stage, success_probability, payload
3009
          ) VALUES (
3010
            ${prediction.predictionId}, ${String(row.assessment_id)}, ${operatorId},
3011
            ${prediction.interactionId}, ${prediction.targetAgentId}, ${prediction.targetAgentVersion},
3012
            ${prediction.taskCategory}, ${prediction.stage}, ${prediction.successProbability},
3013
            ${JSON.stringify(prediction)}::jsonb
3014
          )
3015
        `;
3016
      }
3017
    }
3018
    return { safeguard, prediction, contractRevisionRequired: status !== 'rejected' };
3019
  }
3020
3021
  private async recalculateAssuranceComparison(
3022
    operatorId: string,
3023
    interactionId: string,
3024
    targetAgentId: string,
3025
  ): Promise<AssuranceClaimOutcomeComparison | undefined> {
3026
    const responseRows = await this.sql`
3027
      SELECT responses.response_id, responses.phase, responses.payload
3028
      FROM openclasp_assurance_probe_responses responses
3029
      INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id
3030
      WHERE responses.interaction_id = ${interactionId}
3031
        AND responses.agent_id = ${targetAgentId}
3032
        AND plans.operator_id = ${operatorId}
3033
      ORDER BY responses.created_at DESC
3034
    `;
3035
    const preRow = responseRows.find((row) => row.phase === 'pre_task');
3036
    if (!preRow) return undefined;
3037
    const pre = AssuranceProbeResponseSchema.parse(preRow.payload);
3038
    const postRow = responseRows.find((row) => row.phase === 'post_task');
3039
    const post = postRow ? AssuranceProbeResponseSchema.parse(postRow.payload) : undefined;
3040
    const reportRows = await this.sql`
3041
      SELECT payload FROM openclasp_records
3042
      WHERE operator_id = ${operatorId}
3043
        AND kind = 'completion_report'
3044
        AND payload->>'interactionId' = ${interactionId}
3045
        AND payload->>'reportingAgentId' = ${targetAgentId}
3046
      ORDER BY updated_at DESC
3047
      LIMIT 1
3048
    `;
3049
    const report = InteractionCompletionReportSchema.safeParse(reportRows[0]?.payload);
3050
    if (!report.success) return undefined;
3051
    const comparison = AssuranceClaimOutcomeComparisonSchema.parse({
3052
      protocolVersion: '0.1',
3053
      comparisonId: deterministicUuid(`assurance-comparison:${operatorId}:${pre.responseId}`),
3054
      interactionId,
3055
      contractHash: pre.contractHash,
3056
      targetAgentId,
3057
      preTaskResponseId: pre.responseId,
3058
      ...(post ? { postTaskResponseId: post.responseId } : {}),
3059
      completionReportIds: [report.data.reportId],
3060
      comparisons: pre.answers.map((answer) => assuranceClaimComparison(answer, report.data)),
3061
      calculatedAt: new Date().toISOString(),
3062
    });
3063
    await this.sql`
3064
      INSERT INTO openclasp_assurance_claim_comparisons(
3065
        comparison_id, operator_id, interaction_id, target_agent_id,
3066
        pre_task_response_id, post_task_response_id, payload
3067
      ) VALUES (
3068
        ${comparison.comparisonId}, ${operatorId}, ${interactionId}, ${targetAgentId},
3069
        ${comparison.preTaskResponseId}, ${comparison.postTaskResponseId ?? null},
3070
        ${JSON.stringify(comparison)}::jsonb
3071
      )
3072
      ON CONFLICT (operator_id, interaction_id, target_agent_id, pre_task_response_id)
3073
      DO UPDATE SET post_task_response_id = EXCLUDED.post_task_response_id,
3074
        payload = EXCLUDED.payload, created_at = NOW()
3075
    `;
3076
    return comparison;
3077
  }
3078
3079
  async submitAssuranceProbeResponse(
3080
    operatorId: string,
3081
    agentId: string,
3082
    value: AssuranceProbeResponse,
3083
  ) {
3084
    const response = AssuranceProbeResponseSchema.parse(value);
3085
    const participant = await this.interactionParticipant(
3086
      operatorId,
3087
      response.interactionId,
3088
      agentId,
3089
    );
3090
    if (response.agentId !== agentId)
3091
      throw new Error('Assurance response identity does not match the authenticated agent');
3092
    if (participant.interaction.termsHash !== response.contractHash)
3093
      throw new Error('Assurance response contract hash is stale');
3094
    const planRows = await this.sql`
3095
      SELECT operator_id, target_agent_id, phase, payload, expires_at
3096
      FROM openclasp_assurance_probe_plans
3097
      WHERE plan_id = ${response.planId} AND interaction_id = ${response.interactionId}
3098
      LIMIT 1
3099
    `;
3100
    const row = planRows[0];
3101
    if (!row) throw new Error('Assurance probe plan not found');
3102
    const plan = AssuranceProbePlanSchema.parse(row.payload);
3103
    if (row.target_agent_id !== agentId || response.phase !== row.phase)
3104
      throw new Error('Assurance response does not match the targeted probe plan');
3105
    if (response.agentVersion !== plan.targetAgentVersion)
3106
      throw new Error('Assurance response agent version does not match the probe plan');
3107
    if (Date.parse(String(row.expires_at)) <= Date.now())
3108
      throw new Error('Assurance probe plan expired');
3109
    const answerIds = new Set(response.answers.map((answer) => answer.probeId));
3110
    for (const question of plan.questions) {
3111
      const answer = response.answers.find((candidate) => candidate.probeId === question.probeId);
3112
      if (question.required && !answer)
3113
        throw new Error(`Required assurance probe ${question.questionCode} is unanswered`);
3114
      if (!answer) continue;
3115
      if (
3116
        answer.questionCode !== question.questionCode ||
3117
        answer.responseType !== question.responseType ||
3118
        (question.responseType === 'enum' && !question.choices?.includes(String(answer.answer)))
3119
      )
3120
        throw new Error(`Assurance answer does not match probe ${question.questionCode}`);
3121
    }
3122
    if (
3123
      answerIds.size !== response.answers.length ||
3124
      response.answers.some(
3125
        (answer) => !plan.questions.some((question) => question.probeId === answer.probeId),
3126
      )
3127
    )
3128
      throw new Error('Assurance response contains duplicate or unknown probes');
3129
    const rows = await this.sql`
3130
      INSERT INTO openclasp_assurance_probe_responses(
3131
        response_id, plan_id, operator_id, interaction_id, contract_hash, phase, agent_id, payload
3132
      ) VALUES (
3133
        ${response.responseId}, ${response.planId}, ${operatorId}, ${response.interactionId},
3134
        ${response.contractHash}, ${response.phase}, ${agentId}, ${JSON.stringify(response)}::jsonb
3135
      )
3136
      ON CONFLICT (plan_id, agent_id) DO NOTHING
3137
      RETURNING payload
3138
    `;
3139
    if (!rows.length) throw new Error('This agent already answered the assurance probe plan');
3140
    const storedResponse = AssuranceProbeResponseSchema.parse(rows[0]!.payload);
3141
    const prediction = await this.predictionAfterProbe(
3142
      String(row.operator_id),
3143
      plan,
3144
      storedResponse,
3145
    );
3146
    const comparison = await this.recalculateAssuranceComparison(
3147
      String(row.operator_id),
3148
      response.interactionId,
3149
      agentId,
3150
    );
3151
    return { response: storedResponse, prediction, comparison };
3152
  }
3153
3154
  async recordSessionAssuranceResponse(token: string, value: AssuranceProbeResponse) {
3155
    const response = AssuranceProbeResponseSchema.parse(value);
3156
    const grant = this.verifySessionGrant(token);
3157
    if (grant.interactionId !== response.interactionId || grant.senderAgentId !== response.agentId)
3158
      throw new Error('Session credential does not match the assurance response');
3159
    const owners = await this.sql`
3160
      SELECT initiator_operator_id, responder_operator_id, initiator_agent_id
3161
      FROM openclasp_federated_interactions
3162
      WHERE interaction_id = ${response.interactionId}
3163
      LIMIT 1
3164
    `;
3165
    const row = owners[0];
3166
    if (!row) throw new Error('Interaction not found');
3167
    const operatorId = String(
3168
      row.initiator_agent_id === response.agentId
3169
        ? row.initiator_operator_id
3170
        : row.responder_operator_id,
3171
    );
3172
    return this.submitAssuranceProbeResponse(operatorId, response.agentId, response);
3173
  }
3174
3175
  async listAssuranceComparisons(operatorId: string, interactionId: string, agentId: string) {
3176
    await this.interactionParticipant(operatorId, interactionId, agentId);
3177
    const rows = await this.sql`
3178
      SELECT payload FROM openclasp_assurance_claim_comparisons
3179
      WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
3180
      ORDER BY created_at DESC
3181
    `;
3182
    return rows.map((row) => AssuranceClaimOutcomeComparisonSchema.parse(row.payload));
3183
  }
3184
3185
  private async evaluateAssuranceOutcome(
3186
    operatorId: string,
3187
    report: InteractionCompletionReport,
3188
  ): Promise<AssuranceEffectivenessEvaluation | undefined> {
3189
    const [predictionRows, planRows, responseRows, safeguardRows] = await Promise.all([
3190
      this
3191
        .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`,
3192
      this
3193
        .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`,
3194
      this
3195
        .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`,
3196
      this
3197
        .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`,
3198
    ]);
3199
    if (!predictionRows.length && !planRows.length) return undefined;
3200
    const predictions = predictionRows.map((row) =>
3201
      AssurancePredictionSnapshotSchema.parse(row.payload),
3202
    );
3203
    const plans = planRows.map((row) => AssuranceProbePlanSchema.parse(row.payload));
3204
    const responses = responseRows.map((row) => AssuranceProbeResponseSchema.parse(row.payload));
3205
    const safeguards = safeguardRows.map((row) => AssuranceSafeguardSchema.parse(row.payload));
3206
    const outcomeValue = report.outcome === 'success' ? 1 : report.outcome === 'partial' ? 0.5 : 0;
3207
    const evaluation = AssuranceEffectivenessEvaluationSchema.parse({
3208
      protocolVersion: '0.1',
3209
      evaluationId: deterministicUuid(`assurance-evaluation:${operatorId}:${report.reportId}`),
3210
      interactionId: report.interactionId,
3211
      contractHash: report.contractHash,
3212
      targetAgentId: report.reportingAgentId,
3213
      targetAgentVersion: report.agentVersion,
3214
      taskCategory: predictions[0]?.taskCategory ?? 'general',
3215
      completionReportId: report.reportId,
3216
      outcomeValue,
3217
      predictionScores: predictions.map((prediction) => ({
3218
        predictionId: prediction.predictionId,
3219
        stage: prediction.stage,
3220
        probability: prediction.successProbability,
3221
        brierScore: (prediction.successProbability - outcomeValue) ** 2,
3222
      })),
3223
      questionScores: plans.flatMap((plan) =>
3224
        plan.questions.map((question) => {
3225
          const response = responses.find((candidate) => candidate.planId === plan.planId);
3226
          const answer = response?.answers.find(
3227
            (candidate) => candidate.probeId === question.probeId,
3228
          );
3229
          const signal = answer
3230
            ? question.expectedSignals.find(
3231
                (candidate) =>
3232
                  candidate.answer.toLowerCase() === String(answer.answer).toLowerCase(),
3233
              )
3234
            : undefined;
3235
          const before = predictions.find(
3236
            (prediction) => prediction.predictionId === plan.predictionBeforeId,
3237
          );
3238
          const after = response
3239
            ? predictions.find((prediction) => prediction.triggerResponseId === response.responseId)
3240
            : undefined;
3241
          return {
3242
            probeId: question.probeId,
3243
            questionCode: question.questionCode,
3244
            questionFamily: question.questionFamily,
3245
            answered: Boolean(answer),
3246
            exposedMaterialRisk:
3247
              signal?.effect === 'reduce_success' ||
3248
              Boolean(answer?.limitations.length) ||
3249
              report.blockers.length > 0,
3250
            predictionDelta:
3251
              before && after ? after.successProbability - before.successProbability : 0,
3252
          };
3253
        }),
3254
      ),
3255
      safeguardScores: safeguards.map((safeguard) => ({
3256
        safeguardId: safeguard.safeguardId,
3257
        type: safeguard.type,
3258
        status: safeguard.status,
3259
        outcomeAssociation:
3260
          safeguard.status === 'accepted' || safeguard.status === 'modified'
3261
            ? outcomeValue >= 0.75
3262
              ? 'positive'
3263
              : outcomeValue <= 0.25
3264
                ? 'negative'
3265
                : 'unclear'
3266
            : 'unclear',
3267
        causalClaim: false,
3268
      })),
3269
      evaluatedAt: new Date().toISOString(),
3270
    });
3271
    await this.sql`
3272
      INSERT INTO openclasp_assurance_evaluations(
3273
        evaluation_id, operator_id, interaction_id, target_agent_id, target_agent_version,
3274
        task_category, completion_report_id, payload
3275
      ) VALUES (
3276
        ${evaluation.evaluationId}, ${operatorId}, ${evaluation.interactionId},
3277
        ${evaluation.targetAgentId}, ${evaluation.targetAgentVersion}, ${evaluation.taskCategory},
3278
        ${evaluation.completionReportId}, ${JSON.stringify(evaluation)}::jsonb
3279
      )
3280
      ON CONFLICT (operator_id, completion_report_id)
3281
      DO UPDATE SET payload = EXCLUDED.payload, created_at = NOW()
3282
    `;
3283
    return evaluation;
3284
  }
3285
3286
  private async ensureFeedbackRequest(
3287
    operatorId: string,
3288
    interactionId: string,
3289
    reviewerAgentId: string,
3290
    subjectAgentId: string,
3291
  ) {
3292
    const existing = await this.sql`
3293
      SELECT payload FROM openclasp_records
3294
      WHERE operator_id = ${operatorId}
3295
        AND kind = 'feedback_request'
3296
        AND payload->>'interactionId' = ${interactionId}
3297
        AND payload->>'reviewerAgentId' = ${reviewerAgentId}
3298
      ORDER BY updated_at DESC
3299
      LIMIT 1
3300
    `;
3301
    const parsed = FeedbackRequestSchema.safeParse(existing[0]?.payload);
3302
    if (parsed.success) return parsed.data;
3303
    const requestedAt = new Date();
3304
    const base = FeedbackRequestSchema.parse({
3305
      requestId: deterministicUuid(`feedback-request:${interactionId}:${reviewerAgentId}`),
3306
      interactionId,
3307
      reviewerAgentId,
3308
      subjectAgentId,
3309
      status: 'pending',
3310
      requestedDimensions: FEEDBACK_DIMENSIONS,
3311
      requestedAt: requestedAt.toISOString(),
3312
      dueAt: new Date(requestedAt.getTime() + feedbackWindowMilliseconds()).toISOString(),
3313
    });
3314
    const request = FeedbackRequestSchema.parse({
3315
      ...base,
3316
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
3317
    });
3318
    await this.upsert(operatorId, 'feedback_request', request.requestId, request);
3319
    return request;
3320
  }
3321
3322
  private async requestRuntimeFinalization(
3323
    interactionId: string,
3324
    reportingAgentId: string,
3325
    counterpartyAgentId: string,
3326
    contractHash: string,
3327
  ) {
3328
    const rows = await this.sql`
3329
      SELECT endpoint FROM openclasp_agent_runtimes
3330
      WHERE agent_id = ${counterpartyAgentId} AND status = 'verified'
3331
      LIMIT 1
3332
    `;
3333
    if (!rows[0]?.endpoint) return false;
3334
    const requestId = crypto.randomUUID();
3335
    const value = {
3336
      type: 'openclasp.session.finalization_request',
3337
      version: '1',
3338
      requestId,
3339
      interactionId,
3340
      agentId: counterpartyAgentId,
3341
      peerAgentId: reportingAgentId,
3342
      contractHash,
3343
      requestedAt: new Date().toISOString(),
3344
    };
3345
    const response = await this.signedRuntimeRequest(
3346
      { callbackEndpoint: String(rows[0].endpoint) },
3347
      requestId,
3348
      value,
3349
    );
3350
    return response.status >= 200 && response.status < 300;
3351
  }
3352
3353
  async listFeedbackRequests(operatorId: string, agentId: string) {
3354
    const rows = await this.sql`
3355
      SELECT payload FROM openclasp_records
3356
      WHERE operator_id = ${operatorId}
3357
        AND kind = 'feedback_request'
3358
        AND payload->>'reviewerAgentId' = ${agentId}
3359
      ORDER BY updated_at DESC
3360
    `;
3361
    return rows.map((row) => FeedbackRequestSchema.parse(row.payload));
3362
  }
3363
3364
  async submitCompletionReport(
3365
    operatorId: string,
3366
    agentId: string,
3367
    value: InteractionCompletionReport,
3368
    submissionMethod:
3369
      'oauth_account' | 'oauth_installation' | 'agent_access_token' | 'runtime_session',
3370
  ) {
3371
    const participant = await this.interactionParticipant(operatorId, value.interactionId, agentId);
3372
    const report = InteractionCompletionReportSchema.parse(value);
3373
    if (report.platformAttestation)
3374
      throw new Error('Platform attestation is assigned by OpenClasp');
3375
    if (report.reportingAgentId !== agentId)
3376
      throw new Error('Completion report identity does not match the authenticated agent');
3377
    if (report.counterpartyAgentId !== participant.counterpartyAgentId)
3378
      throw new Error('Completion report counterparty does not match the interaction');
3379
    if (report.contractHash !== participant.interaction.termsHash)
3380
      throw new Error('Completion report contract hash does not match the interaction');
3381
    if (report.requestedOutcome !== participant.interaction.contract.requestedOutcome)
3382
      throw new Error('Completion report requested outcome does not match the contract');
3383
    if (
3384
      report.criteria.some(
3385
        (criterion) =>
3386
          !participant.interaction.contract.successCriteria.includes(criterion.criterion),
3387
      )
3388
    )
3389
      throw new Error('Completion report contains a criterion outside the contract');
3390
    if (!['active', 'completed'].includes(participant.interaction.status))
3391
      throw new Error('Only active or completed interactions accept completion reports');
3392
    const profiles = await this.sql`
3393
      SELECT payload FROM openclasp_records
3394
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
3395
      LIMIT 1
3396
    `;
3397
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
3398
    if (!profile || profile.agentVersion !== report.agentVersion)
3399
      throw new Error('Completion report agent version does not match the registered agent');
3400
    let verifiedSubmissionMethod: InteractionCompletionReport['submissionMethod'] =
3401
      submissionMethod;
3402
    if (report.signature) {
3403
      if (report.submissionMethod !== 'agent_signature')
3404
        throw new Error('Signed completion reports must declare agent_signature submission');
3405
      const identities = await this.sql`
3406
        SELECT payload FROM openclasp_records
3407
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
3408
        LIMIT 1
3409
      `;
3410
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
3411
      if (
3412
        !identity.success ||
3413
        !verifyObject(report as unknown as Record<string, unknown>, identity.data.publicKey)
3414
      )
3415
        throw new Error('Completion report agent signature is invalid or unverifiable');
3416
      verifiedSubmissionMethod = 'agent_signature';
3417
    }
3418
    const unsigned = InteractionCompletionReportSchema.parse({
3419
      ...report,
3420
      submissionMethod: verifiedSubmissionMethod,
3421
    });
3422
    const stored = InteractionCompletionReportSchema.parse({
3423
      ...unsigned,
3424
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unsigned),
3425
    });
3426
    const existing = await this.sql`
3427
      SELECT payload FROM openclasp_records
3428
      WHERE kind = 'completion_report' AND record_id = ${stored.reportId}
3429
        AND payload->>'interactionId' = ${stored.interactionId}
3430
      LIMIT 1
3431
    `;
3432
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
3433
      throw new Error('Conflicting completion report ID');
3434
    await Promise.all([
3435
      this.upsert(participant.participantOperatorId, 'completion_report', stored.reportId, stored),
3436
      this.upsert(participant.counterpartyOperatorId, 'completion_report', stored.reportId, stored),
3437
    ]);
3438
    const assuranceComparisons = (
3439
      await Promise.all([
3440
        this.recalculateAssuranceComparison(
3441
          participant.participantOperatorId,
3442
          stored.interactionId,
3443
          stored.reportingAgentId,
3444
        ).catch(() => undefined),
3445
        this.recalculateAssuranceComparison(
3446
          participant.counterpartyOperatorId,
3447
          stored.interactionId,
3448
          stored.reportingAgentId,
3449
        ).catch(() => undefined),
3450
      ])
3451
    ).filter((comparison) => comparison !== undefined);
3452
    const assuranceEvaluations = (
3453
      await Promise.all([
3454
        this.evaluateAssuranceOutcome(participant.participantOperatorId, stored).catch(
3455
          () => undefined,
3456
        ),
3457
        this.evaluateAssuranceOutcome(participant.counterpartyOperatorId, stored).catch(
3458
          () => undefined,
3459
        ),
3460
      ])
3461
    ).filter((evaluation) => evaluation !== undefined);
3462
    const [feedbackRequest] = await Promise.all([
3463
      this.ensureFeedbackRequest(
3464
        participant.participantOperatorId,
3465
        stored.interactionId,
3466
        stored.reportingAgentId,
3467
        stored.counterpartyAgentId,
3468
      ),
3469
      this.ensureFeedbackRequest(
3470
        participant.counterpartyOperatorId,
3471
        stored.interactionId,
3472
        stored.counterpartyAgentId,
3473
        stored.reportingAgentId,
3474
      ),
3475
    ]);
3476
    const completed = await this.sql`
3477
      SELECT COUNT(DISTINCT payload->>'reportingAgentId') AS count
3478
      FROM openclasp_records
3479
      WHERE kind = 'completion_report'
3480
        AND payload->>'interactionId' = ${stored.interactionId}
3481
    `;
3482
    let peerReportStatus: InteractionConclusion['peerReportStatus'] = 'received';
3483
    if (Number(completed[0]?.count ?? 0) >= 2) {
3484
      const completedAt = new Date().toISOString();
3485
      await Promise.all([
3486
        this.sql`
3487
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
3488
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
3489
        `,
3490
        this.sql`
3491
          UPDATE openclasp_federated_interactions
3492
          SET status = 'completed',
3493
            payload = jsonb_set(
3494
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
3495
              '{updatedAt}', to_jsonb(${completedAt}::text)
3496
            ),
3497
            updated_at = NOW()
3498
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
3499
        `,
3500
      ]);
3501
      await Promise.all([
3502
        this.journalLiveSessionState(stored.interactionId),
3503
        this.journalFederatedInteraction(stored.interactionId),
3504
      ]);
3505
    } else {
3506
      const requested = await this.requestRuntimeFinalization(
3507
        stored.interactionId,
3508
        stored.reportingAgentId,
3509
        stored.counterpartyAgentId,
3510
        stored.contractHash,
3511
      ).catch(() => false);
3512
      peerReportStatus = requested ? 'awaiting' : 'unreachable';
3513
    }
3514
    const conclusion = await this.finalizeInteractionConclusion(stored.interactionId, {
3515
      peerReportStatus,
3516
    });
3517
    return {
3518
      report: stored,
3519
      feedbackRequest,
3520
      peerReportRequested: peerReportStatus === 'awaiting',
3521
      assuranceComparisons,
3522
      assuranceEvaluations,
3523
      ...(conclusion.released ? { conclusion: conclusion.conclusion } : {}),
3524
    };
3525
  }
3526
3527
  async recordSessionCompletionReport(token: string, value: InteractionCompletionReport) {
3528
    const report = InteractionCompletionReportSchema.parse(value);
3529
    const grant = this.verifySessionGrant(token);
3530
    if (
3531
      grant.interactionId !== report.interactionId ||
3532
      grant.senderAgentId !== report.reportingAgentId ||
3533
      grant.recipientAgentId !== report.counterpartyAgentId
3534
    )
3535
      throw new Error('Session credential does not match the completion report');
3536
    const owners = await this.sql`
3537
      SELECT initiator_operator_id, responder_operator_id,
3538
        initiator_agent_id, responder_agent_id
3539
      FROM openclasp_federated_interactions
3540
      WHERE interaction_id = ${report.interactionId}
3541
      LIMIT 1
3542
    `;
3543
    const row = owners[0];
3544
    if (!row) throw new Error('Interaction not found');
3545
    const operatorId = String(
3546
      row.initiator_agent_id === report.reportingAgentId
3547
        ? row.initiator_operator_id
3548
        : row.responder_operator_id,
3549
    );
3550
    return this.submitCompletionReport(
3551
      operatorId,
3552
      report.reportingAgentId,
3553
      report,
3554
      'runtime_session',
3555
    );
3556
  }
3557
3558
  private async applyInteractionLearning(input: {
3559
    interaction: FederatedInteraction;
3560
    initiatorOperatorId: string;
3561
    responderOperatorId: string;
3562
    reports: InteractionCompletionReport[];
3563
    feedback: InteractionFeedback[];
3564
    conclusion: InteractionConclusion;
3565
  }) {
3566
    const { interaction, reports, feedback, conclusion } = input;
3567
    const [initiatorSettings, responderSettings, cards, pairActivity] = await Promise.all([
3568
      this.getSettings(input.initiatorOperatorId),
3569
      this.getSettings(input.responderOperatorId),
3570
      this.sql`
3571
        SELECT agent_id, card FROM openclasp_public_agents
3572
        WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
3573
      `,
3574
      this.sql`
3575
        SELECT COUNT(*)::integer AS count
3576
        FROM openclasp_federated_interactions
3577
        WHERE created_at >= NOW() - INTERVAL '24 hours'
3578
          AND (
3579
            (initiator_agent_id = ${interaction.initiatorAgentId}
3580
              AND responder_agent_id = ${interaction.responderAgentId})
3581
            OR
3582
            (initiator_agent_id = ${interaction.responderAgentId}
3583
              AND responder_agent_id = ${interaction.initiatorAgentId})
3584
          )
3585
      `,
3586
    ]);
3587
    const contributionMode =
3588
      initiatorSettings.contributionEnabled && responderSettings.contributionEnabled
3589
        ? ('network_aggregate' as const)
3590
        : ('local_only' as const);
3591
    const eligibilityBase = evaluateLearningEligibility({
3592
      interactionId: interaction.interactionId,
3593
      reports,
3594
      feedback,
3595
      consensus: conclusion.consensus,
3596
      contributionMode,
3597
      reviewerCredibility: Object.fromEntries(
3598
        feedback.map((item) => [item.reviewerAgentId, reviewerCredibility(item)]),
3599
      ),
3600
      manipulationSignals: [
3601
        ...(input.initiatorOperatorId === input.responderOperatorId ? ['same_operator_pair'] : []),
3602
        ...(Number(pairActivity[0]?.count ?? 0) > 10 ? ['rapid_reciprocal_pair_activity'] : []),
3603
      ],
3604
      decisionId: deterministicUuid(`learning-eligibility:${interaction.interactionId}`),
3605
      decidedAt: conclusion.generatedAt,
3606
    });
3607
    const eligibility = LearningEligibilityDecisionSchema.parse({
3608
      ...eligibilityBase,
3609
      platformAttestation: attestSessionRecord(this.gatewaySecret(), eligibilityBase),
3610
    });
3611
    await Promise.all([
3612
      this.upsert(
3613
        input.initiatorOperatorId,
3614
        'learning_eligibility',
3615
        eligibility.decisionId,
3616
        eligibility,
3617
      ),
3618
      this.upsert(
3619
        input.responderOperatorId,
3620
        'learning_eligibility',
3621
        eligibility.decisionId,
3622
        eligibility,
3623
      ),
3624
    ]);
3625
    if (!eligibility.eligible || eligibility.sampleWeight <= 0)
3626
      return { eligibility, profileDeltas: [] as BehaviouralProfileDelta[] };
3627
3628
    const appliedAt = new Date().toISOString();
3629
    const versions = new Map<string, string>();
3630
    for (const report of reports) versions.set(report.reportingAgentId, report.agentVersion);
3631
    for (const row of cards) {
3632
      const card = PublicAgentCardSchema.safeParse(row.card);
3633
      if (card.success && !versions.has(String(row.agent_id)))
3634
        versions.set(String(row.agent_id), card.data.agentVersion);
3635
    }
3636
    const participants = [
3637
      {
3638
        viewerOperatorId: input.initiatorOperatorId,
3639
        reviewerAgentId: interaction.initiatorAgentId,
3640
        subjectAgentId: interaction.responderAgentId,
3641
      },
3642
      {
3643
        viewerOperatorId: input.responderOperatorId,
3644
        reviewerAgentId: interaction.responderAgentId,
3645
        subjectAgentId: interaction.initiatorAgentId,
3646
      },
3647
      {
3648
        viewerOperatorId: input.initiatorOperatorId,
3649
        reviewerAgentId: interaction.responderAgentId,
3650
        subjectAgentId: interaction.initiatorAgentId,
3651
      },
3652
      {
3653
        viewerOperatorId: input.responderOperatorId,
3654
        reviewerAgentId: interaction.initiatorAgentId,
3655
        subjectAgentId: interaction.responderAgentId,
3656
      },
3657
    ];
3658
    const profileDeltas: Array<BehaviouralProfileDelta | undefined> = [];
3659
    for (const participant of participants) {
3660
      const agentVersion = versions.get(participant.subjectAgentId);
3661
      if (!agentVersion) {
3662
        profileDeltas.push(undefined);
3663
        continue;
3664
      }
3665
      const deltaId = deterministicUuid(
3666
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.reviewerAgentId}:${participant.subjectAgentId}`,
3667
      );
3668
      const legacyDeltaId = deterministicUuid(
3669
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.subjectAgentId}`,
3670
      );
3671
      const existingDelta = await this.sql`
3672
          SELECT payload FROM openclasp_records
3673
          WHERE operator_id = ${participant.viewerOperatorId}
3674
            AND kind = 'profile_delta'
3675
            AND (record_id = ${deltaId} OR record_id = ${legacyDeltaId})
3676
          LIMIT 1
3677
        `;
3678
      const parsedDelta = BehaviouralProfileDeltaSchema.safeParse(existingDelta[0]?.payload);
3679
      if (parsedDelta.success) {
3680
        profileDeltas.push(parsedDelta.data);
3681
        continue;
3682
      }
3683
      const profileId = deterministicUuid(
3684
        `contextual-profile:${participant.subjectAgentId}:${agentVersion}:${interaction.contract.taskCategory}`,
3685
      );
3686
      const currentRows = await this.sql`
3687
          SELECT payload FROM openclasp_records
3688
          WHERE operator_id = ${participant.viewerOperatorId}
3689
            AND kind = 'profile' AND record_id = ${profileId}
3690
          LIMIT 1
3691
        `;
3692
      const current = currentRows[0]?.payload as Partial<ContextualProfile> | undefined;
3693
      const reviewerFeedback = feedback.find(
3694
        (item) =>
3695
          item.reviewerAgentId === participant.reviewerAgentId &&
3696
          item.subjectAgentId === participant.subjectAgentId,
3697
      );
3698
      const observations = deriveBehaviouralObservations({
3699
        subjectAgentId: participant.subjectAgentId,
3700
        reviewerAgentId: participant.reviewerAgentId,
3701
        reports,
3702
        ...(reviewerFeedback ? { reviewerFeedback } : {}),
3703
        conclusion,
3704
      });
3705
      if (!Object.keys(observations).length) {
3706
        profileDeltas.push(undefined);
3707
        continue;
3708
      }
3709
      const applied = updateContextualBehaviouralProfile({
3710
        ...(current ? { current } : {}),
3711
        observations,
3712
        sampleWeight: eligibility.sampleWeight,
3713
        appliedAt,
3714
      });
3715
      const profile: ContextualProfile = {
3716
        recordId: profileId,
3717
        agentId: participant.subjectAgentId,
3718
        agentVersion,
3719
        taskCategory: interaction.contract.taskCategory,
3720
        ...applied.profile,
3721
      };
3722
      const deltaBase = BehaviouralProfileDeltaSchema.parse({
3723
        deltaId,
3724
        interactionId: interaction.interactionId,
3725
        agentId: participant.subjectAgentId,
3726
        agentVersion,
3727
        taskCategory: interaction.contract.taskCategory,
3728
        sampleWeight: eligibility.sampleWeight,
3729
        dimensionDeltas: applied.dimensionDeltas,
3730
        explanation:
3731
          'Applied platform-attested structured outcome and eligible reviewer signals; private comments and raw conversation content were excluded.',
3732
        appliedAt,
3733
      });
3734
      const delta = BehaviouralProfileDeltaSchema.parse({
3735
        ...deltaBase,
3736
        platformAttestation: attestSessionRecord(this.gatewaySecret(), deltaBase),
3737
      });
3738
      await Promise.all([
3739
        this.upsert(participant.viewerOperatorId, 'profile', profileId, profile),
3740
        this.upsert(participant.viewerOperatorId, 'profile_delta', deltaId, delta),
3741
      ]);
3742
      profileDeltas.push(delta);
3743
    }
3744
    return {
3745
      eligibility,
3746
      profileDeltas: profileDeltas.filter(
3747
        (delta): delta is BehaviouralProfileDelta => delta !== undefined,
3748
      ),
3749
    };
3750
  }
3751
3752
  private async finalizeInteractionConclusion(
3753
    interactionId: string,
3754
    options: { peerReportStatus?: InteractionConclusion['peerReportStatus'] } = {},
3755
  ) {
3756
    const [interactionRows, reportRows, feedbackRows, requestRows, existingRows] =
3757
      await Promise.all([
3758
        this.sql`
3759
        SELECT payload, initiator_operator_id, responder_operator_id
3760
        FROM openclasp_federated_interactions
3761
        WHERE interaction_id = ${interactionId}
3762
        LIMIT 1
3763
      `,
3764
        this.sql`
3765
        SELECT record_id, payload FROM openclasp_records
3766
        WHERE kind = 'completion_report' AND payload->>'interactionId' = ${interactionId}
3767
      `,
3768
        this.sql`
3769
        SELECT record_id, payload FROM openclasp_records
3770
        WHERE kind = 'interaction_feedback' AND payload->>'interactionId' = ${interactionId}
3771
      `,
3772
        this.sql`
3773
        SELECT record_id, payload FROM openclasp_records
3774
        WHERE kind = 'feedback_request' AND payload->>'interactionId' = ${interactionId}
3775
      `,
3776
        this.sql`
3777
        SELECT payload FROM openclasp_records
3778
        WHERE kind = 'interaction_conclusion' AND payload->>'interactionId' = ${interactionId}
3779
        LIMIT 1
3780
      `,
3781
      ]);
3782
    const interactionRow = interactionRows[0];
3783
    if (!interactionRow) throw new Error('Interaction not found');
3784
    const interaction = FederatedInteractionSchema.parse(interactionRow.payload);
3785
    const reports = [
3786
      ...new Map(
3787
        reportRows
3788
          .map((row) => InteractionCompletionReportSchema.parse(row.payload))
3789
          .map((report) => [report.reportId, report]),
3790
      ).values(),
3791
    ];
3792
    const feedback = [
3793
      ...new Map(
3794
        feedbackRows
3795
          .map((row) => InteractionFeedbackSchema.parse(row.payload))
3796
          .map((item) => [item.feedbackId, item]),
3797
      ).values(),
3798
    ];
3799
    const requests = [
3800
      ...new Map(
3801
        requestRows
3802
          .map((row) => FeedbackRequestSchema.parse(row.payload))
3803
          .map((request) => [request.requestId, request]),
3804
      ).values(),
3805
    ];
3806
    if (!reports.length) return { released: false as const, feedbackRevealed: false as const };
3807
    const existing = InteractionConclusionSchema.safeParse(existingRows[0]?.payload);
3808
    const reportingAgentIds = new Set(reports.map((report) => report.reportingAgentId));
3809
    const missingReportAgentIds = [
3810
      interaction.initiatorAgentId,
3811
      interaction.responderAgentId,
3812
    ].filter((agentId) => !reportingAgentIds.has(agentId));
3813
    const feedbackWindowClosed =
3814
      requests.length >= 2 && requests.every((request) => request.status !== 'pending');
3815
    const lifecycle =
3816
      missingReportAgentIds.length === 0 || feedbackWindowClosed
3817
        ? ('final' as const)
3818
        : ('provisional' as const);
3819
    const visibleFeedback = feedbackWindowClosed ? feedback : [];
3820
    const peerReportStatus: InteractionConclusion['peerReportStatus'] =
3821
      missingReportAgentIds.length === 0
3822
        ? 'received'
3823
        : feedbackWindowClosed
3824
          ? 'timed_out'
3825
          : (options.peerReportStatus ?? existing.data?.peerReportStatus ?? 'awaiting');
3826
    const pendingFeedbackAgentIds = requests
3827
      .filter((request) => request.status === 'pending')
3828
      .map((request) => request.reviewerAgentId);
3829
    if (requests.length < 2) return { released: false as const };
3830
    const base = buildInteractionConclusion({
3831
      interaction,
3832
      reports,
3833
      feedback: visibleFeedback,
3834
      conclusionId: deterministicUuid(`interaction-conclusion:${interactionId}`),
3835
      lifecycle,
3836
      pendingFeedbackAgentIds,
3837
      peerReportStatus,
3838
    });
3839
    const conclusion = InteractionConclusionSchema.parse({
3840
      ...base,
3841
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
3842
    });
3843
    const startedAt = reports
3844
      .map((report) => report.startedAt)
3845
      .filter((value): value is string => typeof value === 'string')
3846
      .sort()[0];
3847
    const receiptBase = {
3848
      receiptId: deterministicUuid(`interaction-receipt:${interactionId}`),
3849
      interactionId,
3850
      participants: [interaction.initiatorAgentId, interaction.responderAgentId],
3851
      agentVersions: Object.fromEntries(
3852
        reports.map((report) => [report.reportingAgentId, report.agentVersion]),
3853
      ),
3854
      contractHash: interaction.termsHash,
3855
      startedAt: startedAt ?? interaction.createdAt,
3856
      completedAt:
3857
        reports
3858
          .map((report) => report.completedAt)
3859
          .sort()
3860
          .at(-1) ?? conclusion.generatedAt,
3861
      outcome: conclusion.outcome,
3862
      commitmentsFulfilled: conclusion.criteria
3863
        .filter((criterion) => criterion.status === 'met')
3864
        .map((criterion) => criterion.criterion),
3865
      commitmentsMissed: conclusion.criteria
3866
        .filter((criterion) => criterion.status !== 'met')
3867
        .map((criterion) => criterion.criterion),
3868
      evidenceHashes: conclusion.evidenceReferences.map((reference) => canonicalHash(reference)),
3869
      policyWarnings: reports.flatMap((report) => report.blockers),
3870
      policyViolations: [],
3871
      disputeStatus: conclusion.consensus === 'conflicting' ? ('open' as const) : ('none' as const),
3872
      delegationChainHash: canonicalHash(interaction.contract.delegationRules),
3873
      unilateral: reports.length < 2,
3874
      provisional: lifecycle === 'provisional',
3875
      confidence: conclusion.confidence,
3876
      signatures: {},
3877
      completionReportIds: conclusion.reportIds,
3878
      conclusionId: conclusion.conclusionId,
3879
    };
3880
    const receipt = ReceiptSchema.parse({
3881
      ...receiptBase,
3882
      platformAttestation: attestSessionRecord(this.gatewaySecret(), receiptBase),
3883
    });
3884
    await Promise.all([
3885
      this.upsert(
3886
        String(interactionRow.initiator_operator_id),
3887
        'interaction_conclusion',
3888
        conclusion.conclusionId,
3889
        conclusion,
3890
      ),
3891
      this.upsert(
3892
        String(interactionRow.responder_operator_id),
3893
        'interaction_conclusion',
3894
        conclusion.conclusionId,
3895
        conclusion,
3896
      ),
3897
      this.upsert(
3898
        String(interactionRow.initiator_operator_id),
3899
        'receipt',
3900
        receipt.receiptId,
3901
        receipt,
3902
      ),
3903
      this.upsert(
3904
        String(interactionRow.responder_operator_id),
3905
        'receipt',
3906
        receipt.receiptId,
3907
        receipt,
3908
      ),
3909
    ]);
3910
    const learning =
3911
      lifecycle === 'final' && feedbackWindowClosed
3912
        ? await this.applyInteractionLearning({
3913
            interaction,
3914
            initiatorOperatorId: String(interactionRow.initiator_operator_id),
3915
            responderOperatorId: String(interactionRow.responder_operator_id),
3916
            reports,
3917
            feedback: visibleFeedback,
3918
            conclusion,
3919
          })
3920
        : undefined;
3921
    return {
3922
      released: true as const,
3923
      feedbackRevealed: feedbackWindowClosed,
3924
      conclusion,
3925
      receipt,
3926
      ...(learning ? { learning } : {}),
3927
    };
3928
  }
3929
3930
  async submitInteractionFeedback(
3931
    operatorId: string,
3932
    agentId: string,
3933
    value: InteractionFeedback,
3934
    submissionMethod:
3935
      'oauth_account' | 'oauth_installation' | 'agent_access_token' | 'runtime_session',
3936
  ) {
3937
    const feedback = InteractionFeedbackSchema.parse(value);
3938
    if (feedback.platformAttestation)
3939
      throw new Error('Platform attestation is assigned by OpenClasp');
3940
    if (feedback.reviewerAgentId !== agentId)
3941
      throw new Error('Feedback reviewer does not match the authenticated agent');
3942
    const participant = await this.interactionParticipant(
3943
      operatorId,
3944
      feedback.interactionId,
3945
      agentId,
3946
    );
3947
    if (feedback.subjectAgentId !== participant.counterpartyAgentId)
3948
      throw new Error('Feedback subject does not match the interaction counterparty');
3949
    const requestRows = await this.sql`
3950
      SELECT payload FROM openclasp_records
3951
      WHERE operator_id = ${operatorId}
3952
        AND kind = 'feedback_request'
3953
        AND record_id = ${feedback.requestId}
3954
      LIMIT 1
3955
    `;
3956
    const request = FeedbackRequestSchema.parse(requestRows[0]?.payload);
3957
    if (
3958
      request.interactionId !== feedback.interactionId ||
3959
      request.reviewerAgentId !== feedback.reviewerAgentId ||
3960
      request.subjectAgentId !== feedback.subjectAgentId
3961
    )
3962
      throw new Error('Feedback does not match its request');
3963
    if (request.status !== 'pending') throw new Error('Feedback request is no longer pending');
3964
    if (Date.parse(request.dueAt) <= Date.now()) throw new Error('Feedback request has expired');
3965
    if (
3966
      request.requestedDimensions.some(
3967
        (dimension) => typeof feedback.ratings[dimension] !== 'number',
3968
      )
3969
    )
3970
      throw new Error('Feedback must rate every requested dimension');
3971
    const profiles = await this.sql`
3972
      SELECT payload FROM openclasp_records
3973
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
3974
      LIMIT 1
3975
    `;
3976
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
3977
    if (!profile || profile.agentVersion !== feedback.reviewerAgentVersion)
3978
      throw new Error('Feedback reviewer version does not match the registered agent');
3979
    let verifiedSubmissionMethod: InteractionFeedback['submissionMethod'] = submissionMethod;
3980
    if (feedback.signature) {
3981
      if (feedback.submissionMethod !== 'agent_signature')
3982
        throw new Error('Signed feedback must declare agent_signature submission');
3983
      const identities = await this.sql`
3984
        SELECT payload FROM openclasp_records
3985
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
3986
        LIMIT 1
3987
      `;
3988
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
3989
      if (
3990
        !identity.success ||
3991
        !verifyObject(feedback as unknown as Record<string, unknown>, identity.data.publicKey)
3992
      )
3993
        throw new Error('Feedback agent signature is invalid or unverifiable');
3994
      verifiedSubmissionMethod = 'agent_signature';
3995
    }
3996
    const unattested = InteractionFeedbackSchema.parse({
3997
      ...feedback,
3998
      submissionMethod: verifiedSubmissionMethod,
3999
    });
4000
    const stored = InteractionFeedbackSchema.parse({
4001
      ...unattested,
4002
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unattested),
4003
    });
4004
    const existing = await this.sql`
4005
      SELECT payload FROM openclasp_records
4006
      WHERE operator_id = ${operatorId}
4007
        AND kind = 'interaction_feedback'
4008
        AND record_id = ${stored.feedbackId}
4009
      LIMIT 1
4010
    `;
4011
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
4012
      throw new Error('Conflicting feedback ID');
4013
    await this.upsert(operatorId, 'interaction_feedback', stored.feedbackId, stored);
4014
    const requestBase = { ...request, status: 'submitted' as const };
4015
    delete requestBase.platformAttestation;
4016
    const updatedRequest = FeedbackRequestSchema.parse({
4017
      ...requestBase,
4018
      platformAttestation: attestSessionRecord(this.gatewaySecret(), requestBase),
4019
    });
4020
    await this.upsert(operatorId, 'feedback_request', request.requestId, updatedRequest);
4021
    const release = await this.finalizeInteractionConclusion(feedback.interactionId);
4022
    return {
4023
      feedbackId: stored.feedbackId,
4024
      status: 'submitted' as const,
4025
      revealed: release.released && release.feedbackRevealed,
4026
      ...(release.released ? { conclusion: release.conclusion } : {}),
4027
    };
4028
  }
4029
4030
  async processDueFeedback(now = new Date()) {
4031
    await this.ensureSchema();
4032
    const expiredInteractions = await this.sql`
4033
      UPDATE openclasp_federated_interactions
4034
      SET status = 'expired',
4035
        payload = jsonb_set(
4036
          jsonb_set(payload, '{status}', '"expired"'::jsonb),
4037
          '{updatedAt}', to_jsonb(${now.toISOString()}::text)
4038
        ),
4039
        updated_at = NOW()
4040
      WHERE status IN ('pending', 'active') AND expires_at <= ${now.toISOString()}
4041
      RETURNING interaction_id
4042
    `;
4043
    if (expiredInteractions.length) {
4044
      await this.sql`
4045
        UPDATE openclasp_live_sessions
4046
        SET status = 'failed', completed_at = COALESCE(completed_at, NOW()),
4047
          last_error = COALESCE(last_error, 'Interaction expired before bilateral completion')
4048
        WHERE status IN ('preparing', 'active')
4049
          AND interaction_id IN (
4050
            SELECT interaction_id FROM openclasp_federated_interactions
4051
            WHERE status = 'expired' AND expires_at <= ${now.toISOString()}
4052
          )
4053
      `;
4054
      await Promise.all(
4055
        expiredInteractions.flatMap((row) => {
4056
          const interactionId = String(row.interaction_id);
4057
          return [
4058
            this.journalFederatedInteraction(interactionId),
4059
            this.journalLiveSessionState(interactionId),
4060
          ];
4061
        }),
4062
      );
4063
    }
4064
    const [rows, backfillRows] = await Promise.all([
4065
      this.sql`
4066
        SELECT operator_id, record_id, payload FROM openclasp_records
4067
        WHERE kind = 'feedback_request'
4068
          AND payload->>'status' = 'pending'
4069
          AND (payload->>'dueAt')::timestamptz <= ${now.toISOString()}
4070
        LIMIT 500
4071
      `,
4072
      this.sql`
4073
        SELECT source.interaction_id
4074
        FROM (
4075
          SELECT conclusion.payload->>'interactionId' AS interaction_id
4076
          FROM openclasp_records conclusion
4077
          WHERE conclusion.kind = 'interaction_conclusion'
4078
            AND NOT EXISTS (
4079
              SELECT 1 FROM openclasp_records eligibility
4080
              WHERE eligibility.operator_id = conclusion.operator_id
4081
                AND eligibility.kind = 'learning_eligibility'
4082
                AND eligibility.payload->>'interactionId' = conclusion.payload->>'interactionId'
4083
            )
4084
          UNION
4085
          SELECT report.payload->>'interactionId' AS interaction_id
4086
          FROM openclasp_records report
4087
          WHERE report.kind = 'completion_report'
4088
            AND NOT EXISTS (
4089
              SELECT 1 FROM openclasp_records conclusion
4090
              WHERE conclusion.kind = 'interaction_conclusion'
4091
                AND conclusion.payload->>'interactionId' = report.payload->>'interactionId'
4092
            )
4093
        ) source
4094
        WHERE source.interaction_id IS NOT NULL
4095
        GROUP BY source.interaction_id
4096
        ORDER BY source.interaction_id ASC
4097
        LIMIT 200
4098
      `,
4099
    ]);
4100
    const interactions = new Set<string>();
4101
    for (const row of rows) {
4102
      const request = FeedbackRequestSchema.parse(row.payload);
4103
      const base = { ...request, status: 'expired' as const };
4104
      delete base.platformAttestation;
4105
      const expired = FeedbackRequestSchema.parse({
4106
        ...base,
4107
        platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
4108
      });
4109
      await this.upsert(
4110
        String(row.operator_id),
4111
        'feedback_request',
4112
        String(row.record_id),
4113
        expired,
4114
      );
4115
      interactions.add(request.interactionId);
4116
    }
4117
    for (const row of backfillRows) interactions.add(String(row.interaction_id));
4118
    let released = 0;
4119
    for (const interactionId of interactions) {
4120
      const result = await this.finalizeInteractionConclusion(interactionId);
4121
      if (result.released) released += 1;
4122
    }
4123
    return {
4124
      expired: rows.length,
4125
      interactionsExpired: expiredInteractions.length,
4126
      released,
4127
      backfilled: backfillRows.length,
4128
    };
4129
  }
4130
4131
  async recordSessionFeedback(token: string, value: InteractionFeedback) {
4132
    const feedback = InteractionFeedbackSchema.parse(value);
4133
    const grant = this.verifySessionGrant(token);
4134
    if (
4135
      grant.interactionId !== feedback.interactionId ||
4136
      grant.senderAgentId !== feedback.reviewerAgentId ||
4137
      grant.recipientAgentId !== feedback.subjectAgentId
4138
    )
4139
      throw new Error('Session credential does not match the feedback');
4140
    const owners = await this.sql`
4141
      SELECT initiator_operator_id, responder_operator_id,
4142
        initiator_agent_id, responder_agent_id
4143
      FROM openclasp_federated_interactions
4144
      WHERE interaction_id = ${feedback.interactionId}
4145
      LIMIT 1
4146
    `;
4147
    const row = owners[0];
4148
    if (!row) throw new Error('Interaction not found');
4149
    const operatorId = String(
4150
      row.initiator_agent_id === feedback.reviewerAgentId
4151
        ? row.initiator_operator_id
4152
        : row.responder_operator_id,
4153
    );
4154
    return this.submitInteractionFeedback(
4155
      operatorId,
4156
      feedback.reviewerAgentId,
4157
      feedback,
4158
      'runtime_session',
4159
    );
4160
  }
4161
4162
  async recordLiveSessionEvent(token: string, value: LiveSessionEvent) {
4163
    await this.ensureSchema();
4164
    const event = LiveSessionEventSchema.parse(value);
4165
    const grant = this.verifySessionGrant(token);
4166
    if (grant.interactionId !== event.interactionId || grant.senderAgentId !== event.agentId)
4167
      throw new Error('Session credential does not match the event');
4168
    const sessions = await this.sql`
4169
      SELECT 1 FROM openclasp_live_sessions
4170
      WHERE interaction_id = ${event.interactionId}
4171
        AND status = 'active'
4172
        AND ${event.agentId} IN (initiator_agent_id, responder_agent_id)
4173
    `;
4174
    if (!sessions.length) throw new Error('Active live session not found');
4175
    const attestation = attestSessionRecord(this.gatewaySecret(), event);
4176
    const encoded = JSON.stringify({ ...event, attestation });
4177
    const rows = await this.sql`
4178
      INSERT INTO openclasp_live_session_events(
4179
        interaction_id, event_id, agent_id, sequence, event
4180
      ) SELECT ${event.interactionId}, ${event.eventId}, ${event.agentId}, ${event.sequence}, ${encoded}::jsonb
4181
      WHERE (
4182
        SELECT COUNT(*) FROM openclasp_live_session_events
4183
        WHERE interaction_id = ${event.interactionId}
4184
      ) < 1000
4185
      ON CONFLICT DO NOTHING
4186
      RETURNING event_id
4187
    `;
4188
    if (rows.length)
4189
      await this.journalLiveSessionEvent({
4190
        ...event,
4191
        attestation,
4192
      });
4193
    if (event.type === 'session_completed' || event.type === 'session_failed') {
4194
      const terminal = await this.sql`
4195
        SELECT COUNT(DISTINCT agent_id) AS count
4196
        FROM openclasp_live_session_events
4197
        WHERE interaction_id = ${event.interactionId}
4198
          AND event->>'type' IN ('session_completed', 'session_failed')
4199
      `;
4200
      if (Number(terminal[0]?.count ?? 0) >= 2) {
4201
        await this.sql`
4202
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
4203
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
4204
        `;
4205
        const completedAt = new Date().toISOString();
4206
        await this.sql`
4207
          UPDATE openclasp_federated_interactions
4208
          SET status = 'completed',
4209
            payload = jsonb_set(
4210
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
4211
              '{updatedAt}', to_jsonb(${completedAt}::text)
4212
            ),
4213
            updated_at = NOW()
4214
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
4215
        `;
4216
        await Promise.all([
4217
          this.journalLiveSessionState(event.interactionId),
4218
          this.journalFederatedInteraction(event.interactionId),
4219
        ]);
4220
      }
4221
    }
4222
    return {
4223
      recorded: rows.length > 0,
4224
      deduplicated: rows.length === 0,
4225
      eventId: event.eventId,
4226
      attestation,
4227
    };
4228
  }
4229
4230
  async createFederatedInteraction(
4231
    operatorId: string,
4232
    value: FederatedInteraction,
4233
  ): Promise<FederatedInteraction> {
4234
    await this.ensureSchema();
4235
    let interaction = FederatedInteractionSchema.parse(value);
4236
    if (interaction.status !== 'pending') throw new Error('New interactions must be pending');
4237
    if (interaction.contractRevision !== 1 || interaction.contractRevisions.length)
4238
      throw new Error('New interactions cannot supply contract revision history');
4239
    if (interaction.contract.interactionId !== interaction.interactionId)
4240
      throw new Error('Contract interaction ID does not match');
4241
    if (canonicalHash(interaction.contract) !== interaction.termsHash)
4242
      throw new Error('Contract hash does not match the immutable terms');
4243
    if (
4244
      interaction.contract.parties.length !== 2 ||
4245
      interaction.contract.parties[0] !== interaction.initiatorAgentId ||
4246
      interaction.contract.parties[1] !== interaction.responderAgentId
4247
    )
4248
      throw new Error('Contract parties do not match the interaction participants');
4249
    const acceptanceEntries = Object.entries(interaction.acceptances);
4250
    if (
4251
      acceptanceEntries.length !== 1 ||
4252
      acceptanceEntries[0]?.[0] !== interaction.initiatorAgentId ||
4253
      acceptanceEntries[0]?.[1].agentId !== interaction.initiatorAgentId ||
4254
      acceptanceEntries[0]?.[1].termsHash !== interaction.termsHash ||
4255
      !['oauth_installation', 'oauth_account'].includes(acceptanceEntries[0]?.[1].method ?? '')
4256
    )
4257
      throw new Error('A pending interaction requires only the initiator acceptance');
4258
    if (Date.parse(interaction.expiresAt) <= Date.now())
4259
      throw new Error('Interaction expiry must be in the future');
4260
    interaction = withContractRevisionHistory(interaction);
4261
    const owners = await this.sql`
4262
      SELECT agent_id, operator_id, card FROM openclasp_public_agents
4263
      WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
4264
    `;
4265
    const initiator = owners.find((row) => row.agent_id === interaction.initiatorAgentId);
4266
    const responder = owners.find((row) => row.agent_id === interaction.responderAgentId);
4267
    if (!initiator || initiator.operator_id !== operatorId)
4268
      throw new Error('The initiating agent must be published and owned by this account');
4269
    if (!responder) throw new Error('Responder is not a published OpenClasp agent');
4270
    if (responder.operator_id === operatorId)
4271
      throw new Error('Federated interactions require agents from different accounts');
4272
    const responderCard = normalizePublicAgentCard(responder.card);
4273
    if (
4274
      !responderCard.transports.some(
4275
        (transport) =>
4276
          transport.endpoint === interaction.responderTransport.endpoint &&
4277
          transport.protocolBinding === interaction.responderTransport.protocolBinding,
4278
      )
4279
    )
4280
      throw new Error('Responder transport does not match its published Agent Card');
4281
    const encoded = JSON.stringify(interaction);
4282
    const rows = await this.sql`
4283
      INSERT INTO openclasp_federated_interactions(
4284
        interaction_id, initiator_operator_id, responder_operator_id,
4285
        initiator_agent_id, responder_agent_id, status, payload, expires_at
4286
      ) VALUES (
4287
        ${interaction.interactionId}, ${operatorId}, ${responder.operator_id},
4288
        ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
4289
        ${interaction.status}, ${encoded}::jsonb, ${interaction.expiresAt}
4290
      )
4291
      RETURNING payload
4292
    `;
4293
    const stored = FederatedInteractionSchema.parse(rows[0]?.payload);
4294
    await this.journalFederatedInteraction(stored.interactionId);
4295
    const profiles = await this.sql`
4296
      SELECT payload FROM openclasp_records
4297
      WHERE operator_id = ${responder.operator_id}
4298
        AND kind = 'agent_profile'
4299
        AND record_id = ${interaction.responderAgentId}
4300
      LIMIT 1
4301
    `;
4302
    const responderProfile = profiles[0]?.payload as AgentProfile | undefined;
4303
    if (responderProfile && canAutoAcceptInteraction(responderProfile, stored))
4304
      return this.respondToFederatedInteraction(
4305
        String(responder.operator_id),
4306
        stored.interactionId,
4307
        stored.responderAgentId,
4308
        'accept',
4309
        'policy_auto_accept',
4310
      );
4311
    return stored;
4312
  }
4313
4314
  async listFederatedInteractions(operatorId: string): Promise<FederatedInteraction[]> {
4315
    await this.ensureSchema();
4316
    const expired = await this.sql`
4317
      UPDATE openclasp_federated_interactions
4318
      SET status = 'expired', payload = jsonb_set(payload, '{status}', '"expired"'::jsonb), updated_at = NOW()
4319
      WHERE status = 'pending' AND expires_at <= NOW()
4320
      RETURNING interaction_id
4321
    `;
4322
    await Promise.all(
4323
      expired.map((row) => this.journalFederatedInteraction(String(row.interaction_id))),
4324
    );
4325
    const rows = await this.sql`
4326
      SELECT payload FROM openclasp_federated_interactions
4327
      WHERE initiator_operator_id = ${operatorId} OR responder_operator_id = ${operatorId}
4328
      ORDER BY updated_at DESC
4329
    `;
4330
    return rows.map((row) =>
4331
      withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload)),
4332
    );
4333
  }
4334
4335
  async getFederatedInteraction(
4336
    operatorId: string,
4337
    interactionId: string,
4338
  ): Promise<FederatedInteraction | undefined> {
4339
    const values = await this.listFederatedInteractions(operatorId);
4340
    return values.find((value) => value.interactionId === interactionId);
4341
  }
4342
4343
  async respondToFederatedInteraction(
4344
    operatorId: string,
4345
    interactionId: string,
4346
    agentId: string,
4347
    decision: 'accept' | 'reject',
4348
    method: 'oauth_installation' | 'oauth_account' | 'policy_auto_accept' = 'oauth_account',
4349
  ): Promise<FederatedInteraction> {
4350
    await this.ensureSchema();
4351
    const rows = await this.sql`
4352
      SELECT payload, responder_operator_id, responder_agent_id
4353
      FROM openclasp_federated_interactions WHERE interaction_id = ${interactionId}
4354
    `;
4355
    const row = rows[0];
4356
    if (!row) throw new Error('Interaction invitation not found');
4357
    if (row.responder_operator_id !== operatorId || row.responder_agent_id !== agentId)
4358
      throw new Error('Only the invited agent may respond');
4359
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
4360
    if (current.status !== 'pending') throw new Error('Invitation is no longer pending');
4361
    const now = new Date().toISOString();
4362
    if (Date.parse(current.expiresAt) <= Date.now()) {
4363
      const expired = { ...current, status: 'expired' as const, updatedAt: now };
4364
      await this.updateFederatedInteraction(
4365
        interactionId,
4366
        current.status,
4367
        expired,
4368
        current.updatedAt,
4369
      );
4370
      throw new Error('Invitation has expired');
4371
    }
4372
    const proposed = openContractRevision(current);
4373
    if (!proposed || proposed.termsHash !== current.termsHash)
4374
      throw new Error('No current contract proposal is available');
4375
    const acceptance = {
4376
      agentId,
4377
      method,
4378
      termsHash: current.termsHash,
4379
      acceptedAt: now,
4380
    };
4381
    const proposalBase: ContractRevision = {
4382
      ...proposed,
4383
      status: decision === 'accept' ? 'accepted' : 'rejected',
4384
      acceptances:
4385
        decision === 'accept'
4386
          ? { ...proposed.acceptances, [agentId]: acceptance }
4387
          : proposed.acceptances,
4388
      updatedAt: now,
4389
    };
4390
    const resolvedProposal =
4391
      decision === 'accept'
4392
        ? ContractRevisionSchema.parse({
4393
            ...proposalBase,
4394
            platformAttestation: attestSessionRecord(this.gatewaySecret(), proposalBase),
4395
          })
4396
        : proposalBase;
4397
    const next = FederatedInteractionSchema.parse({
4398
      ...current,
4399
      status: decision === 'accept' ? 'active' : 'rejected',
4400
      updatedAt: now,
4401
      acceptances: decision === 'accept' ? resolvedProposal.acceptances : current.acceptances,
4402
      contractRevisions: current.contractRevisions.map((revision) =>
4403
        revision.revisionId === resolvedProposal.revisionId ? resolvedProposal : revision,
4404
      ),
4405
    });
4406
    if (decision === 'accept') {
4407
      try {
4408
        await this.brokerLiveSession(next);
4409
      } catch (error) {
4410
        const reason = error instanceof Error ? error.message.slice(0, 500) : 'Live session failed';
4411
        await this.sql`
4412
          UPDATE openclasp_agent_runtimes SET last_error = ${reason}, updated_at = NOW()
4413
          WHERE agent_id IN (${current.initiatorAgentId}, ${current.responderAgentId})
4414
        `;
4415
        throw error;
4416
      }
4417
    }
4418
    const updated = await this.updateFederatedInteraction(
4419
      interactionId,
4420
      current.status,
4421
      next,
4422
      current.updatedAt,
4423
    );
4424
    if (!updated) throw new Error('Invitation was already handled');
4425
    return next;
4426
  }
4427
4428
  async proposeContractRevision(
4429
    operatorId: string,
4430
    interactionId: string,
4431
    agentId: string,
4432
    contract: InteractionContract,
4433
    expectedTermsHash?: string,
4434
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
4435
  ): Promise<FederatedInteraction> {
4436
    await this.ensureSchema();
4437
    const rows = await this.sql`
4438
      SELECT payload, initiator_operator_id, responder_operator_id,
4439
        initiator_agent_id, responder_agent_id
4440
      FROM openclasp_federated_interactions
4441
      WHERE interaction_id = ${interactionId}
4442
    `;
4443
    const row = rows[0];
4444
    if (!row) throw new Error('Interaction not found');
4445
    const ownsParticipant =
4446
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
4447
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
4448
    if (!ownsParticipant) throw new Error('Only a participating agent may propose contract terms');
4449
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
4450
    if (!['pending', 'active'].includes(current.status))
4451
      throw new Error('This interaction no longer accepts contract proposals');
4452
    const parsedContract = InteractionContractSchema.parse(contract);
4453
    if (parsedContract.interactionId !== interactionId)
4454
      throw new Error('Contract interaction ID does not match');
4455
    if (
4456
      parsedContract.parties.length !== 2 ||
4457
      parsedContract.parties[0] !== current.initiatorAgentId ||
4458
      parsedContract.parties[1] !== current.responderAgentId
4459
    )
4460
      throw new Error('Contract parties cannot change during negotiation');
4461
    const open = openContractRevision(current);
4462
    const expected = open?.termsHash ?? current.termsHash;
4463
    if (expectedTermsHash && expectedTermsHash !== expected)
4464
      throw new Error('Contract terms changed; fetch the interaction before countering');
4465
    const termsHash = canonicalHash(parsedContract);
4466
    if (termsHash === expected) throw new Error('Proposed terms are unchanged');
4467
    const now = new Date().toISOString();
4468
    const revisionNumber =
4469
      Math.max(
4470
        current.contractRevision,
4471
        ...current.contractRevisions.map((item) => item.revision),
4472
      ) + 1;
4473
    const acceptance = {
4474
      agentId,
4475
      method,
4476
      termsHash,
4477
      acceptedAt: now,
4478
    } as const;
4479
    const revision = ContractRevisionSchema.parse({
4480
      revisionId: crypto.randomUUID(),
4481
      interactionId,
4482
      revision: revisionNumber,
4483
      previousTermsHash: expected,
4484
      termsHash,
4485
      contract: parsedContract,
4486
      proposedByAgentId: agentId,
4487
      status: 'proposed',
4488
      acceptances: { [agentId]: acceptance },
4489
      createdAt: now,
4490
      updatedAt: now,
4491
    });
4492
    const revisions = [
4493
      ...current.contractRevisions.map((item) =>
4494
        item.status === 'proposed'
4495
          ? ContractRevisionSchema.parse({ ...item, status: 'superseded', updatedAt: now })
4496
          : item,
4497
      ),
4498
      revision,
4499
    ];
4500
    const next = FederatedInteractionSchema.parse({
4501
      ...current,
4502
      ...(current.status === 'pending'
4503
        ? {
4504
            contract: parsedContract,
4505
            termsHash,
4506
            acceptances: revision.acceptances,
4507
            contractRevision: revisionNumber,
4508
          }
4509
        : {}),
4510
      contractRevisions: revisions,
4511
      updatedAt: now,
4512
    });
4513
    const updated = await this.updateFederatedInteraction(
4514
      interactionId,
4515
      current.status,
4516
      next,
4517
      current.updatedAt,
4518
    );
4519
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
4520
    return next;
4521
  }
4522
4523
  async respondToContractRevision(
4524
    operatorId: string,
4525
    interactionId: string,
4526
    agentId: string,
4527
    revisionId: string,
4528
    decision: 'accept' | 'reject',
4529
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
4530
  ): Promise<FederatedInteraction> {
4531
    await this.ensureSchema();
4532
    const rows = await this.sql`
4533
      SELECT payload, initiator_operator_id, responder_operator_id,
4534
        initiator_agent_id, responder_agent_id
4535
      FROM openclasp_federated_interactions
4536
      WHERE interaction_id = ${interactionId}
4537
    `;
4538
    const row = rows[0];
4539
    if (!row) throw new Error('Interaction not found');
4540
    const ownsParticipant =
4541
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
4542
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
4543
    if (!ownsParticipant) throw new Error('Only a participating agent may respond to terms');
4544
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
4545
    if (!['pending', 'active'].includes(current.status))
4546
      throw new Error('This interaction no longer accepts contract responses');
4547
    const proposal = openContractRevision(current);
4548
    if (!proposal || proposal.revisionId !== revisionId)
4549
      throw new Error('Contract proposal is no longer current');
4550
    if (proposal.acceptances[agentId]) throw new Error('Agent already accepted this proposal');
4551
    const now = new Date().toISOString();
4552
    const acceptances =
4553
      decision === 'accept'
4554
        ? {
4555
            ...proposal.acceptances,
4556
            [agentId]: { agentId, method, termsHash: proposal.termsHash, acceptedAt: now },
4557
          }
4558
        : proposal.acceptances;
4559
    const fullyAccepted = current.contract.parties.every((party) => party in acceptances);
4560
    const revisionBase = ContractRevisionSchema.parse({
4561
      ...proposal,
4562
      status: decision === 'reject' ? 'rejected' : fullyAccepted ? 'accepted' : 'proposed',
4563
      acceptances,
4564
      updatedAt: now,
4565
    });
4566
    const revision =
4567
      fullyAccepted && decision === 'accept'
4568
        ? ContractRevisionSchema.parse({
4569
            ...revisionBase,
4570
            platformAttestation: attestSessionRecord(this.gatewaySecret(), revisionBase),
4571
          })
4572
        : revisionBase;
4573
    const next = FederatedInteractionSchema.parse({
4574
      ...current,
4575
      status:
4576
        current.status === 'pending'
4577
          ? decision === 'reject'
4578
            ? 'rejected'
4579
            : fullyAccepted
4580
              ? 'active'
4581
              : 'pending'
4582
          : 'active',
4583
      ...(fullyAccepted
4584
        ? {
4585
            contract: revision.contract,
4586
            termsHash: revision.termsHash,
4587
            acceptances: revision.acceptances,
4588
            contractRevision: revision.revision,
4589
          }
4590
        : {}),
4591
      contractRevisions: current.contractRevisions.map((item) =>
4592
        item.revisionId === revision.revisionId ? revision : item,
4593
      ),
4594
      updatedAt: now,
4595
    });
4596
    if (current.status === 'pending' && decision === 'accept' && fullyAccepted)
4597
      await this.brokerLiveSession(next);
4598
    const updated = await this.updateFederatedInteraction(
4599
      interactionId,
4600
      current.status,
4601
      next,
4602
      current.updatedAt,
4603
    );
4604
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
4605
    return next;
4606
  }
4607
4608
  private async updateFederatedInteraction(
4609
    interactionId: string,
4610
    expectedStatus: string,
4611
    value: FederatedInteraction,
4612
    expectedUpdatedAt?: string,
4613
  ): Promise<boolean> {
4614
    const encoded = JSON.stringify(FederatedInteractionSchema.parse(value));
4615
    const rows = await this.sql`
4616
      UPDATE openclasp_federated_interactions
4617
      SET status = ${value.status}, payload = ${encoded}::jsonb, updated_at = NOW()
4618
      WHERE interaction_id = ${interactionId} AND status = ${expectedStatus}
4619
        AND (${expectedUpdatedAt ?? null}::text IS NULL OR payload->>'updatedAt' = ${expectedUpdatedAt ?? null})
4620
      RETURNING interaction_id
4621
    `;
4622
    if (rows.length) await this.journalFederatedInteraction(interactionId);
4623
    return rows.length > 0;
4624
  }
4625
4626
  async getSettings(operatorId: string): Promise<AccountSettings> {
4627
    await this.ensureSchema();
4628
    const rows = await this.sql`
4629
      SELECT display_name, contribution_enabled
4630
      FROM openclasp_account_settings
4631
      WHERE operator_id = ${operatorId}
4632
    `;
4633
    const row = rows[0] as
4634
      | {
4635
          display_name: string;
4636
          contribution_enabled: boolean;
4637
        }
4638
      | undefined;
4639
    return row
4640
      ? {
4641
          displayName: row.display_name,
4642
          contributionEnabled: row.contribution_enabled,
4643
          rawConversationsStored: false,
4644
        }
4645
      : defaults;
4646
  }
4647
4648
  async saveSettings(
4649
    operatorId: string,
4650
    settings: Omit<AccountSettings, 'rawConversationsStored'>,
4651
  ): Promise<AccountSettings> {
4652
    await this.ensureSchema();
4653
    await this.sql`
4654
      INSERT INTO openclasp_account_settings(
4655
        operator_id, display_name, contribution_enabled
4656
      ) VALUES (
4657
        ${operatorId}, ${settings.displayName}, ${settings.contributionEnabled}
4658
      )
4659
      ON CONFLICT (operator_id) DO UPDATE SET
4660
        display_name = EXCLUDED.display_name,
4661
        contribution_enabled = EXCLUDED.contribution_enabled,
4662
        updated_at = NOW()
4663
    `;
4664
    return { ...settings, rawConversationsStored: false };
4665
  }
4666
}