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

Viewing files at commit 867f032d6ce0Commit detailsView latest

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