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

Viewing files at commit 77f5465edca6Commit detailsView latest

packages/persistence/src/hosted.ts 3781 lines · 145.4 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
  BehaviouralProfileDeltaSchema,
15
  DEFAULT_EXTENSION_URI,
16
  CounterpartyBriefSchema,
17
  ContractRevisionSchema,
18
  FederatedInteractionSchema,
19
  FeedbackDimensionSchema,
20
  FeedbackRequestSchema,
21
  LiveSessionAcceptanceSchema,
22
  LiveSessionActivationSchema,
23
  LiveSessionEventSchema,
24
  LiveSessionOfferSchema,
25
  LiveSessionStateRecordSchema,
26
  HostedMessageSchema,
27
  HostedThreadSchema,
28
  InteractionCompletionReportSchema,
29
  InteractionContractSchema,
30
  InteractionConclusionSchema,
31
  InteractionFeedbackSchema,
32
  LearningEligibilityDecisionSchema,
33
  PublicAgentCardSchema,
34
  ReceiptSchema,
35
  canonicalHash,
36
  verifyObject,
37
  type FederatedInteraction,
38
  type AgentPresence,
39
  type AgentResolution,
40
  type BehaviouralProfileDelta,
41
  type CounterpartyBrief,
42
  type ContractRevision,
43
  type LiveSessionActivation,
44
  type LiveSessionEvent,
45
  type LiveSessionInsight,
46
  type HostedMessage,
47
  type HostedThread,
48
  type InteractionCompletionReport,
49
  type InteractionContract,
50
  type InteractionFeedback,
51
  type InteractionConclusion,
52
  type PublicAgentCard,
53
} from '../../protocol/src/index.js';
54
import type { AgentProfile } from './onboarding.js';
55
import type { AgentInstallation } from './onboarding.js';
56
import {
57
  agentAccessTokenClientId,
58
  agentAccessTokenId,
59
  createAgentAccessToken,
60
  matchesAgentAccessToken,
61
  type AgentAccessTokenMetadata,
62
} from './access-token.js';
63
import {
64
  decryptGatewayPayload,
65
  encryptGatewayPayload,
66
  attestSessionRecord,
67
  getSessionKeyId,
68
  getSessionVerificationKey,
69
  issueSessionGrant,
70
  signSessionControl,
71
  verifySessionGrant,
72
} from './relay.js';
73
import { postRuntimeJson, resolvePublicRuntimeEndpoint } from './runtime.js';
74
import { verifyHostedMigrations } from './hosted-migrations.js';
75
import {
76
  buildSourceRecordEnvelope,
77
  shouldJournalSourceRecord,
78
  type SourceRecordWriteMetadata,
79
} from './source-record.js';
80
81
export type HostedRecordKind =
82
  | 'agent'
83
  | 'delegation'
84
  | 'contract'
85
  | 'interaction'
86
  | 'event'
87
  | 'receipt'
88
  | 'feedback'
89
  | 'counterparty_brief'
90
  | 'completion_report'
91
  | 'feedback_request'
92
  | 'interaction_feedback'
93
  | 'interaction_conclusion'
94
  | 'learning_eligibility'
95
  | 'profile_delta'
96
  | 'conflict'
97
  | 'profile'
98
  | 'consent'
99
  | 'revocation'
100
  | 'project'
101
  | 'agent_profile'
102
  | 'installation'
103
  | 'setup_request'
104
  | 'publication'
105
  | 'presence';
106
107
export type { PublicAgentCard, FederatedInteraction } from '../../protocol/src/index.js';
108
109
type ContextualProfile = {
110
  recordId: string;
111
  agentId: string;
112
  agentVersion: string;
113
  taskCategory: string;
114
  sampleSize: number;
115
  effectiveSampleSize?: number;
116
  dimensionSampleSizes?: Partial<
117
    Record<
118
      | 'completion'
119
      | 'acceptance'
120
      | 'specification'
121
      | 'deadline'
122
      | 'communication'
123
      | 'evidence'
124
      | 'scope'
125
      | 'correction'
126
      | 'limitations'
127
      | 'disputes',
128
      number
129
    >
130
  >;
131
  updatedAt: string;
132
  completion: number;
133
  acceptance: number;
134
  specification: number;
135
  deadline: number;
136
  communication: number;
137
  evidence: number;
138
  scope: number;
139
  correction: number;
140
  limitations: number;
141
  disputes: number;
142
};
143
144
const FEEDBACK_DIMENSIONS = FeedbackDimensionSchema.options;
145
146
function feedbackWindowMilliseconds() {
147
  const configured = Number(process.env.OPENCLASP_FEEDBACK_WINDOW_MINUTES ?? 120);
148
  const minutes = Number.isFinite(configured) ? Math.max(15, Math.min(1440, configured)) : 120;
149
  return minutes * 60_000;
150
}
151
152
function reviewerCredibility(feedback: InteractionFeedback): number {
153
  const provenance =
154
    feedback.submissionMethod === 'agent_signature'
155
      ? 0.9
156
      : feedback.submissionMethod === 'runtime_session'
157
        ? 0.8
158
        : feedback.submissionMethod === 'agent_access_token'
159
          ? 0.75
160
          : 0.65;
161
  return Math.min(1, provenance + (feedback.evidenceReferences.length ? 0.1 : 0));
162
}
163
164
function deterministicUuid(value: string): string {
165
  const bytes = createHash('sha256').update(value).digest().subarray(0, 16);
166
  bytes[6] = (bytes[6]! & 0x0f) | 0x50;
167
  bytes[8] = (bytes[8]! & 0x3f) | 0x80;
168
  const hex = bytes.toString('hex');
169
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
170
}
171
172
function normalizePublicAgentCard(value: unknown): PublicAgentCard {
173
  const current = PublicAgentCardSchema.safeParse(value);
174
  const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
175
    /\/$/,
176
    '',
177
  );
178
  if (current.success) {
179
    const slug = current.data.slug ?? publicAgentSlug(current.data.name, current.data.agentId);
180
    return PublicAgentCardSchema.parse({
181
      ...current.data,
182
      slug,
183
      profileUrl: `${baseUrl}/a/${encodeURIComponent(slug)}`,
184
      cardUrl: `${baseUrl}/agents/${encodeURIComponent(current.data.agentId)}/card.json`,
185
      a2aAgentCardUrl: `${baseUrl}/agents/${encodeURIComponent(current.data.agentId)}/a2a-agent-card.json`,
186
      extensionUri: current.data.extensionUri ?? DEFAULT_EXTENSION_URI,
187
      verification: {
188
        ...(current.data.verification ?? {
189
          status: 'verified',
190
          method: 'openclasp_oauth_account',
191
          verifiedAt: current.data.publishedAt,
192
        }),
193
        verificationKeyUrl: `${baseUrl}/.well-known/openclasp-session-key`,
194
      },
195
    });
196
  }
197
  const legacy = value as Record<string, unknown>;
198
  const agentId = String(legacy.agentId ?? '');
199
  const slug = publicAgentSlug(String(legacy.name ?? agentId), agentId);
200
  return PublicAgentCardSchema.parse({
201
    protocolVersion: '0.1',
202
    agentId,
203
    name: legacy.name,
204
    description: '',
205
    framework: legacy.framework,
206
    agentVersion: '1.0.0',
207
    capabilities: legacy.capabilities,
208
    limitations: legacy.limitations,
209
    assurance: legacy.assurance,
210
    transports:
211
      typeof legacy.a2aEndpoint === 'string'
212
        ? [
213
            {
214
              protocol: 'A2A/1.0',
215
              protocolBinding: 'JSONRPC',
216
              endpoint: legacy.a2aEndpoint,
217
              managedBy: 'agent',
218
            },
219
          ]
220
        : [],
221
    slug,
222
    profileUrl: `${baseUrl}/a/${encodeURIComponent(slug)}`,
223
    cardUrl: `${baseUrl}/agents/${encodeURIComponent(agentId)}/card.json`,
224
    a2aAgentCardUrl: `${baseUrl}/agents/${encodeURIComponent(agentId)}/a2a-agent-card.json`,
225
    extensionUri: DEFAULT_EXTENSION_URI,
226
    verification: {
227
      status: 'verified',
228
      method: 'openclasp_oauth_account',
229
      verifiedAt: legacy.publishedAt,
230
      verificationKeyUrl: `${baseUrl}/.well-known/openclasp-session-key`,
231
    },
232
    publishedAt: legacy.publishedAt,
233
    updatedAt: legacy.updatedAt,
234
  });
235
}
236
237
function publicAgentSlug(name: string, agentId: string) {
238
  const prefix =
239
    name
240
      .trim()
241
      .toLowerCase()
242
      .replace(/[^a-z0-9]+/g, '-')
243
      .replace(/^-|-$/g, '')
244
      .slice(0, 48) || 'agent';
245
  const suffix = createHash('sha256').update(agentId).digest('hex').slice(0, 8);
246
  return `${prefix}-${suffix}`;
247
}
248
249
export function buildPublicAgentCard(
250
  agent: AgentProfile,
251
  baseUrl: string,
252
  previous?: PublicAgentCard,
253
): PublicAgentCard {
254
  const now = new Date().toISOString();
255
  const root = baseUrl.replace(/\/$/, '');
256
  const slug = previous?.slug ?? publicAgentSlug(agent.name, agent.agentId);
257
  return PublicAgentCardSchema.parse({
258
    protocolVersion: '0.1',
259
    agentId: agent.agentId,
260
    name: agent.name,
261
    description: agent.description ?? '',
262
    framework: agent.framework,
263
    agentVersion: agent.agentVersion ?? '1.0.0',
264
    capabilities: agent.capabilities,
265
    limitations: agent.limitations,
266
    assurance: 'oauth_authenticated',
267
    agentMode: agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat'),
268
    transports:
269
      (agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat')) ===
270
      'temporary_chat'
271
        ? [
272
            {
273
              protocol: 'A2A/1.0',
274
              protocolBinding: 'JSONRPC',
275
              endpoint: `${root}/a2a/temporary/${encodeURIComponent(agent.agentId)}`,
276
              managedBy: 'openclasp',
277
            },
278
          ]
279
        : agent.a2aEndpoint
280
          ? [
281
              {
282
                protocol: 'A2A/1.0',
283
                protocolBinding: 'JSONRPC',
284
                endpoint: agent.a2aEndpoint,
285
                managedBy: 'agent',
286
              },
287
            ]
288
          : [],
289
    slug,
290
    profileUrl: `${root}/a/${encodeURIComponent(slug)}`,
291
    cardUrl: `${root}/agents/${encodeURIComponent(agent.agentId)}/card.json`,
292
    a2aAgentCardUrl: `${root}/agents/${encodeURIComponent(agent.agentId)}/a2a-agent-card.json`,
293
    extensionUri: DEFAULT_EXTENSION_URI,
294
    verification: {
295
      status: 'verified',
296
      method: 'openclasp_oauth_account',
297
      verifiedAt: previous?.verification?.verifiedAt ?? previous?.publishedAt ?? now,
298
      verificationKeyUrl: `${root}/.well-known/openclasp-session-key`,
299
    },
300
    publishedAt: previous?.publishedAt ?? now,
301
    updatedAt: now,
302
  });
303
}
304
305
const normalized = (value: string) =>
306
  value
307
    .trim()
308
    .toLowerCase()
309
    .replace(/[^a-z0-9]+/g, '_');
310
311
export const AGENT_ONLINE_WINDOW_MS = 2 * 60_000;
312
313
export function resolveAgentPresence(lastSeenAt?: string, now = new Date()): AgentPresence {
314
  const online = Boolean(
315
    lastSeenAt && now.getTime() - Date.parse(lastSeenAt) <= AGENT_ONLINE_WINDOW_MS,
316
  );
317
  return {
318
    status: online ? 'online' : 'offline',
319
    ...(lastSeenAt ? { lastSeenAt } : {}),
320
    checkedAt: now.toISOString(),
321
  };
322
}
323
324
export function canAutoAcceptInteraction(
325
  agent: AgentProfile,
326
  interaction: FederatedInteraction,
327
): boolean {
328
  if (agent.autoAcceptPolicy !== 'safe_matching' || agent.status !== 'active') return false;
329
  const categories = new Set(
330
    [...(agent.autoAcceptTaskCategories ?? []), ...(agent.capabilities ?? [])].map(normalized),
331
  );
332
  const taskCategory = normalized(interaction.contract.taskCategory);
333
  if (!taskCategory || !categories.has(taskCategory)) return false;
334
  if (interaction.contract.allowedData.length > 0) return false;
335
  if (
336
    /\b(password|secret|api[_ -]?key|access[_ -]?token|credential|bank|payment|medical|health|ssn|personal[_ -]?data|customer[_ -]?record)\b/i.test(
337
      `${interaction.contract.purpose} ${interaction.contract.requestedOutcome}`,
338
    )
339
  )
340
    return false;
341
  if (interaction.contract.humanApprovalRequirements.length > 0) return false;
342
  if (interaction.contract.retentionDays > 30) return false;
343
  if (interaction.contract.allowedActions.length > 0) {
344
    const capabilities = new Set((agent.capabilities ?? []).map(normalized));
345
    if (
346
      !interaction.contract.allowedActions.every((action) => capabilities.has(normalized(action)))
347
    )
348
      return false;
349
  }
350
  return true;
351
}
352
353
function withContractRevisionHistory(value: FederatedInteraction): FederatedInteraction {
354
  if (value.contractRevisions.length) return value;
355
  const status =
356
    value.status === 'pending'
357
      ? 'proposed'
358
      : ['active', 'completed'].includes(value.status)
359
        ? 'accepted'
360
        : 'rejected';
361
  const revision = ContractRevisionSchema.parse({
362
    revisionId: deterministicUuid(`${value.interactionId}:contract:1:${value.termsHash}`),
363
    interactionId: value.interactionId,
364
    revision: value.contractRevision,
365
    termsHash: value.termsHash,
366
    contract: value.contract,
367
    proposedByAgentId: value.initiatorAgentId,
368
    status,
369
    acceptances: value.acceptances,
370
    createdAt: value.createdAt,
371
    updatedAt: value.updatedAt,
372
    expiresAt: value.expiresAt,
373
  });
374
  return FederatedInteractionSchema.parse({ ...value, contractRevisions: [revision] });
375
}
376
377
function openContractRevision(value: FederatedInteraction): ContractRevision | undefined {
378
  return [...value.contractRevisions].reverse().find((revision) => revision.status === 'proposed');
379
}
380
381
export type AccountSettings = {
382
  displayName: string;
383
  contributionEnabled: boolean;
384
  retentionDays: number;
385
  evidenceSharing: 'never' | 'ask' | 'contract_only';
386
  rawConversationsStored: false;
387
};
388
389
const defaults: AccountSettings = {
390
  displayName: '',
391
  contributionEnabled: false,
392
  retentionDays: 30,
393
  evidenceSharing: 'ask',
394
  rawConversationsStored: false,
395
};
396
397
export class HostedRepository {
398
  private readonly sql: NeonQueryFunction<false, false>;
399
  private initialized?: Promise<void>;
400
401
  constructor(databaseUrl: string) {
402
    this.sql = neon(databaseUrl);
403
  }
404
405
  private gatewaySecret() {
406
    const value = process.env.OPENCLASP_RELAY_ENCRYPTION_KEY;
407
    if (!value || value.length < 32) throw new Error('Gateway encryption is not configured');
408
    return value;
409
  }
410
411
  private attestPublicAgentCard(value: PublicAgentCard): PublicAgentCard {
412
    const unsigned = { ...value };
413
    delete unsigned.platformAttestation;
414
    return PublicAgentCardSchema.parse({
415
      ...unsigned,
416
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unsigned),
417
    });
418
  }
419
420
  ensureSchema(): Promise<void> {
421
    return (this.initialized ??= verifyHostedMigrations(this.sql));
422
  }
423
424
  private async appendSourceRecord(
425
    operatorId: string,
426
    kind: string,
427
    recordId: string,
428
    payload: unknown,
429
    metadata?: SourceRecordWriteMetadata,
430
  ) {
431
    const source = buildSourceRecordEnvelope({
432
      operatorId,
433
      kind,
434
      recordId,
435
      payload,
436
      ...(metadata ? { metadata } : {}),
437
    });
438
    await this.sql`
439
      INSERT INTO openclasp_source_records(
440
        event_id, operator_id, kind, record_id, schema_name, schema_version, payload,
441
        payload_digest, entity_refs, provenance, visibility, retention_class, learning_scope,
442
        reported_at, ingested_at
443
      ) VALUES (
444
        ${source.eventId}, ${source.operatorId}, ${source.kind}, ${source.recordId},
445
        ${source.schemaName}, ${source.schemaVersion}, ${JSON.stringify(source.payload)}::jsonb,
446
        ${source.payloadDigest}, ${JSON.stringify(source.entityRefs)}::jsonb,
447
        ${source.provenance}, ${source.visibility}, ${source.retentionClass},
448
        ${source.learningScope}, ${source.reportedAt}, ${source.ingestedAt}
449
      )
450
      ON CONFLICT (operator_id, kind, record_id, payload_digest) DO NOTHING
451
    `;
452
  }
453
454
  private async interactionOperatorIds(interactionId: string): Promise<string[]> {
455
    const rows = await this.sql`
456
      SELECT initiator_operator_id, responder_operator_id
457
      FROM openclasp_federated_interactions
458
      WHERE interaction_id = ${interactionId}
459
      LIMIT 1
460
    `;
461
    if (!rows[0]) return [];
462
    return [
463
      ...new Set([String(rows[0].initiator_operator_id), String(rows[0].responder_operator_id)]),
464
    ];
465
  }
466
467
  private async journalFederatedInteraction(interactionId: string) {
468
    const rows = await this.sql`
469
      SELECT payload, initiator_operator_id, responder_operator_id
470
      FROM openclasp_federated_interactions
471
      WHERE interaction_id = ${interactionId}
472
      LIMIT 1
473
    `;
474
    if (!rows[0]) return;
475
    const payload = FederatedInteractionSchema.parse(rows[0].payload);
476
    const operatorIds = [
477
      ...new Set([String(rows[0].initiator_operator_id), String(rows[0].responder_operator_id)]),
478
    ];
479
    await Promise.all(
480
      operatorIds.map((operatorId) =>
481
        this.appendSourceRecord(operatorId, 'federated_interaction', interactionId, payload, {
482
          schemaName: 'openclasp.federated_interaction',
483
          schemaVersion: '0.1',
484
          visibility: 'shared_participants',
485
          retentionClass: 'audit',
486
        }),
487
      ),
488
    );
489
  }
490
491
  private async journalLiveSessionState(interactionId: string) {
492
    const rows = await this.sql`
493
      SELECT session.interaction_id, session.initiator_agent_id, session.responder_agent_id,
494
        session.status, session.expires_at, session.created_at, session.activated_at,
495
        session.completed_at, interaction.initiator_operator_id,
496
        interaction.responder_operator_id
497
      FROM openclasp_live_sessions session
498
      INNER JOIN openclasp_federated_interactions interaction
499
        ON interaction.interaction_id = session.interaction_id
500
      WHERE session.interaction_id = ${interactionId}
501
      LIMIT 1
502
    `;
503
    if (!rows[0]) return;
504
    const row = rows[0];
505
    const payload = LiveSessionStateRecordSchema.parse({
506
      interactionId: String(row.interaction_id),
507
      initiatorAgentId: String(row.initiator_agent_id),
508
      responderAgentId: String(row.responder_agent_id),
509
      status: String(row.status),
510
      expiresAt: new Date(String(row.expires_at)).toISOString(),
511
      createdAt: new Date(String(row.created_at)).toISOString(),
512
      ...(row.activated_at
513
        ? { activatedAt: new Date(String(row.activated_at)).toISOString() }
514
        : {}),
515
      ...(row.completed_at
516
        ? { completedAt: new Date(String(row.completed_at)).toISOString() }
517
        : {}),
518
      ...(row.status === 'failed' ? { failureCode: 'session_failed' } : {}),
519
    });
520
    const operatorIds = [
521
      ...new Set([String(row.initiator_operator_id), String(row.responder_operator_id)]),
522
    ];
523
    await Promise.all(
524
      operatorIds.map((operatorId) =>
525
        this.appendSourceRecord(operatorId, 'live_session_state', interactionId, payload, {
526
          schemaName: 'openclasp.live_session_state',
527
          schemaVersion: '0.1',
528
          visibility: 'shared_participants',
529
          retentionClass: 'audit',
530
          reportedAt: payload.completedAt ?? payload.activatedAt ?? payload.createdAt,
531
        }),
532
      ),
533
    );
534
  }
535
536
  private async journalLiveSessionEvent(payload: Record<string, unknown>) {
537
    const interactionId = String(payload.interactionId ?? '');
538
    const eventId = String(payload.eventId ?? '');
539
    if (!interactionId || !eventId) return;
540
    const operatorIds = await this.interactionOperatorIds(interactionId);
541
    await Promise.all(
542
      operatorIds.map((operatorId) =>
543
        this.appendSourceRecord(operatorId, 'live_session_event', eventId, payload, {
544
          schemaName: 'openclasp.live_session_event',
545
          schemaVersion: '0.1',
546
          visibility: 'shared_participants',
547
          retentionClass: 'audit',
548
        }),
549
      ),
550
    );
551
  }
552
553
  getRuntimeVerificationKey() {
554
    return {
555
      algorithm: 'Ed25519' as const,
556
      keyId: getSessionKeyId(this.gatewaySecret()),
557
      publicKey: getSessionVerificationKey(this.gatewaySecret()),
558
    };
559
  }
560
561
  async upsert(
562
    operatorId: string,
563
    kind: HostedRecordKind,
564
    recordId: string,
565
    payload: unknown,
566
    metadata?: SourceRecordWriteMetadata,
567
  ): Promise<void> {
568
    await this.ensureSchema();
569
    const encoded = JSON.stringify(payload);
570
    if (!(metadata?.journal ?? shouldJournalSourceRecord(kind))) {
571
      await this.sql`
572
        INSERT INTO openclasp_records(operator_id, kind, record_id, payload)
573
        VALUES (${operatorId}, ${kind}, ${recordId}, ${encoded}::jsonb)
574
        ON CONFLICT (operator_id, kind, record_id)
575
        DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()
576
      `;
577
      return;
578
    }
579
    const source = buildSourceRecordEnvelope({
580
      operatorId,
581
      kind,
582
      recordId,
583
      payload,
584
      ...(metadata ? { metadata } : {}),
585
    });
586
    const sourcePayload = JSON.stringify(source.payload);
587
    const sourceEntityRefs = JSON.stringify(source.entityRefs);
588
    await this.sql.transaction((transaction) => [
589
      transaction`
590
        INSERT INTO openclasp_records(operator_id, kind, record_id, payload)
591
        VALUES (${operatorId}, ${kind}, ${recordId}, ${encoded}::jsonb)
592
        ON CONFLICT (operator_id, kind, record_id)
593
        DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()
594
      `,
595
      transaction`
596
        INSERT INTO openclasp_source_records(
597
          event_id,
598
          operator_id,
599
          kind,
600
          record_id,
601
          schema_name,
602
          schema_version,
603
          payload,
604
          payload_digest,
605
          entity_refs,
606
          provenance,
607
          visibility,
608
          retention_class,
609
          learning_scope,
610
          reported_at,
611
          ingested_at
612
        ) VALUES (
613
          ${source.eventId},
614
          ${source.operatorId},
615
          ${source.kind},
616
          ${source.recordId},
617
          ${source.schemaName},
618
          ${source.schemaVersion},
619
          ${sourcePayload}::jsonb,
620
          ${source.payloadDigest},
621
          ${sourceEntityRefs}::jsonb,
622
          ${source.provenance},
623
          ${source.visibility},
624
          ${source.retentionClass},
625
          ${source.learningScope},
626
          ${source.reportedAt},
627
          ${source.ingestedAt}
628
        )
629
        ON CONFLICT (operator_id, kind, record_id, payload_digest) DO NOTHING
630
      `,
631
    ]);
632
  }
633
634
  async list(
635
    operatorId: string,
636
  ): Promise<{ kind: HostedRecordKind; recordId: string; payload: any }[]> {
637
    await this.ensureSchema();
638
    const rows = await this.sql`
639
      SELECT kind, record_id, payload
640
      FROM openclasp_records
641
      WHERE operator_id = ${operatorId}
642
      ORDER BY created_at ASC
643
    `;
644
    return rows.map((row) => ({
645
      kind: row.kind as HostedRecordKind,
646
      recordId: String(row.record_id),
647
      payload: row.payload,
648
    }));
649
  }
650
651
  async dashboard(operatorId: string) {
652
    await this.ensureSchema();
653
    const dueFeedback = await this.sql`
654
      SELECT 1 FROM openclasp_records
655
      WHERE operator_id = ${operatorId}
656
        AND kind = 'feedback_request'
657
        AND payload->>'status' = 'pending'
658
        AND (payload->>'dueAt')::timestamptz <= NOW()
659
      LIMIT 1
660
    `;
661
    if (dueFeedback.length) await this.processDueFeedback().catch(() => undefined);
662
    const outstanding = await this.sql`
663
      SELECT DISTINCT report.payload->>'interactionId' AS interaction_id
664
      FROM openclasp_records report
665
      INNER JOIN openclasp_federated_interactions interaction
666
        ON interaction.interaction_id::text = report.payload->>'interactionId'
667
      WHERE report.kind = 'completion_report'
668
        AND (
669
          interaction.initiator_operator_id = ${operatorId}
670
          OR interaction.responder_operator_id = ${operatorId}
671
        )
672
        AND NOT EXISTS (
673
          SELECT 1 FROM openclasp_records conclusion
674
          WHERE conclusion.operator_id = ${operatorId}
675
            AND conclusion.kind = 'interaction_conclusion'
676
            AND conclusion.payload->>'interactionId' = report.payload->>'interactionId'
677
        )
678
      LIMIT 20
679
    `;
680
    for (const row of outstanding) {
681
      const interactionId = String(row.interaction_id ?? '');
682
      if (interactionId)
683
        await this.finalizeInteractionConclusion(interactionId).catch(() => undefined);
684
    }
685
    const intelligenceBackfill = await this.sql`
686
      SELECT DISTINCT interaction.interaction_id
687
      FROM openclasp_federated_interactions interaction
688
      INNER JOIN openclasp_records conclusion
689
        ON conclusion.payload->>'interactionId' = interaction.interaction_id::text
690
        AND conclusion.kind = 'interaction_conclusion'
691
        AND conclusion.payload->>'lifecycle' = 'final'
692
      WHERE (
693
          interaction.initiator_operator_id = ${operatorId}
694
          OR interaction.responder_operator_id = ${operatorId}
695
        )
696
        AND EXISTS (
697
          SELECT 1 FROM openclasp_records eligibility
698
          WHERE eligibility.kind = 'learning_eligibility'
699
            AND eligibility.payload->>'interactionId' = interaction.interaction_id::text
700
            AND eligibility.payload->>'eligible' = 'true'
701
        )
702
        AND NOT EXISTS (
703
          SELECT 1 FROM openclasp_records delta
704
          WHERE delta.operator_id = ${operatorId}
705
            AND delta.kind = 'profile_delta'
706
            AND delta.payload->>'interactionId' = interaction.interaction_id::text
707
            AND delta.payload->>'agentId' = CASE
708
              WHEN interaction.initiator_operator_id = ${operatorId}
709
                THEN interaction.initiator_agent_id::text
710
              ELSE interaction.responder_agent_id::text
711
            END
712
        )
713
      ORDER BY interaction.interaction_id DESC
714
      LIMIT 20
715
    `;
716
    for (const row of intelligenceBackfill) {
717
      const interactionId = String(row.interaction_id ?? '');
718
      if (interactionId)
719
        await this.finalizeInteractionConclusion(interactionId).catch(() => undefined);
720
    }
721
    const rows = await this.list(operatorId);
722
    const [federatedInteractions, runtimes, accessTokens, liveSessionRows, liveEventRows] =
723
      await Promise.all([
724
        this.listFederatedInteractions(operatorId),
725
        this.listAgentRuntimes(operatorId),
726
        this.listAgentAccessTokens(operatorId),
727
        this.sql`
728
        SELECT session.interaction_id, session.initiator_agent_id, session.responder_agent_id,
729
          session.status, session.created_at, session.activated_at, session.completed_at,
730
          session.expires_at, session.last_error
731
        FROM openclasp_live_sessions session
732
        INNER JOIN openclasp_federated_interactions interaction
733
          ON interaction.interaction_id = session.interaction_id
734
        WHERE interaction.initiator_operator_id = ${operatorId}
735
           OR interaction.responder_operator_id = ${operatorId}
736
        ORDER BY session.created_at DESC
737
      `,
738
        this.sql`
739
        SELECT events.event
740
        FROM openclasp_live_session_events events
741
        INNER JOIN openclasp_federated_interactions interaction
742
          ON interaction.interaction_id = events.interaction_id
743
        WHERE interaction.initiator_operator_id = ${operatorId}
744
           OR interaction.responder_operator_id = ${operatorId}
745
        ORDER BY events.created_at ASC
746
      `,
747
      ]);
748
    const ofKind = (kind: HostedRecordKind) =>
749
      rows.filter((row) => row.kind === kind).map((row) => row.payload);
750
    const agentProfiles = ofKind('agent_profile') as AgentProfile[];
751
    const hostedThreads = (
752
      await Promise.all(
753
        agentProfiles
754
          .filter(
755
            (agent) =>
756
              (agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat')) ===
757
              'temporary_chat',
758
          )
759
          .map((agent) => this.listHostedThreads(operatorId, agent.agentId)),
760
      )
761
    ).flat();
762
    const presence = new Map(
763
      rows
764
        .filter((row) => row.kind === 'presence')
765
        .map((row) => [row.recordId, String(row.payload.lastSeenAt)]),
766
    );
767
    const intelligenceSummaries = await this.listContextualIntelligence(operatorId);
768
    return {
769
      agents: [...ofKind('agent_profile'), ...ofKind('agent')].map((agent) => ({
770
        ...agent,
771
        presence: resolveAgentPresence(presence.get(String(agent.agentId))),
772
      })),
773
      projects: ofKind('project'),
774
      installations: ofKind('installation'),
775
      setupRequests: ofKind('setup_request'),
776
      publications: ofKind('publication'),
777
      interactions: ofKind('interaction'),
778
      events: [...ofKind('event'), ...liveEventRows.map((row) => row.event)],
779
      conflicts: ofKind('conflict'),
780
      receipts: ofKind('receipt'),
781
      profiles: ofKind('profile'),
782
      counterpartyBriefs: ofKind('counterparty_brief'),
783
      completionReports: ofKind('completion_report'),
784
      feedbackRequests: ofKind('feedback_request'),
785
      interactionFeedback: ofKind('interaction_feedback'),
786
      interactionConclusions: ofKind('interaction_conclusion'),
787
      learningEligibility: ofKind('learning_eligibility'),
788
      profileDeltas: ofKind('profile_delta'),
789
      intelligenceSummaries,
790
      federatedInteractions,
791
      liveSessions: liveSessionRows.map((row) => ({
792
        interactionId: String(row.interaction_id),
793
        initiatorAgentId: String(row.initiator_agent_id),
794
        responderAgentId: String(row.responder_agent_id),
795
        status: String(row.status),
796
        createdAt: new Date(String(row.created_at)).toISOString(),
797
        ...(row.activated_at
798
          ? { activatedAt: new Date(String(row.activated_at)).toISOString() }
799
          : {}),
800
        ...(row.completed_at
801
          ? { completedAt: new Date(String(row.completed_at)).toISOString() }
802
          : {}),
803
        expiresAt: new Date(String(row.expires_at)).toISOString(),
804
        ...(row.last_error ? { lastError: String(row.last_error) } : {}),
805
      })),
806
      hostedThreads,
807
      runtimes,
808
      accessTokens,
809
    };
810
  }
811
812
  async listContextualIntelligence(
813
    operatorId: string,
814
    input: { agentId?: string; taskCategory?: string } = {},
815
  ) {
816
    await this.ensureSchema();
817
    const [records, publicRows] = await Promise.all([
818
      this.list(operatorId),
819
      this
820
        .sql`SELECT agent_id, card FROM openclasp_public_agents ORDER BY updated_at DESC LIMIT 1000`,
821
    ]);
822
    const currentVersions = new Map<string, string>();
823
    for (const row of publicRows) {
824
      const card = PublicAgentCardSchema.safeParse(normalizePublicAgentCard(row.card));
825
      if (card.success) currentVersions.set(String(row.agent_id), card.data.agentVersion);
826
    }
827
    for (const record of records.filter((item) => item.kind === 'agent_profile')) {
828
      const profile = record.payload as Partial<AgentProfile>;
829
      if (typeof profile.agentId === 'string' && typeof profile.agentVersion === 'string')
830
        currentVersions.set(profile.agentId, profile.agentVersion);
831
    }
832
    const deltas = records
833
      .filter((record) => record.kind === 'profile_delta')
834
      .map((record) => BehaviouralProfileDeltaSchema.safeParse(record.payload))
835
      .filter((result) => result.success)
836
      .map((result) => result.data);
837
    return records
838
      .filter((record) => record.kind === 'profile')
839
      .map((record) => ({ recordId: record.recordId, ...record.payload }) as ContextualProfile)
840
      .filter(
841
        (profile) =>
842
          (!input.agentId || profile.agentId === input.agentId) &&
843
          (!input.taskCategory || profile.taskCategory === input.taskCategory),
844
      )
845
      .map((profile) =>
846
        summarizeContextualReliability({
847
          profile: {
848
            ...profile,
849
            effectiveSampleSize: profile.effectiveSampleSize ?? profile.sampleSize,
850
          },
851
          deltas: deltas.filter(
852
            (delta) =>
853
              delta.agentId === profile.agentId &&
854
              delta.agentVersion === profile.agentVersion &&
855
              delta.taskCategory === profile.taskCategory,
856
          ),
857
          currentAgentVersion: currentVersions.get(profile.agentId) ?? profile.agentVersion,
858
        }),
859
      )
860
      .sort((left, right) => {
861
        if (left.agentId !== right.agentId) return left.agentId.localeCompare(right.agentId);
862
        if (left.versionStatus.status !== right.versionStatus.status)
863
          return left.versionStatus.status === 'current' ? -1 : 1;
864
        return right.confidence.value - left.confidence.value;
865
      });
866
  }
867
868
  async issueAgentAccessToken(
869
    operatorId: string,
870
    agentId: string,
871
    input: { name: string; expiresInDays: number },
872
  ): Promise<AgentAccessTokenMetadata & { token: string }> {
873
    await this.ensureSchema();
874
    const agents = await this.sql`
875
      SELECT payload FROM openclasp_records
876
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
877
      LIMIT 1
878
    `;
879
    const agent = agents[0]?.payload as AgentProfile | undefined;
880
    if (!agent) throw new Error('Owned agent not found');
881
    if (agent.status !== 'active') throw new Error('Revoked agents cannot receive access tokens');
882
    const name = input.name.trim();
883
    if (!name) throw new Error('Token name is required');
884
    const { tokenId, token, tokenHash } = createAgentAccessToken();
885
    const createdAt = new Date();
886
    const expiresAt = new Date(createdAt.getTime() + input.expiresInDays * 86_400_000);
887
    // The credential is bound to one agent. It may use MCP outbound and let that
888
    // same agent register its own inbound runtime; it cannot manage other agents.
889
    const scopes = ['mcp:access', 'runtime:connect'];
890
    await this.sql`
891
      INSERT INTO openclasp_agent_access_tokens(
892
        token_id, operator_id, agent_id, name, token_hash, scopes, expires_at, created_at
893
      ) VALUES (
894
        ${tokenId}, ${operatorId}, ${agentId}, ${name}, ${tokenHash},
895
        ${JSON.stringify(scopes)}::jsonb, ${expiresAt.toISOString()}, ${createdAt.toISOString()}
896
      )
897
    `;
898
    const clientId = agentAccessTokenClientId(tokenId);
899
    await this.upsert(operatorId, 'installation', clientId, {
900
      installationId: `install_${tokenId}`,
901
      clientId,
902
      agentId,
903
      projectId: agent.projectId,
904
      connectedAt: createdAt.toISOString(),
905
      updatedAt: createdAt.toISOString(),
906
    } satisfies AgentInstallation);
907
    return {
908
      tokenId,
909
      token,
910
      agentId,
911
      name,
912
      scopes,
913
      createdAt: createdAt.toISOString(),
914
      expiresAt: expiresAt.toISOString(),
915
    };
916
  }
917
918
  async listAgentAccessTokens(
919
    operatorId: string,
920
    agentId?: string,
921
  ): Promise<AgentAccessTokenMetadata[]> {
922
    await this.ensureSchema();
923
    const rows = agentId
924
      ? await this.sql`
925
          SELECT token_id, agent_id, name, scopes, expires_at, last_used_at, revoked_at, created_at
926
          FROM openclasp_agent_access_tokens
927
          WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
928
          ORDER BY created_at DESC
929
        `
930
      : await this.sql`
931
          SELECT token_id, agent_id, name, scopes, expires_at, last_used_at, revoked_at, created_at
932
          FROM openclasp_agent_access_tokens
933
          WHERE operator_id = ${operatorId}
934
          ORDER BY created_at DESC
935
        `;
936
    return rows.map((row) => ({
937
      tokenId: String(row.token_id),
938
      agentId: String(row.agent_id),
939
      name: String(row.name),
940
      scopes: Array.isArray(row.scopes)
941
        ? row.scopes.filter((scope): scope is string => typeof scope === 'string')
942
        : [],
943
      createdAt: new Date(String(row.created_at)).toISOString(),
944
      expiresAt: new Date(String(row.expires_at)).toISOString(),
945
      ...(row.last_used_at ? { lastUsedAt: new Date(String(row.last_used_at)).toISOString() } : {}),
946
      ...(row.revoked_at ? { revokedAt: new Date(String(row.revoked_at)).toISOString() } : {}),
947
    }));
948
  }
949
950
  async revokeAgentAccessToken(operatorId: string, agentId: string, tokenId: string) {
951
    await this.ensureSchema();
952
    const rows = await this.sql`
953
      UPDATE openclasp_agent_access_tokens
954
      SET revoked_at = COALESCE(revoked_at, NOW())
955
      WHERE token_id = ${tokenId} AND operator_id = ${operatorId} AND agent_id = ${agentId}
956
      RETURNING revoked_at
957
    `;
958
    if (!rows.length) throw new Error('Agent access token not found');
959
    const clientId = agentAccessTokenClientId(tokenId);
960
    await this.sql`
961
      DELETE FROM openclasp_records
962
      WHERE operator_id = ${operatorId} AND kind = 'installation' AND record_id = ${clientId}
963
    `;
964
    return {
965
      tokenId,
966
      agentId,
967
      revokedAt: new Date(String(rows[0]?.revoked_at)).toISOString(),
968
    };
969
  }
970
971
  async verifyAgentAccessToken(token: string) {
972
    await this.ensureSchema();
973
    const tokenId = agentAccessTokenId(token);
974
    if (!tokenId) throw new Error('Invalid agent access token');
975
    const rows = await this.sql`
976
      SELECT tokens.operator_id, tokens.agent_id, tokens.token_hash, tokens.scopes,
977
        tokens.expires_at, profile.payload AS profile
978
      FROM openclasp_agent_access_tokens tokens
979
      INNER JOIN openclasp_records profile
980
        ON profile.operator_id = tokens.operator_id
981
        AND profile.kind = 'agent_profile'
982
        AND profile.record_id = tokens.agent_id
983
      WHERE tokens.token_id = ${tokenId} AND tokens.revoked_at IS NULL
984
      LIMIT 1
985
    `;
986
    const row = rows[0];
987
    if (!row || !matchesAgentAccessToken(token, String(row.token_hash)))
988
      throw new Error('Invalid agent access token');
989
    if (Date.parse(String(row.expires_at)) <= Date.now())
990
      throw new Error('Agent access token has expired');
991
    const profile = row.profile as Partial<AgentProfile>;
992
    if (profile.status !== 'active') throw new Error('Agent access token is disabled');
993
    await this.sql`
994
      UPDATE openclasp_agent_access_tokens
995
      SET last_used_at = NOW()
996
      WHERE token_id = ${tokenId}
997
        AND (last_used_at IS NULL OR last_used_at < NOW() - INTERVAL '5 minutes')
998
    `;
999
    return {
1000
      tokenId,
1001
      operatorId: String(row.operator_id),
1002
      agentId: String(row.agent_id),
1003
      clientId: agentAccessTokenClientId(tokenId),
1004
      scopes: Array.isArray(row.scopes)
1005
        ? row.scopes.filter((scope): scope is string => typeof scope === 'string')
1006
        : [],
1007
      expiresAt: new Date(String(row.expires_at)).toISOString(),
1008
    };
1009
  }
1010
1011
  async publishAgent(operatorId: string, card: PublicAgentCard): Promise<PublicAgentCard> {
1012
    await this.ensureSchema();
1013
    card = PublicAgentCardSchema.parse(card);
1014
    const runtimes = await this.sql`
1015
      SELECT a2a_endpoint, endpoint FROM openclasp_agent_runtimes
1016
      WHERE operator_id = ${operatorId} AND agent_id = ${card.agentId} AND status = 'verified'
1017
    `;
1018
    if (runtimes[0]) {
1019
      card = PublicAgentCardSchema.parse({
1020
        ...card,
1021
        agentMode: 'persistent_runtime',
1022
        transports: [
1023
          {
1024
            protocol: 'A2A/1.0',
1025
            protocolBinding: 'JSONRPC',
1026
            endpoint: String(runtimes[0].a2a_endpoint ?? runtimes[0].endpoint),
1027
            managedBy: 'agent',
1028
          },
1029
        ],
1030
      });
1031
    }
1032
    card = this.attestPublicAgentCard(card);
1033
    const encoded = JSON.stringify(card);
1034
    const rows = await this.sql`
1035
      INSERT INTO openclasp_public_agents(agent_id, operator_id, card, published_at, updated_at)
1036
      VALUES (${card.agentId}, ${operatorId}, ${encoded}::jsonb, NOW(), NOW())
1037
      ON CONFLICT (agent_id) DO UPDATE SET
1038
        card = EXCLUDED.card,
1039
        updated_at = NOW()
1040
      WHERE openclasp_public_agents.operator_id = ${operatorId}
1041
      RETURNING card
1042
    `;
1043
    const published = rows[0]?.card as PublicAgentCard | undefined;
1044
    if (!published) throw new Error('Agent ID is already owned by another operator');
1045
    return PublicAgentCardSchema.parse(published);
1046
  }
1047
1048
  async unpublishAgent(operatorId: string, agentId: string): Promise<boolean> {
1049
    await this.ensureSchema();
1050
    const rows = await this.sql`
1051
      DELETE FROM openclasp_public_agents
1052
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
1053
      RETURNING agent_id
1054
    `;
1055
    return rows.length > 0;
1056
  }
1057
1058
  async getPublishedAgent(agentId: string): Promise<PublicAgentCard | undefined> {
1059
    await this.ensureSchema();
1060
    const rows = await this.sql`
1061
      SELECT agents.card, runtime.a2a_endpoint, runtime.endpoint, profile.payload AS profile
1062
      FROM openclasp_public_agents agents
1063
      LEFT JOIN openclasp_agent_runtimes runtime
1064
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
1065
      LEFT JOIN openclasp_records profile
1066
        ON profile.operator_id = agents.operator_id AND profile.kind = 'agent_profile'
1067
        AND profile.record_id = agents.agent_id
1068
      WHERE agents.agent_id = ${agentId}
1069
    `;
1070
    if (!rows[0]?.card) return undefined;
1071
    let card = normalizePublicAgentCard(rows[0].card);
1072
    const profile = rows[0].profile as Partial<AgentProfile> | undefined;
1073
    const mode =
1074
      profile?.agentMode ??
1075
      (rows[0].a2a_endpoint || rows[0].endpoint ? 'persistent_runtime' : 'temporary_chat');
1076
    if (mode === 'temporary_chat') {
1077
      const root = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
1078
        /\/$/,
1079
        '',
1080
      );
1081
      card = PublicAgentCardSchema.parse({
1082
        ...card,
1083
        agentMode: 'temporary_chat',
1084
        transports: [
1085
          {
1086
            protocol: 'A2A/1.0',
1087
            protocolBinding: 'JSONRPC',
1088
            endpoint: `${root}/a2a/temporary/${encodeURIComponent(agentId)}`,
1089
            managedBy: 'openclasp',
1090
          },
1091
        ],
1092
      });
1093
    } else if (rows[0].a2a_endpoint || rows[0].endpoint)
1094
      card = PublicAgentCardSchema.parse({
1095
        ...card,
1096
        agentMode: 'persistent_runtime',
1097
        transports: [
1098
          {
1099
            protocol: 'A2A/1.0',
1100
            protocolBinding: 'JSONRPC',
1101
            endpoint: String(rows[0].a2a_endpoint ?? rows[0].endpoint),
1102
            managedBy: 'agent',
1103
          },
1104
        ],
1105
      });
1106
    return this.attestPublicAgentCard({ ...card, presence: await this.getAgentPresence(agentId) });
1107
  }
1108
1109
  async resolveAgentReference(reference: string): Promise<AgentResolution | undefined> {
1110
    await this.ensureSchema();
1111
    const normalizedReference = reference.trim().replace(/\/$/, '');
1112
    if (!normalizedReference || normalizedReference.length > 2048)
1113
      throw new Error('Agent reference is invalid');
1114
    const rows = await this.sql`
1115
      SELECT agent_id, card
1116
      FROM openclasp_public_agents
1117
      WHERE agent_id = ${normalizedReference}
1118
        OR card->>'slug' = ${normalizedReference}
1119
        OR RTRIM(card->>'profileUrl', '/') = ${normalizedReference}
1120
        OR RTRIM(card->>'cardUrl', '/') = ${normalizedReference}
1121
        OR RTRIM(card->>'a2aAgentCardUrl', '/') = ${normalizedReference}
1122
      LIMIT 1
1123
    `;
1124
    let row = rows[0];
1125
    if (!row) {
1126
      const legacyRows = await this.sql`
1127
        SELECT agent_id, card
1128
        FROM openclasp_public_agents
1129
        ORDER BY updated_at DESC
1130
        LIMIT 1000
1131
      `;
1132
      row = legacyRows.find((candidate) => {
1133
        const card = normalizePublicAgentCard(candidate.card);
1134
        return [card.slug, card.profileUrl, card.cardUrl, card.a2aAgentCardUrl]
1135
          .filter((value): value is string => typeof value === 'string')
1136
          .some((value) => value.replace(/\/$/, '') === normalizedReference);
1137
      });
1138
    }
1139
    if (!row) return undefined;
1140
    const stored = normalizePublicAgentCard(row.card);
1141
    const matchedBy =
1142
      String(row.agent_id) === normalizedReference
1143
        ? 'agent_id'
1144
        : stored.slug === normalizedReference
1145
          ? 'slug'
1146
          : stored.profileUrl?.replace(/\/$/, '') === normalizedReference
1147
            ? 'profile_url'
1148
            : stored.cardUrl.replace(/\/$/, '') === normalizedReference
1149
              ? 'card_url'
1150
              : 'a2a_card_url';
1151
    const card = await this.getPublishedAgent(String(row.agent_id));
1152
    if (!card) return undefined;
1153
    return AgentResolutionSchema.parse({
1154
      reference,
1155
      matchedBy,
1156
      verified: true,
1157
      card,
1158
      resolvedAt: new Date().toISOString(),
1159
    });
1160
  }
1161
1162
  async searchPublishedAgents(input: {
1163
    query?: string | undefined;
1164
    capability?: string | undefined;
1165
    limit?: number | undefined;
1166
  }): Promise<PublicAgentCard[]> {
1167
    await this.ensureSchema();
1168
    const rows = await this.sql`
1169
      SELECT agents.card, runtime.a2a_endpoint, runtime.endpoint, profile.payload AS profile
1170
      FROM openclasp_public_agents agents
1171
      LEFT JOIN openclasp_agent_runtimes runtime
1172
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
1173
      LEFT JOIN openclasp_records profile
1174
        ON profile.operator_id = agents.operator_id AND profile.kind = 'agent_profile'
1175
        AND profile.record_id = agents.agent_id
1176
      ORDER BY agents.updated_at DESC LIMIT 100
1177
    `;
1178
    const query = input.query?.trim().toLowerCase();
1179
    const capability = input.capability?.trim().toLowerCase();
1180
    const presenceRows = await this.sql`
1181
      SELECT records.record_id, records.payload
1182
      FROM openclasp_records records
1183
      INNER JOIN openclasp_public_agents agents ON agents.agent_id = records.record_id
1184
      WHERE records.kind = 'presence'
1185
    `;
1186
    const presence = new Map(
1187
      presenceRows.map((row) => [String(row.record_id), String(row.payload.lastSeenAt)]),
1188
    );
1189
    return rows
1190
      .map((row) => {
1191
        let card = normalizePublicAgentCard(row.card);
1192
        const profile = row.profile as Partial<AgentProfile> | undefined;
1193
        const mode =
1194
          profile?.agentMode ??
1195
          (row.a2a_endpoint || row.endpoint ? 'persistent_runtime' : 'temporary_chat');
1196
        if (mode === 'temporary_chat') {
1197
          const root = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
1198
            /\/$/,
1199
            '',
1200
          );
1201
          card = PublicAgentCardSchema.parse({
1202
            ...card,
1203
            agentMode: 'temporary_chat',
1204
            transports: [
1205
              {
1206
                protocol: 'A2A/1.0',
1207
                protocolBinding: 'JSONRPC',
1208
                endpoint: `${root}/a2a/temporary/${encodeURIComponent(card.agentId)}`,
1209
                managedBy: 'openclasp',
1210
              },
1211
            ],
1212
          });
1213
        } else if (row.a2a_endpoint || row.endpoint)
1214
          card = PublicAgentCardSchema.parse({
1215
            ...card,
1216
            agentMode: 'persistent_runtime',
1217
            transports: [
1218
              {
1219
                protocol: 'A2A/1.0',
1220
                protocolBinding: 'JSONRPC',
1221
                endpoint: String(row.a2a_endpoint ?? row.endpoint),
1222
                managedBy: 'agent',
1223
              },
1224
            ],
1225
          });
1226
        return this.attestPublicAgentCard({
1227
          ...card,
1228
          presence: resolveAgentPresence(presence.get(card.agentId)),
1229
        });
1230
      })
1231
      .filter(
1232
        (card) =>
1233
          (!query ||
1234
            card.agentId.toLowerCase().includes(query) ||
1235
            card.slug?.includes(query) ||
1236
            card.name.toLowerCase().includes(query) ||
1237
            card.description.toLowerCase().includes(query) ||
1238
            card.framework.toLowerCase().includes(query) ||
1239
            card.capabilities.some((value) => value.toLowerCase().includes(query)) ||
1240
            card.limitations.some((value) => value.toLowerCase().includes(query))) &&
1241
          (!capability ||
1242
            card.capabilities.some((value) => value.toLowerCase().includes(capability))),
1243
      )
1244
      .slice(0, Math.min(Math.max(input.limit ?? 20, 1), 50));
1245
  }
1246
1247
  async searchPersonalizedMarketplace(
1248
    operatorId: string,
1249
    input: {
1250
      agentId?: string;
1251
      taskCategory?: string;
1252
      query?: string;
1253
      limit?: number;
1254
    } = {},
1255
  ) {
1256
    const [cards, records, summaries] = await Promise.all([
1257
      this.searchPublishedAgents({ query: input.query, limit: input.limit }),
1258
      this.list(operatorId),
1259
      this.listContextualIntelligence(operatorId, {
1260
        ...(input.taskCategory ? { taskCategory: input.taskCategory } : {}),
1261
      }),
1262
    ]);
1263
    const ownedAgents = records
1264
      .filter((record) => record.kind === 'agent_profile')
1265
      .map((record) => record.payload as Partial<AgentProfile>);
1266
    const ownedIds = new Set(ownedAgents.map((agent) => agent.agentId));
1267
    const selected = ownedAgents.find((agent) => agent.agentId === input.agentId);
1268
    const taskCategory = input.taskCategory ?? selected?.capabilities?.[0] ?? 'general';
1269
    const normalizedCategory = taskCategory.toLowerCase();
1270
    const summaryFor = (agentId: string) =>
1271
      summaries.find(
1272
        (summary) =>
1273
          summary.agentId === agentId &&
1274
          summary.taskCategory.toLowerCase() === normalizedCategory &&
1275
          summary.versionStatus.status === 'current',
1276
      ) ?? summaries.find((summary) => summary.agentId === agentId);
1277
    return cards
1278
      .filter((card) => !ownedIds.has(card.agentId))
1279
      .map((card) => {
1280
        const intelligence = summaryFor(card.agentId);
1281
        const capabilityMatch = card.capabilities.some((capability) =>
1282
          capability.toLowerCase().includes(normalizedCategory),
1283
        );
1284
        const online = card.presence?.status === 'online';
1285
        const fitScore = Math.min(
1286
          1,
1287
          0.25 +
1288
            (capabilityMatch ? 0.35 : 0) +
1289
            (online ? 0.1 : 0) +
1290
            (intelligence ? intelligence.score * intelligence.confidence.value * 0.3 : 0),
1291
        );
1292
        return {
1293
          card,
1294
          taskCategory,
1295
          ...(intelligence ? { contextualReliability: intelligence } : {}),
1296
          match: {
1297
            score: fitScore,
1298
            label: fitScore >= 0.72 ? 'strong' : fitScore >= 0.48 ? 'possible' : 'unproven',
1299
            reasons: [
1300
              capabilityMatch
1301
                ? `Published capability matches ${taskCategory}`
1302
                : `No explicit ${taskCategory} capability`,
1303
              intelligence
1304
                ? `${intelligence.confidence.evidenceCount} verified outcomes in private history`
1305
                : 'No verified private history for this task',
1306
              online ? 'Currently online' : 'Currently offline',
1307
            ],
1308
          },
1309
        };
1310
      })
1311
      .sort((left, right) => right.match.score - left.match.score);
1312
  }
1313
1314
  async touchAgentPresence(operatorId: string, agentId: string): Promise<AgentPresence> {
1315
    await this.ensureSchema();
1316
    const owned = await this.sql`
1317
      SELECT 1 FROM openclasp_records
1318
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1319
    `;
1320
    if (!owned.length) throw new Error('Agent is not owned by this account');
1321
    const lastSeenAt = new Date().toISOString();
1322
    await this.upsert(operatorId, 'presence', agentId, { lastSeenAt });
1323
    await this.sql`
1324
      UPDATE openclasp_agent_runtimes SET last_seen_at = ${lastSeenAt}, updated_at = NOW()
1325
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId} AND status = 'verified'
1326
    `;
1327
    return resolveAgentPresence(lastSeenAt);
1328
  }
1329
1330
  async getAgentPresence(agentId: string): Promise<AgentPresence> {
1331
    await this.ensureSchema();
1332
    const rows = await this.sql`
1333
      SELECT records.payload
1334
      FROM openclasp_public_agents agents
1335
      LEFT JOIN openclasp_records records
1336
        ON records.operator_id = agents.operator_id
1337
        AND records.kind = 'presence'
1338
        AND records.record_id = agents.agent_id
1339
      WHERE agents.agent_id = ${agentId}
1340
      LIMIT 1
1341
    `;
1342
    if (!rows.length) throw new Error('Published agent not found');
1343
    const lastSeenAt = rows[0]?.payload?.lastSeenAt;
1344
    return resolveAgentPresence(typeof lastSeenAt === 'string' ? lastSeenAt : undefined);
1345
  }
1346
1347
  async listAgentRuntimes(operatorId: string) {
1348
    await this.ensureSchema();
1349
    const rows = await this.sql`
1350
      SELECT agent_id, endpoint, a2a_endpoint, status, verified_at, last_seen_at, last_error, updated_at
1351
      FROM openclasp_agent_runtimes
1352
      WHERE operator_id = ${operatorId}
1353
      ORDER BY updated_at DESC
1354
    `;
1355
    return rows.map((row) => ({
1356
      agentId: String(row.agent_id),
1357
      endpoint: String(row.endpoint),
1358
      a2aEndpoint: String(row.a2a_endpoint ?? row.endpoint),
1359
      status: row.status as 'verified' | 'disabled',
1360
      verifiedAt: new Date(String(row.verified_at)).toISOString(),
1361
      ...(row.last_seen_at ? { lastSeenAt: new Date(String(row.last_seen_at)).toISOString() } : {}),
1362
      ...(row.last_error ? { lastError: String(row.last_error) } : {}),
1363
      updatedAt: new Date(String(row.updated_at)).toISOString(),
1364
    }));
1365
  }
1366
1367
  async registerAgentRuntime(operatorId: string, agentId: string, endpoint: string) {
1368
    await this.ensureSchema();
1369
    const owned = await this.sql`
1370
      SELECT 1 FROM openclasp_records
1371
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1372
    `;
1373
    if (!owned.length) throw new Error('Agent is not owned by this account');
1374
    const challenge = crypto.randomUUID();
1375
    const verification = await postRuntimeJson(endpoint, {
1376
      type: 'openclasp.runtime.verify',
1377
      version: '1',
1378
      agentId,
1379
      challenge,
1380
    });
1381
    if (
1382
      verification.status < 200 ||
1383
      verification.status >= 300 ||
1384
      !verification.body ||
1385
      typeof verification.body !== 'object' ||
1386
      (verification.body as { type?: unknown }).type !== 'openclasp.runtime.verified' ||
1387
      (verification.body as { version?: unknown }).version !== '1' ||
1388
      (verification.body as { agentId?: unknown }).agentId !== agentId ||
1389
      (verification.body as { challenge?: unknown }).challenge !== challenge
1390
    )
1391
      throw new Error('Runtime endpoint did not return the verification challenge');
1392
    const a2aEndpointValue = (verification.body as { a2aEndpoint?: unknown }).a2aEndpoint;
1393
    const a2aEndpoint =
1394
      typeof a2aEndpointValue === 'string' && a2aEndpointValue ? a2aEndpointValue : endpoint;
1395
    await resolvePublicRuntimeEndpoint(a2aEndpoint);
1396
    const runtimeSecret = randomBytes(32).toString('base64url');
1397
    const encrypted = encryptGatewayPayload(this.gatewaySecret(), runtimeSecret);
1398
    const verifiedAt = new Date().toISOString();
1399
    const saved = await this.sql`
1400
      INSERT INTO openclasp_agent_runtimes(
1401
        agent_id, operator_id, endpoint, a2a_endpoint, secret_ciphertext, secret_iv, secret_auth_tag,
1402
        status, verified_at, last_seen_at, updated_at
1403
      ) VALUES (
1404
        ${agentId}, ${operatorId}, ${endpoint}, ${a2aEndpoint}, ${encrypted.ciphertext},
1405
        ${encrypted.iv}, ${encrypted.authTag}, 'verified', ${verifiedAt}, ${verifiedAt}, NOW()
1406
      )
1407
      ON CONFLICT (agent_id) DO UPDATE SET
1408
        operator_id = EXCLUDED.operator_id,
1409
        endpoint = EXCLUDED.endpoint,
1410
        a2a_endpoint = EXCLUDED.a2a_endpoint,
1411
        secret_ciphertext = EXCLUDED.secret_ciphertext,
1412
        secret_iv = EXCLUDED.secret_iv,
1413
        secret_auth_tag = EXCLUDED.secret_auth_tag,
1414
        status = 'verified',
1415
        verified_at = EXCLUDED.verified_at,
1416
        last_seen_at = EXCLUDED.last_seen_at,
1417
        last_error = NULL,
1418
        updated_at = NOW()
1419
      WHERE openclasp_agent_runtimes.operator_id = ${operatorId}
1420
      RETURNING agent_id
1421
    `;
1422
    if (!saved.length) throw new Error('Agent runtime is owned by another account');
1423
    await this.sql`
1424
      UPDATE openclasp_public_agents
1425
      SET card = jsonb_set(
1426
          jsonb_set(
1427
            card,
1428
            '{transports}',
1429
            ${JSON.stringify([
1430
              {
1431
                protocol: 'A2A/1.0',
1432
                protocolBinding: 'JSONRPC',
1433
                endpoint: a2aEndpoint,
1434
                managedBy: 'agent',
1435
              },
1436
            ])}::jsonb
1437
          ),
1438
          '{agentMode}', '"persistent_runtime"'::jsonb
1439
        ), updated_at = NOW()
1440
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
1441
    `;
1442
    await this.sql`
1443
      UPDATE openclasp_records
1444
      SET payload = jsonb_set(
1445
          jsonb_set(payload, '{a2aEndpoint}', to_jsonb(${a2aEndpoint}::text)),
1446
          '{agentMode}', '"persistent_runtime"'::jsonb
1447
        ),
1448
        updated_at = NOW()
1449
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1450
    `;
1451
    await this.touchAgentPresence(operatorId, agentId);
1452
    return {
1453
      agentId,
1454
      endpoint,
1455
      a2aEndpoint,
1456
      status: 'verified' as const,
1457
      verifiedAt,
1458
      verificationKey: getSessionVerificationKey(this.gatewaySecret()),
1459
    };
1460
  }
1461
1462
  async disableAgentRuntime(operatorId: string, agentId: string) {
1463
    await this.ensureSchema();
1464
    const rows = await this.sql`
1465
      UPDATE openclasp_agent_runtimes
1466
      SET status = 'disabled', updated_at = NOW()
1467
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1468
      RETURNING agent_id
1469
    `;
1470
    if (!rows.length) throw new Error('Agent runtime not found');
1471
    return { agentId, status: 'disabled' as const };
1472
  }
1473
1474
  async deleteAgent(operatorId: string, agentId: string) {
1475
    await this.ensureSchema();
1476
    const owned = await this.sql`
1477
      SELECT 1 FROM openclasp_records
1478
      WHERE operator_id = ${operatorId}
1479
        AND kind = 'agent_profile'
1480
        AND record_id = ${agentId}
1481
      LIMIT 1
1482
    `;
1483
    if (!owned.length) throw new Error('Owned agent not found');
1484
    const openInteractions = await this.sql`
1485
      SELECT interaction_id FROM openclasp_federated_interactions
1486
      WHERE (initiator_agent_id = ${agentId} OR responder_agent_id = ${agentId})
1487
        AND status IN ('pending', 'active')
1488
      LIMIT 1
1489
    `;
1490
    if (openInteractions.length) {
1491
      const error = new Error('Finish or reject the agent’s open interactions before deleting it');
1492
      Object.assign(error, { statusCode: 409 });
1493
      throw error;
1494
    }
1495
    await this.sql`
1496
      DELETE FROM openclasp_hosted_threads
1497
      WHERE participant_a_agent_id = ${agentId} OR participant_b_agent_id = ${agentId}
1498
    `;
1499
    await this.sql`
1500
      DELETE FROM openclasp_public_agents
1501
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1502
    `;
1503
    await this.sql`
1504
      DELETE FROM openclasp_agent_runtimes
1505
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1506
    `;
1507
    await this.sql`
1508
      DELETE FROM openclasp_agent_access_tokens
1509
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1510
    `;
1511
    await this.sql`
1512
      DELETE FROM openclasp_records
1513
      WHERE operator_id = ${operatorId}
1514
        AND (
1515
          (record_id = ${agentId} AND kind IN ('agent', 'agent_profile', 'publication', 'presence'))
1516
          OR (kind = 'installation' AND payload->>'agentId' = ${agentId})
1517
          OR (kind = 'setup_request' AND payload->>'existingAgentId' = ${agentId})
1518
        )
1519
    `;
1520
    return {
1521
      agentId,
1522
      deleted: true as const,
1523
      historyRetained: true as const,
1524
    };
1525
  }
1526
1527
  private async sessionParticipant(agentId: string) {
1528
    const rows = await this.sql`
1529
      SELECT agents.operator_id, agents.card, profile.payload AS profile,
1530
        runtime.endpoint, runtime.a2a_endpoint
1531
      FROM openclasp_public_agents agents
1532
      LEFT JOIN openclasp_records profile
1533
        ON profile.operator_id = agents.operator_id
1534
        AND profile.kind = 'agent_profile'
1535
        AND profile.record_id = agents.agent_id
1536
      LEFT JOIN openclasp_agent_runtimes runtime
1537
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
1538
      WHERE agents.agent_id = ${agentId}
1539
      LIMIT 1
1540
    `;
1541
    const row = rows[0];
1542
    if (!row) throw new Error(`Published agent ${agentId} was not found`);
1543
    const card = normalizePublicAgentCard(row.card);
1544
    const profile = row.profile as Partial<AgentProfile> | undefined;
1545
    const mode =
1546
      profile?.agentMode ??
1547
      (row.endpoint || row.a2a_endpoint ? 'persistent_runtime' : 'temporary_chat');
1548
    if (mode === 'persistent_runtime' && !row.endpoint)
1549
      throw new Error(`Agent ${agentId} does not have a verified live runtime`);
1550
    const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
1551
      /\/$/,
1552
      '',
1553
    );
1554
    return {
1555
      agentId,
1556
      operatorId: String(row.operator_id),
1557
      mode: mode as 'persistent_runtime' | 'temporary_chat',
1558
      ...(row.endpoint ? { callbackEndpoint: String(row.endpoint) } : {}),
1559
      a2aEndpoint:
1560
        mode === 'temporary_chat'
1561
          ? `${baseUrl}/a2a/temporary/${encodeURIComponent(agentId)}`
1562
          : String(row.a2a_endpoint ?? row.endpoint),
1563
      card: PublicAgentCardSchema.parse({ ...card, agentMode: mode }),
1564
    };
1565
  }
1566
1567
  private async signedRuntimeRequest(
1568
    runtime: { callbackEndpoint: string },
1569
    requestId: string,
1570
    value: unknown,
1571
  ) {
1572
    const body = JSON.stringify(value);
1573
    const timestamp = new Date().toISOString();
1574
    return postRuntimeJson(runtime.callbackEndpoint, value, {
1575
      'openclasp-request-id': requestId,
1576
      'openclasp-timestamp': timestamp,
1577
      'openclasp-signature': `v1=${signSessionControl(this.gatewaySecret(), requestId, timestamp, body)}`,
1578
    });
1579
  }
1580
1581
  private sessionGrant(
1582
    interaction: FederatedInteraction,
1583
    senderAgentId: string,
1584
    recipientAgentId: string,
1585
  ) {
1586
    return issueSessionGrant(this.gatewaySecret(), {
1587
      interactionId: interaction.interactionId,
1588
      senderAgentId,
1589
      recipientAgentId,
1590
      expiresAt: Math.min(Date.parse(interaction.expiresAt), Date.now() + 60 * 60_000),
1591
    });
1592
  }
1593
1594
  private async privateCounterpartyInsights(
1595
    viewerOperatorId: string,
1596
    counterparty: { agentId: string; card: PublicAgentCard },
1597
    taskCategory: string,
1598
  ): Promise<{
1599
    insights: LiveSessionInsight[];
1600
    relevantSampleSize: number;
1601
    historyConfidence: number;
1602
  }> {
1603
    const rows = await this.sql`
1604
      SELECT record_id, payload
1605
      FROM openclasp_records
1606
      WHERE operator_id = ${viewerOperatorId}
1607
        AND kind = 'profile'
1608
        AND payload->>'agentId' = ${counterparty.agentId}
1609
        AND payload->>'taskCategory' = ${taskCategory}
1610
      ORDER BY (payload->>'sampleSize')::integer DESC, updated_at DESC
1611
    `;
1612
    const profiles: ContextualProfile[] = rows
1613
      .map<Record<string, unknown>>((row) => ({
1614
        recordId: String(row.record_id),
1615
        ...(row.payload as Record<string, unknown>),
1616
      }))
1617
      .filter(
1618
        (profile) =>
1619
          typeof profile.agentVersion === 'string' &&
1620
          typeof profile.sampleSize === 'number' &&
1621
          typeof profile.updatedAt === 'string' &&
1622
          typeof profile.completion === 'number' &&
1623
          typeof profile.acceptance === 'number' &&
1624
          typeof profile.specification === 'number' &&
1625
          typeof profile.deadline === 'number' &&
1626
          typeof profile.communication === 'number' &&
1627
          typeof profile.evidence === 'number' &&
1628
          typeof profile.scope === 'number' &&
1629
          typeof profile.disputes === 'number',
1630
      ) as ContextualProfile[];
1631
    const exact = profiles.find(
1632
      (profile) => profile.agentVersion === counterparty.card.agentVersion,
1633
    );
1634
    const current = exact ?? profiles[0];
1635
    const insights: LiveSessionInsight[] = [];
1636
    let relevantSampleSize = 0;
1637
    let historyConfidence = 0;
1638
    if (!current) {
1639
      insights.push({
1640
        code: 'limited_verified_history',
1641
        severity: 'caution',
1642
        message: `No eligible ${taskCategory} history is available for this agent version. Ask for task-relevant evidence.`,
1643
        evidenceReferences: [],
1644
        requirementReferences: [],
1645
      });
1646
    } else {
1647
      const ageDays = Math.max(
1648
        0,
1649
        (Date.now() - Date.parse(String(current.updatedAt))) / 86_400_000,
1650
      );
1651
      const freshness = Math.exp(-ageDays / 180);
1652
      const sampleSize = Number(current.sampleSize);
1653
      const effectiveSampleSize = Number(current.effectiveSampleSize ?? sampleSize);
1654
      const versionChanged = current.agentVersion !== counterparty.card.agentVersion;
1655
      const confidence = Math.min(
1656
        0.95,
1657
        (effectiveSampleSize / (effectiveSampleSize + 5)) * freshness * (versionChanged ? 0.35 : 1),
1658
      );
1659
      relevantSampleSize = sampleSize;
1660
      historyConfidence = confidence;
1661
      const reference = `openclasp:profile:${current.recordId}`;
1662
      insights.push({
1663
        code: versionChanged ? 'version_history_only' : 'contextual_history',
1664
        severity: versionChanged || confidence < 0.25 ? 'caution' : 'info',
1665
        message: versionChanged
1666
          ? `Only prior-version ${taskCategory} history is available (${sampleSize} eligible interaction${sampleSize === 1 ? '' : 's'}); confidence is reduced for version ${counterparty.card.agentVersion}.`
1667
          : `Based on ${sampleSize} eligible ${taskCategory} interaction${sampleSize === 1 ? '' : 's'}; confidence ${(confidence * 100).toFixed(0)}%.`,
1668
        evidenceReferences: [reference],
1669
        requirementReferences: [],
1670
      });
1671
      const weakDimensions = [
1672
        ['completion', Number(current.completion)],
1673
        ['output acceptance', Number(current.acceptance)],
1674
        ['specification adherence', Number(current.specification)],
1675
        ['deadline reliability', Number(current.deadline)],
1676
        ['communication quality', Number(current.communication)],
1677
        ['evidence quality', Number(current.evidence)],
1678
        ['scope adherence', Number(current.scope)],
1679
      ].filter(([, score]) => Number(score) < 0.7);
1680
      if (weakDimensions.length)
1681
        insights.push({
1682
          code: 'historical_weaknesses',
1683
          severity: weakDimensions.some(([, score]) => Number(score) < 0.5) ? 'high' : 'caution',
1684
          message: `Observed weaker areas: ${weakDimensions
1685
            .map(([name, score]) => `${String(name)} ${(Number(score) * 100).toFixed(0)}%`)
1686
            .join(', ')}.`,
1687
          evidenceReferences: [reference],
1688
          requirementReferences: [],
1689
        });
1690
      if (Number(current.disputes) > 0.25)
1691
        insights.push({
1692
          code: 'elevated_dispute_history',
1693
          severity: Number(current.disputes) >= 0.5 ? 'high' : 'caution',
1694
          message: `Eligible feedback shows a ${(Number(current.disputes) * 100).toFixed(0)}% dispute rate in this task category.`,
1695
          evidenceReferences: [reference],
1696
          requirementReferences: [],
1697
        });
1698
    }
1699
    if (counterparty.card.limitations.length)
1700
      insights.push({
1701
        code: 'declared_limitations',
1702
        severity: 'info',
1703
        message: `Counterparty declares: ${counterparty.card.limitations.join('; ')}`,
1704
        evidenceReferences: [counterparty.card.cardUrl],
1705
        requirementReferences: [],
1706
      });
1707
    return { insights, relevantSampleSize, historyConfidence };
1708
  }
1709
1710
  async brokerLiveSession(interaction: FederatedInteraction) {
1711
    await this.ensureSchema();
1712
    const existing = await this.sql`
1713
      SELECT status FROM openclasp_live_sessions
1714
      WHERE interaction_id = ${interaction.interactionId}
1715
    `;
1716
    if (existing[0]?.status === 'active') return;
1717
    const [initiator, responder] = await Promise.all([
1718
      this.sessionParticipant(interaction.initiatorAgentId),
1719
      this.sessionParticipant(interaction.responderAgentId),
1720
    ]);
1721
    if (initiator.mode === 'temporary_chat' && responder.mode === 'temporary_chat')
1722
      throw new Error('Temporary-to-temporary hosted conversations are not supported in this MVP');
1723
    const [initiatorContext, responderContext] = await Promise.all([
1724
      this.privateCounterpartyInsights(
1725
        initiator.operatorId,
1726
        responder,
1727
        interaction.contract.taskCategory,
1728
      ),
1729
      this.privateCounterpartyInsights(
1730
        responder.operatorId,
1731
        initiator,
1732
        interaction.contract.taskCategory,
1733
      ),
1734
    ]);
1735
    const issuedAt = new Date().toISOString();
1736
    const initiatorBrief = buildCounterpartyBrief({
1737
      interactionId: interaction.interactionId,
1738
      contractHash: interaction.termsHash,
1739
      contract: interaction.contract,
1740
      recipientAgentId: initiator.agentId,
1741
      subject: responder.card,
1742
      historyInsights: initiatorContext.insights,
1743
      relevantSampleSize: initiatorContext.relevantSampleSize,
1744
      historyConfidence: initiatorContext.historyConfidence,
1745
      generatedAt: issuedAt,
1746
      expiresAt: interaction.expiresAt,
1747
    });
1748
    const responderBrief = buildCounterpartyBrief({
1749
      interactionId: interaction.interactionId,
1750
      contractHash: interaction.termsHash,
1751
      contract: interaction.contract,
1752
      recipientAgentId: responder.agentId,
1753
      subject: initiator.card,
1754
      historyInsights: responderContext.insights,
1755
      relevantSampleSize: responderContext.relevantSampleSize,
1756
      historyConfidence: responderContext.historyConfidence,
1757
      generatedAt: issuedAt,
1758
      expiresAt: interaction.expiresAt,
1759
    });
1760
    await Promise.all([
1761
      this.upsert(
1762
        initiator.operatorId,
1763
        'counterparty_brief',
1764
        initiatorBrief.briefId,
1765
        initiatorBrief,
1766
      ),
1767
      this.upsert(
1768
        responder.operatorId,
1769
        'counterparty_brief',
1770
        responderBrief.briefId,
1771
        responderBrief,
1772
      ),
1773
    ]);
1774
    const makeOffer = (
1775
      runtime: typeof initiator,
1776
      counterparty: typeof responder,
1777
      role: 'initiator' | 'responder',
1778
      privateInsights: LiveSessionInsight[],
1779
      counterpartyBrief: CounterpartyBrief,
1780
    ) =>
1781
      LiveSessionOfferSchema.parse({
1782
        type: 'openclasp.session.offer',
1783
        version: '1',
1784
        offerId: crypto.randomUUID(),
1785
        interactionId: interaction.interactionId,
1786
        agentId: runtime.agentId,
1787
        role,
1788
        counterparty: {
1789
          agentId: counterparty.agentId,
1790
          name: counterparty.card.name,
1791
          agentVersion: counterparty.card.agentVersion,
1792
          capabilities: counterparty.card.capabilities,
1793
        },
1794
        contract: interaction.contract,
1795
        contractHash: interaction.termsHash,
1796
        privateInsights,
1797
        counterpartyBrief,
1798
        issuedAt,
1799
        expiresAt: interaction.expiresAt,
1800
      });
1801
    const initiatorOffer = makeOffer(
1802
      initiator,
1803
      responder,
1804
      'initiator',
1805
      initiatorBrief.insights,
1806
      initiatorBrief,
1807
    );
1808
    const responderOffer = makeOffer(
1809
      responder,
1810
      initiator,
1811
      'responder',
1812
      responderBrief.insights,
1813
      responderBrief,
1814
    );
1815
    const prepare = async (
1816
      participant: typeof initiator,
1817
      offer: typeof initiatorOffer,
1818
      label: 'Initiator' | 'Responder',
1819
    ) => {
1820
      if (participant.mode === 'temporary_chat')
1821
        return LiveSessionAcceptanceSchema.parse({
1822
          type: 'openclasp.session.accepted',
1823
          version: '1',
1824
          offerId: offer.offerId,
1825
          interactionId: interaction.interactionId,
1826
          agentId: participant.agentId,
1827
          sessionId: crypto.randomUUID(),
1828
          a2aEndpoint: participant.a2aEndpoint,
1829
          expiresAt: interaction.expiresAt,
1830
        });
1831
      if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
1832
      const response = await this.signedRuntimeRequest(
1833
        { callbackEndpoint: participant.callbackEndpoint },
1834
        offer.offerId,
1835
        offer,
1836
      );
1837
      if (response.status < 200 || response.status >= 300)
1838
        throw new Error(`${label} runtime is not live (HTTP ${response.status})`);
1839
      return LiveSessionAcceptanceSchema.parse(response.body);
1840
    };
1841
    const [initiatorAcceptance, responderAcceptance] = await Promise.all([
1842
      prepare(initiator, initiatorOffer, 'Initiator'),
1843
      prepare(responder, responderOffer, 'Responder'),
1844
    ]);
1845
    if (
1846
      initiatorAcceptance.offerId !== initiatorOffer.offerId ||
1847
      initiatorAcceptance.agentId !== initiator.agentId ||
1848
      responderAcceptance.offerId !== responderOffer.offerId ||
1849
      responderAcceptance.agentId !== responder.agentId
1850
    )
1851
      throw new Error('Runtime returned a mismatched live-session acceptance');
1852
    await Promise.all([
1853
      ...(initiator.mode === 'persistent_runtime'
1854
        ? [resolvePublicRuntimeEndpoint(initiatorAcceptance.a2aEndpoint)]
1855
        : []),
1856
      ...(responder.mode === 'persistent_runtime'
1857
        ? [resolvePublicRuntimeEndpoint(responderAcceptance.a2aEndpoint)]
1858
        : []),
1859
    ]);
1860
    await this.sql`
1861
      INSERT INTO openclasp_live_sessions(
1862
        interaction_id, initiator_agent_id, responder_agent_id,
1863
        initiator_session_id, responder_session_id,
1864
        initiator_endpoint, responder_endpoint, status, expires_at
1865
      ) VALUES (
1866
        ${interaction.interactionId}, ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
1867
        ${initiatorAcceptance.sessionId}, ${responderAcceptance.sessionId},
1868
        ${initiatorAcceptance.a2aEndpoint}, ${responderAcceptance.a2aEndpoint},
1869
        'preparing', ${interaction.expiresAt}
1870
      )
1871
      ON CONFLICT (interaction_id) DO UPDATE SET
1872
        initiator_session_id = EXCLUDED.initiator_session_id,
1873
        responder_session_id = EXCLUDED.responder_session_id,
1874
        initiator_endpoint = EXCLUDED.initiator_endpoint,
1875
        responder_endpoint = EXCLUDED.responder_endpoint,
1876
        status = 'preparing', expires_at = EXCLUDED.expires_at,
1877
        activated_at = NULL, completed_at = NULL, last_error = NULL
1878
    `;
1879
    await this.journalLiveSessionState(interaction.interactionId);
1880
    const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
1881
      /\/$/,
1882
      '',
1883
    );
1884
    const activatedAt = new Date().toISOString();
1885
    const verificationKey = getSessionVerificationKey(this.gatewaySecret());
1886
    const makeActivation = (
1887
      agentId: string,
1888
      sessionId: string,
1889
      role: 'initiator' | 'responder',
1890
      peerAgentId: string,
1891
      peerSessionId: string,
1892
      peerEndpoint: string,
1893
      privateInsights: LiveSessionInsight[],
1894
      counterpartyBrief: CounterpartyBrief,
1895
    ): LiveSessionActivation => {
1896
      const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
1897
      return LiveSessionActivationSchema.parse({
1898
        type: 'openclasp.session.activation',
1899
        version: '1',
1900
        activationId: crypto.randomUUID(),
1901
        interactionId: interaction.interactionId,
1902
        agentId,
1903
        sessionId,
1904
        role,
1905
        peer: {
1906
          agentId: peerAgentId,
1907
          sessionId: peerSessionId,
1908
          endpoint: peerEndpoint,
1909
          bearerToken,
1910
          verificationKey,
1911
        },
1912
        reporting: {
1913
          endpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/events`,
1914
          completionEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/completion-reports`,
1915
          feedbackEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/feedback`,
1916
          bearerToken,
1917
        },
1918
        privateInsights,
1919
        counterpartyBrief,
1920
        contractHash: interaction.termsHash,
1921
        activatedAt,
1922
        expiresAt: interaction.expiresAt,
1923
      });
1924
    };
1925
    const initiatorActivation = makeActivation(
1926
      initiator.agentId,
1927
      initiatorAcceptance.sessionId,
1928
      'initiator',
1929
      responder.agentId,
1930
      responderAcceptance.sessionId,
1931
      responderAcceptance.a2aEndpoint,
1932
      initiatorBrief.insights,
1933
      initiatorBrief,
1934
    );
1935
    const responderActivation = makeActivation(
1936
      responder.agentId,
1937
      responderAcceptance.sessionId,
1938
      'responder',
1939
      initiator.agentId,
1940
      initiatorAcceptance.sessionId,
1941
      initiatorAcceptance.a2aEndpoint,
1942
      responderBrief.insights,
1943
      responderBrief,
1944
    );
1945
    try {
1946
      const activate = async (
1947
        participant: typeof initiator,
1948
        activation: LiveSessionActivation,
1949
        label: 'Initiator' | 'Responder',
1950
      ) => {
1951
        if (participant.mode === 'temporary_chat') return;
1952
        if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
1953
        const response = await this.signedRuntimeRequest(
1954
          { callbackEndpoint: participant.callbackEndpoint },
1955
          activation.activationId,
1956
          activation,
1957
        );
1958
        if (response.status < 200 || response.status >= 300)
1959
          throw new Error(`${label} activation failed with HTTP ${response.status}`);
1960
      };
1961
      await activate(responder, responderActivation, 'Responder');
1962
      await activate(initiator, initiatorActivation, 'Initiator');
1963
      await this.sql`
1964
        UPDATE openclasp_live_sessions
1965
        SET status = 'active', activated_at = NOW(), last_error = NULL
1966
        WHERE interaction_id = ${interaction.interactionId}
1967
      `;
1968
      await this.journalLiveSessionState(interaction.interactionId);
1969
      await this.sql`
1970
        UPDATE openclasp_agent_runtimes
1971
        SET last_seen_at = NOW(), last_error = NULL, updated_at = NOW()
1972
        WHERE agent_id IN (${initiator.agentId}, ${responder.agentId})
1973
      `;
1974
      await Promise.all([
1975
        this.touchAgentPresence(initiator.operatorId, initiator.agentId),
1976
        this.touchAgentPresence(responder.operatorId, responder.agentId),
1977
      ]);
1978
    } catch (error) {
1979
      const reason =
1980
        error instanceof Error ? error.message.slice(0, 500) : 'Session activation failed';
1981
      await this.sql`
1982
        UPDATE openclasp_live_sessions SET status = 'failed', last_error = ${reason}
1983
        WHERE interaction_id = ${interaction.interactionId}
1984
      `;
1985
      await this.journalLiveSessionState(interaction.interactionId);
1986
      throw error;
1987
    }
1988
  }
1989
1990
  async getLiveSession(operatorId: string, interactionId: string, agentId: string) {
1991
    await this.ensureSchema();
1992
    const rows = await this.sql`
1993
      SELECT session.*, interaction.payload,
1994
        initiator.operator_id AS initiator_operator_id,
1995
        responder.operator_id AS responder_operator_id
1996
      FROM openclasp_live_sessions session
1997
      INNER JOIN openclasp_federated_interactions interaction
1998
        ON interaction.interaction_id = session.interaction_id
1999
      INNER JOIN openclasp_public_agents initiator
2000
        ON initiator.agent_id = session.initiator_agent_id
2001
      INNER JOIN openclasp_public_agents responder
2002
        ON responder.agent_id = session.responder_agent_id
2003
      WHERE session.interaction_id = ${interactionId} AND session.status = 'active'
2004
      LIMIT 1
2005
    `;
2006
    const row = rows[0];
2007
    if (!row) throw new Error('Active live session not found');
2008
    const isInitiator = row.initiator_agent_id === agentId;
2009
    const isResponder = row.responder_agent_id === agentId;
2010
    if (!isInitiator && !isResponder) throw new Error('Agent is not a session participant');
2011
    const owner = isInitiator ? row.initiator_operator_id : row.responder_operator_id;
2012
    if (owner !== operatorId) throw new Error('Agent is not owned by this account');
2013
    const interaction = FederatedInteractionSchema.parse(row.payload);
2014
    const peerAgentId = String(isInitiator ? row.responder_agent_id : row.initiator_agent_id);
2015
    const peer = await this.sessionParticipant(peerAgentId);
2016
    const privateContext = await this.privateCounterpartyInsights(
2017
      String(owner),
2018
      peer,
2019
      interaction.contract.taskCategory,
2020
    );
2021
    const briefRows = await this.sql`
2022
      SELECT payload FROM openclasp_records
2023
      WHERE operator_id = ${String(owner)}
2024
        AND kind = 'counterparty_brief'
2025
        AND payload->>'interactionId' = ${interactionId}
2026
        AND payload->>'recipientAgentId' = ${agentId}
2027
      ORDER BY updated_at DESC
2028
      LIMIT 1
2029
    `;
2030
    const storedBrief = CounterpartyBriefSchema.safeParse(briefRows[0]?.payload);
2031
    const counterpartyBrief = storedBrief.success
2032
      ? storedBrief.data
2033
      : buildCounterpartyBrief({
2034
          interactionId,
2035
          contractHash: interaction.termsHash,
2036
          contract: interaction.contract,
2037
          recipientAgentId: agentId,
2038
          subject: peer.card,
2039
          historyInsights: privateContext.insights,
2040
          relevantSampleSize: privateContext.relevantSampleSize,
2041
          historyConfidence: privateContext.historyConfidence,
2042
          expiresAt: new Date(String(row.expires_at)).toISOString(),
2043
        });
2044
    const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
2045
    return {
2046
      type: 'openclasp.session.activation' as const,
2047
      version: '1' as const,
2048
      activationId: crypto.randomUUID(),
2049
      interactionId,
2050
      agentId,
2051
      sessionId: String(isInitiator ? row.initiator_session_id : row.responder_session_id),
2052
      role: isInitiator ? ('initiator' as const) : ('responder' as const),
2053
      peer: {
2054
        agentId: peerAgentId,
2055
        sessionId: String(isInitiator ? row.responder_session_id : row.initiator_session_id),
2056
        endpoint: String(isInitiator ? row.responder_endpoint : row.initiator_endpoint),
2057
        bearerToken,
2058
        verificationKey: getSessionVerificationKey(this.gatewaySecret()),
2059
      },
2060
      reporting: {
2061
        endpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/events`,
2062
        completionEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/completion-reports`,
2063
        feedbackEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/feedback`,
2064
        bearerToken,
2065
      },
2066
      privateInsights: privateContext.insights,
2067
      counterpartyBrief,
2068
      contractHash: interaction.termsHash,
2069
      activatedAt: row.activated_at
2070
        ? new Date(String(row.activated_at)).toISOString()
2071
        : new Date().toISOString(),
2072
      expiresAt: new Date(String(row.expires_at)).toISOString(),
2073
    };
2074
  }
2075
2076
  private async interactionParticipant(operatorId: string, interactionId: string, agentId: string) {
2077
    await this.ensureSchema();
2078
    const rows = await this.sql`
2079
      SELECT payload, initiator_operator_id, responder_operator_id,
2080
        initiator_agent_id, responder_agent_id
2081
      FROM openclasp_federated_interactions
2082
      WHERE interaction_id = ${interactionId}
2083
        AND (
2084
          (initiator_operator_id = ${operatorId} AND initiator_agent_id = ${agentId})
2085
          OR (responder_operator_id = ${operatorId} AND responder_agent_id = ${agentId})
2086
        )
2087
      LIMIT 1
2088
    `;
2089
    const row = rows[0];
2090
    if (!row) throw new Error('Interaction participant not found for this account');
2091
    const interaction = FederatedInteractionSchema.parse(row.payload);
2092
    const isInitiator = interaction.initiatorAgentId === agentId;
2093
    return {
2094
      interaction,
2095
      counterpartyAgentId: isInitiator
2096
        ? interaction.responderAgentId
2097
        : interaction.initiatorAgentId,
2098
      participantOperatorId: String(
2099
        isInitiator ? row.initiator_operator_id : row.responder_operator_id,
2100
      ),
2101
      counterpartyOperatorId: String(
2102
        isInitiator ? row.responder_operator_id : row.initiator_operator_id,
2103
      ),
2104
    };
2105
  }
2106
2107
  async getCounterpartyBrief(operatorId: string, interactionId: string, agentId: string) {
2108
    await this.interactionParticipant(operatorId, interactionId, agentId);
2109
    const rows = await this.sql`
2110
      SELECT payload FROM openclasp_records
2111
      WHERE operator_id = ${operatorId}
2112
        AND kind = 'counterparty_brief'
2113
        AND payload->>'interactionId' = ${interactionId}
2114
        AND payload->>'recipientAgentId' = ${agentId}
2115
      ORDER BY updated_at DESC
2116
      LIMIT 1
2117
    `;
2118
    const parsed = CounterpartyBriefSchema.safeParse(rows[0]?.payload);
2119
    if (!parsed.success) throw new Error('Counterparty brief is not available');
2120
    return parsed.data;
2121
  }
2122
2123
  private async ensureFeedbackRequest(
2124
    operatorId: string,
2125
    interactionId: string,
2126
    reviewerAgentId: string,
2127
    subjectAgentId: string,
2128
  ) {
2129
    const existing = await this.sql`
2130
      SELECT payload FROM openclasp_records
2131
      WHERE operator_id = ${operatorId}
2132
        AND kind = 'feedback_request'
2133
        AND payload->>'interactionId' = ${interactionId}
2134
        AND payload->>'reviewerAgentId' = ${reviewerAgentId}
2135
      ORDER BY updated_at DESC
2136
      LIMIT 1
2137
    `;
2138
    const parsed = FeedbackRequestSchema.safeParse(existing[0]?.payload);
2139
    if (parsed.success) return parsed.data;
2140
    const requestedAt = new Date();
2141
    const base = FeedbackRequestSchema.parse({
2142
      requestId: deterministicUuid(`feedback-request:${interactionId}:${reviewerAgentId}`),
2143
      interactionId,
2144
      reviewerAgentId,
2145
      subjectAgentId,
2146
      status: 'pending',
2147
      requestedDimensions: FEEDBACK_DIMENSIONS,
2148
      requestedAt: requestedAt.toISOString(),
2149
      dueAt: new Date(requestedAt.getTime() + feedbackWindowMilliseconds()).toISOString(),
2150
    });
2151
    const request = FeedbackRequestSchema.parse({
2152
      ...base,
2153
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
2154
    });
2155
    await this.upsert(operatorId, 'feedback_request', request.requestId, request);
2156
    return request;
2157
  }
2158
2159
  private async requestRuntimeFinalization(
2160
    interactionId: string,
2161
    reportingAgentId: string,
2162
    counterpartyAgentId: string,
2163
    contractHash: string,
2164
  ) {
2165
    const rows = await this.sql`
2166
      SELECT endpoint FROM openclasp_agent_runtimes
2167
      WHERE agent_id = ${counterpartyAgentId} AND status = 'verified'
2168
      LIMIT 1
2169
    `;
2170
    if (!rows[0]?.endpoint) return false;
2171
    const requestId = crypto.randomUUID();
2172
    const value = {
2173
      type: 'openclasp.session.finalization_request',
2174
      version: '1',
2175
      requestId,
2176
      interactionId,
2177
      agentId: counterpartyAgentId,
2178
      peerAgentId: reportingAgentId,
2179
      contractHash,
2180
      requestedAt: new Date().toISOString(),
2181
    };
2182
    const response = await this.signedRuntimeRequest(
2183
      { callbackEndpoint: String(rows[0].endpoint) },
2184
      requestId,
2185
      value,
2186
    );
2187
    return response.status >= 200 && response.status < 300;
2188
  }
2189
2190
  async listFeedbackRequests(operatorId: string, agentId: string) {
2191
    const rows = await this.sql`
2192
      SELECT payload FROM openclasp_records
2193
      WHERE operator_id = ${operatorId}
2194
        AND kind = 'feedback_request'
2195
        AND payload->>'reviewerAgentId' = ${agentId}
2196
      ORDER BY updated_at DESC
2197
    `;
2198
    return rows.map((row) => FeedbackRequestSchema.parse(row.payload));
2199
  }
2200
2201
  async submitCompletionReport(
2202
    operatorId: string,
2203
    agentId: string,
2204
    value: InteractionCompletionReport,
2205
    submissionMethod: 'oauth_installation' | 'agent_access_token' | 'runtime_session',
2206
  ) {
2207
    const participant = await this.interactionParticipant(operatorId, value.interactionId, agentId);
2208
    const report = InteractionCompletionReportSchema.parse(value);
2209
    if (report.platformAttestation)
2210
      throw new Error('Platform attestation is assigned by OpenClasp');
2211
    if (report.reportingAgentId !== agentId)
2212
      throw new Error('Completion report identity does not match the authenticated agent');
2213
    if (report.counterpartyAgentId !== participant.counterpartyAgentId)
2214
      throw new Error('Completion report counterparty does not match the interaction');
2215
    if (report.contractHash !== participant.interaction.termsHash)
2216
      throw new Error('Completion report contract hash does not match the interaction');
2217
    if (report.requestedOutcome !== participant.interaction.contract.requestedOutcome)
2218
      throw new Error('Completion report requested outcome does not match the contract');
2219
    if (
2220
      report.criteria.some(
2221
        (criterion) =>
2222
          !participant.interaction.contract.successCriteria.includes(criterion.criterion),
2223
      )
2224
    )
2225
      throw new Error('Completion report contains a criterion outside the contract');
2226
    if (!['active', 'completed'].includes(participant.interaction.status))
2227
      throw new Error('Only active or completed interactions accept completion reports');
2228
    const profiles = await this.sql`
2229
      SELECT payload FROM openclasp_records
2230
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
2231
      LIMIT 1
2232
    `;
2233
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
2234
    if (!profile || profile.agentVersion !== report.agentVersion)
2235
      throw new Error('Completion report agent version does not match the registered agent');
2236
    let verifiedSubmissionMethod: InteractionCompletionReport['submissionMethod'] =
2237
      submissionMethod;
2238
    if (report.signature) {
2239
      if (report.submissionMethod !== 'agent_signature')
2240
        throw new Error('Signed completion reports must declare agent_signature submission');
2241
      const identities = await this.sql`
2242
        SELECT payload FROM openclasp_records
2243
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
2244
        LIMIT 1
2245
      `;
2246
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
2247
      if (
2248
        !identity.success ||
2249
        !verifyObject(report as unknown as Record<string, unknown>, identity.data.publicKey)
2250
      )
2251
        throw new Error('Completion report agent signature is invalid or unverifiable');
2252
      verifiedSubmissionMethod = 'agent_signature';
2253
    }
2254
    const unsigned = InteractionCompletionReportSchema.parse({
2255
      ...report,
2256
      submissionMethod: verifiedSubmissionMethod,
2257
    });
2258
    const stored = InteractionCompletionReportSchema.parse({
2259
      ...unsigned,
2260
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unsigned),
2261
    });
2262
    const existing = await this.sql`
2263
      SELECT payload FROM openclasp_records
2264
      WHERE kind = 'completion_report' AND record_id = ${stored.reportId}
2265
        AND payload->>'interactionId' = ${stored.interactionId}
2266
      LIMIT 1
2267
    `;
2268
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
2269
      throw new Error('Conflicting completion report ID');
2270
    await Promise.all([
2271
      this.upsert(participant.participantOperatorId, 'completion_report', stored.reportId, stored),
2272
      this.upsert(participant.counterpartyOperatorId, 'completion_report', stored.reportId, stored),
2273
    ]);
2274
    const [feedbackRequest] = await Promise.all([
2275
      this.ensureFeedbackRequest(
2276
        participant.participantOperatorId,
2277
        stored.interactionId,
2278
        stored.reportingAgentId,
2279
        stored.counterpartyAgentId,
2280
      ),
2281
      this.ensureFeedbackRequest(
2282
        participant.counterpartyOperatorId,
2283
        stored.interactionId,
2284
        stored.counterpartyAgentId,
2285
        stored.reportingAgentId,
2286
      ),
2287
    ]);
2288
    const completed = await this.sql`
2289
      SELECT COUNT(DISTINCT payload->>'reportingAgentId') AS count
2290
      FROM openclasp_records
2291
      WHERE kind = 'completion_report'
2292
        AND payload->>'interactionId' = ${stored.interactionId}
2293
    `;
2294
    let peerReportStatus: InteractionConclusion['peerReportStatus'] = 'received';
2295
    if (Number(completed[0]?.count ?? 0) >= 2) {
2296
      const completedAt = new Date().toISOString();
2297
      await Promise.all([
2298
        this.sql`
2299
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
2300
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
2301
        `,
2302
        this.sql`
2303
          UPDATE openclasp_federated_interactions
2304
          SET status = 'completed',
2305
            payload = jsonb_set(
2306
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
2307
              '{updatedAt}', to_jsonb(${completedAt}::text)
2308
            ),
2309
            updated_at = NOW()
2310
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
2311
        `,
2312
      ]);
2313
      await Promise.all([
2314
        this.journalLiveSessionState(stored.interactionId),
2315
        this.journalFederatedInteraction(stored.interactionId),
2316
      ]);
2317
    } else {
2318
      const requested = await this.requestRuntimeFinalization(
2319
        stored.interactionId,
2320
        stored.reportingAgentId,
2321
        stored.counterpartyAgentId,
2322
        stored.contractHash,
2323
      ).catch(() => false);
2324
      peerReportStatus = requested ? 'awaiting' : 'unreachable';
2325
    }
2326
    const conclusion = await this.finalizeInteractionConclusion(stored.interactionId, {
2327
      peerReportStatus,
2328
    });
2329
    return {
2330
      report: stored,
2331
      feedbackRequest,
2332
      peerReportRequested: peerReportStatus === 'awaiting',
2333
      ...(conclusion.released ? { conclusion: conclusion.conclusion } : {}),
2334
    };
2335
  }
2336
2337
  async recordSessionCompletionReport(token: string, value: InteractionCompletionReport) {
2338
    const report = InteractionCompletionReportSchema.parse(value);
2339
    const grant = verifySessionGrant(this.gatewaySecret(), token);
2340
    if (
2341
      grant.interactionId !== report.interactionId ||
2342
      grant.senderAgentId !== report.reportingAgentId ||
2343
      grant.recipientAgentId !== report.counterpartyAgentId
2344
    )
2345
      throw new Error('Session credential does not match the completion report');
2346
    const owners = await this.sql`
2347
      SELECT initiator_operator_id, responder_operator_id,
2348
        initiator_agent_id, responder_agent_id
2349
      FROM openclasp_federated_interactions
2350
      WHERE interaction_id = ${report.interactionId}
2351
      LIMIT 1
2352
    `;
2353
    const row = owners[0];
2354
    if (!row) throw new Error('Interaction not found');
2355
    const operatorId = String(
2356
      row.initiator_agent_id === report.reportingAgentId
2357
        ? row.initiator_operator_id
2358
        : row.responder_operator_id,
2359
    );
2360
    return this.submitCompletionReport(
2361
      operatorId,
2362
      report.reportingAgentId,
2363
      report,
2364
      'runtime_session',
2365
    );
2366
  }
2367
2368
  private async applyInteractionLearning(input: {
2369
    interaction: FederatedInteraction;
2370
    initiatorOperatorId: string;
2371
    responderOperatorId: string;
2372
    reports: InteractionCompletionReport[];
2373
    feedback: InteractionFeedback[];
2374
    conclusion: InteractionConclusion;
2375
  }) {
2376
    const { interaction, reports, feedback, conclusion } = input;
2377
    const [initiatorSettings, responderSettings, cards, pairActivity] = await Promise.all([
2378
      this.getSettings(input.initiatorOperatorId),
2379
      this.getSettings(input.responderOperatorId),
2380
      this.sql`
2381
        SELECT agent_id, card FROM openclasp_public_agents
2382
        WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
2383
      `,
2384
      this.sql`
2385
        SELECT COUNT(*)::integer AS count
2386
        FROM openclasp_federated_interactions
2387
        WHERE created_at >= NOW() - INTERVAL '24 hours'
2388
          AND (
2389
            (initiator_agent_id = ${interaction.initiatorAgentId}
2390
              AND responder_agent_id = ${interaction.responderAgentId})
2391
            OR
2392
            (initiator_agent_id = ${interaction.responderAgentId}
2393
              AND responder_agent_id = ${interaction.initiatorAgentId})
2394
          )
2395
      `,
2396
    ]);
2397
    const contributionMode =
2398
      initiatorSettings.contributionEnabled && responderSettings.contributionEnabled
2399
        ? ('network_aggregate' as const)
2400
        : ('local_only' as const);
2401
    const eligibilityBase = evaluateLearningEligibility({
2402
      interactionId: interaction.interactionId,
2403
      reports,
2404
      feedback,
2405
      consensus: conclusion.consensus,
2406
      contributionMode,
2407
      reviewerCredibility: Object.fromEntries(
2408
        feedback.map((item) => [item.reviewerAgentId, reviewerCredibility(item)]),
2409
      ),
2410
      manipulationSignals: [
2411
        ...(input.initiatorOperatorId === input.responderOperatorId ? ['same_operator_pair'] : []),
2412
        ...(Number(pairActivity[0]?.count ?? 0) > 10 ? ['rapid_reciprocal_pair_activity'] : []),
2413
      ],
2414
      decisionId: deterministicUuid(`learning-eligibility:${interaction.interactionId}`),
2415
      decidedAt: conclusion.generatedAt,
2416
    });
2417
    const eligibility = LearningEligibilityDecisionSchema.parse({
2418
      ...eligibilityBase,
2419
      platformAttestation: attestSessionRecord(this.gatewaySecret(), eligibilityBase),
2420
    });
2421
    await Promise.all([
2422
      this.upsert(
2423
        input.initiatorOperatorId,
2424
        'learning_eligibility',
2425
        eligibility.decisionId,
2426
        eligibility,
2427
      ),
2428
      this.upsert(
2429
        input.responderOperatorId,
2430
        'learning_eligibility',
2431
        eligibility.decisionId,
2432
        eligibility,
2433
      ),
2434
    ]);
2435
    if (!eligibility.eligible || eligibility.sampleWeight <= 0)
2436
      return { eligibility, profileDeltas: [] as BehaviouralProfileDelta[] };
2437
2438
    const appliedAt = new Date().toISOString();
2439
    const versions = new Map<string, string>();
2440
    for (const report of reports) versions.set(report.reportingAgentId, report.agentVersion);
2441
    for (const row of cards) {
2442
      const card = PublicAgentCardSchema.safeParse(row.card);
2443
      if (card.success && !versions.has(String(row.agent_id)))
2444
        versions.set(String(row.agent_id), card.data.agentVersion);
2445
    }
2446
    const participants = [
2447
      {
2448
        viewerOperatorId: input.initiatorOperatorId,
2449
        reviewerAgentId: interaction.initiatorAgentId,
2450
        subjectAgentId: interaction.responderAgentId,
2451
      },
2452
      {
2453
        viewerOperatorId: input.responderOperatorId,
2454
        reviewerAgentId: interaction.responderAgentId,
2455
        subjectAgentId: interaction.initiatorAgentId,
2456
      },
2457
      {
2458
        viewerOperatorId: input.initiatorOperatorId,
2459
        reviewerAgentId: interaction.responderAgentId,
2460
        subjectAgentId: interaction.initiatorAgentId,
2461
      },
2462
      {
2463
        viewerOperatorId: input.responderOperatorId,
2464
        reviewerAgentId: interaction.initiatorAgentId,
2465
        subjectAgentId: interaction.responderAgentId,
2466
      },
2467
    ];
2468
    const profileDeltas: Array<BehaviouralProfileDelta | undefined> = [];
2469
    for (const participant of participants) {
2470
      const agentVersion = versions.get(participant.subjectAgentId);
2471
      if (!agentVersion) {
2472
        profileDeltas.push(undefined);
2473
        continue;
2474
      }
2475
      const deltaId = deterministicUuid(
2476
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.reviewerAgentId}:${participant.subjectAgentId}`,
2477
      );
2478
      const legacyDeltaId = deterministicUuid(
2479
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.subjectAgentId}`,
2480
      );
2481
      const existingDelta = await this.sql`
2482
          SELECT payload FROM openclasp_records
2483
          WHERE operator_id = ${participant.viewerOperatorId}
2484
            AND kind = 'profile_delta'
2485
            AND (record_id = ${deltaId} OR record_id = ${legacyDeltaId})
2486
          LIMIT 1
2487
        `;
2488
      const parsedDelta = BehaviouralProfileDeltaSchema.safeParse(existingDelta[0]?.payload);
2489
      if (parsedDelta.success) {
2490
        profileDeltas.push(parsedDelta.data);
2491
        continue;
2492
      }
2493
      const profileId = deterministicUuid(
2494
        `contextual-profile:${participant.subjectAgentId}:${agentVersion}:${interaction.contract.taskCategory}`,
2495
      );
2496
      const currentRows = await this.sql`
2497
          SELECT payload FROM openclasp_records
2498
          WHERE operator_id = ${participant.viewerOperatorId}
2499
            AND kind = 'profile' AND record_id = ${profileId}
2500
          LIMIT 1
2501
        `;
2502
      const current = currentRows[0]?.payload as Partial<ContextualProfile> | undefined;
2503
      const reviewerFeedback = feedback.find(
2504
        (item) =>
2505
          item.reviewerAgentId === participant.reviewerAgentId &&
2506
          item.subjectAgentId === participant.subjectAgentId,
2507
      );
2508
      const observations = deriveBehaviouralObservations({
2509
        subjectAgentId: participant.subjectAgentId,
2510
        reviewerAgentId: participant.reviewerAgentId,
2511
        reports,
2512
        ...(reviewerFeedback ? { reviewerFeedback } : {}),
2513
        conclusion,
2514
      });
2515
      if (!Object.keys(observations).length) {
2516
        profileDeltas.push(undefined);
2517
        continue;
2518
      }
2519
      const applied = updateContextualBehaviouralProfile({
2520
        ...(current ? { current } : {}),
2521
        observations,
2522
        sampleWeight: eligibility.sampleWeight,
2523
        appliedAt,
2524
      });
2525
      const profile: ContextualProfile = {
2526
        recordId: profileId,
2527
        agentId: participant.subjectAgentId,
2528
        agentVersion,
2529
        taskCategory: interaction.contract.taskCategory,
2530
        ...applied.profile,
2531
      };
2532
      const deltaBase = BehaviouralProfileDeltaSchema.parse({
2533
        deltaId,
2534
        interactionId: interaction.interactionId,
2535
        agentId: participant.subjectAgentId,
2536
        agentVersion,
2537
        taskCategory: interaction.contract.taskCategory,
2538
        sampleWeight: eligibility.sampleWeight,
2539
        dimensionDeltas: applied.dimensionDeltas,
2540
        explanation:
2541
          'Applied platform-attested structured outcome and eligible reviewer signals; private comments and raw conversation content were excluded.',
2542
        appliedAt,
2543
      });
2544
      const delta = BehaviouralProfileDeltaSchema.parse({
2545
        ...deltaBase,
2546
        platformAttestation: attestSessionRecord(this.gatewaySecret(), deltaBase),
2547
      });
2548
      await Promise.all([
2549
        this.upsert(participant.viewerOperatorId, 'profile', profileId, profile),
2550
        this.upsert(participant.viewerOperatorId, 'profile_delta', deltaId, delta),
2551
      ]);
2552
      profileDeltas.push(delta);
2553
    }
2554
    return {
2555
      eligibility,
2556
      profileDeltas: profileDeltas.filter(
2557
        (delta): delta is BehaviouralProfileDelta => delta !== undefined,
2558
      ),
2559
    };
2560
  }
2561
2562
  private async finalizeInteractionConclusion(
2563
    interactionId: string,
2564
    options: { peerReportStatus?: InteractionConclusion['peerReportStatus'] } = {},
2565
  ) {
2566
    const [interactionRows, reportRows, feedbackRows, requestRows, existingRows] =
2567
      await Promise.all([
2568
        this.sql`
2569
        SELECT payload, initiator_operator_id, responder_operator_id
2570
        FROM openclasp_federated_interactions
2571
        WHERE interaction_id = ${interactionId}
2572
        LIMIT 1
2573
      `,
2574
        this.sql`
2575
        SELECT record_id, payload FROM openclasp_records
2576
        WHERE kind = 'completion_report' AND payload->>'interactionId' = ${interactionId}
2577
      `,
2578
        this.sql`
2579
        SELECT record_id, payload FROM openclasp_records
2580
        WHERE kind = 'interaction_feedback' AND payload->>'interactionId' = ${interactionId}
2581
      `,
2582
        this.sql`
2583
        SELECT record_id, payload FROM openclasp_records
2584
        WHERE kind = 'feedback_request' AND payload->>'interactionId' = ${interactionId}
2585
      `,
2586
        this.sql`
2587
        SELECT payload FROM openclasp_records
2588
        WHERE kind = 'interaction_conclusion' AND payload->>'interactionId' = ${interactionId}
2589
        LIMIT 1
2590
      `,
2591
      ]);
2592
    const interactionRow = interactionRows[0];
2593
    if (!interactionRow) throw new Error('Interaction not found');
2594
    const interaction = FederatedInteractionSchema.parse(interactionRow.payload);
2595
    const reports = [
2596
      ...new Map(
2597
        reportRows
2598
          .map((row) => InteractionCompletionReportSchema.parse(row.payload))
2599
          .map((report) => [report.reportId, report]),
2600
      ).values(),
2601
    ];
2602
    const feedback = [
2603
      ...new Map(
2604
        feedbackRows
2605
          .map((row) => InteractionFeedbackSchema.parse(row.payload))
2606
          .map((item) => [item.feedbackId, item]),
2607
      ).values(),
2608
    ];
2609
    const requests = [
2610
      ...new Map(
2611
        requestRows
2612
          .map((row) => FeedbackRequestSchema.parse(row.payload))
2613
          .map((request) => [request.requestId, request]),
2614
      ).values(),
2615
    ];
2616
    if (!reports.length) return { released: false as const, feedbackRevealed: false as const };
2617
    const existing = InteractionConclusionSchema.safeParse(existingRows[0]?.payload);
2618
    const reportingAgentIds = new Set(reports.map((report) => report.reportingAgentId));
2619
    const missingReportAgentIds = [
2620
      interaction.initiatorAgentId,
2621
      interaction.responderAgentId,
2622
    ].filter((agentId) => !reportingAgentIds.has(agentId));
2623
    const feedbackWindowClosed =
2624
      requests.length >= 2 && requests.every((request) => request.status !== 'pending');
2625
    const lifecycle =
2626
      missingReportAgentIds.length === 0 || feedbackWindowClosed
2627
        ? ('final' as const)
2628
        : ('provisional' as const);
2629
    const visibleFeedback = feedbackWindowClosed ? feedback : [];
2630
    const peerReportStatus: InteractionConclusion['peerReportStatus'] =
2631
      missingReportAgentIds.length === 0
2632
        ? 'received'
2633
        : feedbackWindowClosed
2634
          ? 'timed_out'
2635
          : (options.peerReportStatus ?? existing.data?.peerReportStatus ?? 'awaiting');
2636
    const pendingFeedbackAgentIds = requests
2637
      .filter((request) => request.status === 'pending')
2638
      .map((request) => request.reviewerAgentId);
2639
    if (requests.length < 2) return { released: false as const };
2640
    const base = buildInteractionConclusion({
2641
      interaction,
2642
      reports,
2643
      feedback: visibleFeedback,
2644
      conclusionId: deterministicUuid(`interaction-conclusion:${interactionId}`),
2645
      lifecycle,
2646
      pendingFeedbackAgentIds,
2647
      peerReportStatus,
2648
    });
2649
    const conclusion = InteractionConclusionSchema.parse({
2650
      ...base,
2651
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
2652
    });
2653
    const startedAt = reports
2654
      .map((report) => report.startedAt)
2655
      .filter((value): value is string => typeof value === 'string')
2656
      .sort()[0];
2657
    const receiptBase = {
2658
      receiptId: deterministicUuid(`interaction-receipt:${interactionId}`),
2659
      interactionId,
2660
      participants: [interaction.initiatorAgentId, interaction.responderAgentId],
2661
      agentVersions: Object.fromEntries(
2662
        reports.map((report) => [report.reportingAgentId, report.agentVersion]),
2663
      ),
2664
      contractHash: interaction.termsHash,
2665
      startedAt: startedAt ?? interaction.createdAt,
2666
      completedAt:
2667
        reports
2668
          .map((report) => report.completedAt)
2669
          .sort()
2670
          .at(-1) ?? conclusion.generatedAt,
2671
      outcome: conclusion.outcome,
2672
      commitmentsFulfilled: conclusion.criteria
2673
        .filter((criterion) => criterion.status === 'met')
2674
        .map((criterion) => criterion.criterion),
2675
      commitmentsMissed: conclusion.criteria
2676
        .filter((criterion) => criterion.status !== 'met')
2677
        .map((criterion) => criterion.criterion),
2678
      evidenceHashes: conclusion.evidenceReferences.map((reference) => canonicalHash(reference)),
2679
      policyWarnings: reports.flatMap((report) => report.blockers),
2680
      policyViolations: [],
2681
      disputeStatus: conclusion.consensus === 'conflicting' ? ('open' as const) : ('none' as const),
2682
      delegationChainHash: canonicalHash(interaction.contract.delegationRules),
2683
      unilateral: reports.length < 2,
2684
      provisional: lifecycle === 'provisional',
2685
      confidence: conclusion.confidence,
2686
      signatures: {},
2687
      completionReportIds: conclusion.reportIds,
2688
      conclusionId: conclusion.conclusionId,
2689
    };
2690
    const receipt = ReceiptSchema.parse({
2691
      ...receiptBase,
2692
      platformAttestation: attestSessionRecord(this.gatewaySecret(), receiptBase),
2693
    });
2694
    await Promise.all([
2695
      this.upsert(
2696
        String(interactionRow.initiator_operator_id),
2697
        'interaction_conclusion',
2698
        conclusion.conclusionId,
2699
        conclusion,
2700
      ),
2701
      this.upsert(
2702
        String(interactionRow.responder_operator_id),
2703
        'interaction_conclusion',
2704
        conclusion.conclusionId,
2705
        conclusion,
2706
      ),
2707
      this.upsert(
2708
        String(interactionRow.initiator_operator_id),
2709
        'receipt',
2710
        receipt.receiptId,
2711
        receipt,
2712
      ),
2713
      this.upsert(
2714
        String(interactionRow.responder_operator_id),
2715
        'receipt',
2716
        receipt.receiptId,
2717
        receipt,
2718
      ),
2719
    ]);
2720
    const learning =
2721
      lifecycle === 'final' && feedbackWindowClosed
2722
        ? await this.applyInteractionLearning({
2723
            interaction,
2724
            initiatorOperatorId: String(interactionRow.initiator_operator_id),
2725
            responderOperatorId: String(interactionRow.responder_operator_id),
2726
            reports,
2727
            feedback: visibleFeedback,
2728
            conclusion,
2729
          })
2730
        : undefined;
2731
    return {
2732
      released: true as const,
2733
      feedbackRevealed: feedbackWindowClosed,
2734
      conclusion,
2735
      receipt,
2736
      ...(learning ? { learning } : {}),
2737
    };
2738
  }
2739
2740
  async submitInteractionFeedback(
2741
    operatorId: string,
2742
    agentId: string,
2743
    value: InteractionFeedback,
2744
    submissionMethod: 'oauth_installation' | 'agent_access_token' | 'runtime_session',
2745
  ) {
2746
    const feedback = InteractionFeedbackSchema.parse(value);
2747
    if (feedback.platformAttestation)
2748
      throw new Error('Platform attestation is assigned by OpenClasp');
2749
    if (feedback.reviewerAgentId !== agentId)
2750
      throw new Error('Feedback reviewer does not match the authenticated agent');
2751
    const participant = await this.interactionParticipant(
2752
      operatorId,
2753
      feedback.interactionId,
2754
      agentId,
2755
    );
2756
    if (feedback.subjectAgentId !== participant.counterpartyAgentId)
2757
      throw new Error('Feedback subject does not match the interaction counterparty');
2758
    const requestRows = await this.sql`
2759
      SELECT payload FROM openclasp_records
2760
      WHERE operator_id = ${operatorId}
2761
        AND kind = 'feedback_request'
2762
        AND record_id = ${feedback.requestId}
2763
      LIMIT 1
2764
    `;
2765
    const request = FeedbackRequestSchema.parse(requestRows[0]?.payload);
2766
    if (
2767
      request.interactionId !== feedback.interactionId ||
2768
      request.reviewerAgentId !== feedback.reviewerAgentId ||
2769
      request.subjectAgentId !== feedback.subjectAgentId
2770
    )
2771
      throw new Error('Feedback does not match its request');
2772
    if (request.status !== 'pending') throw new Error('Feedback request is no longer pending');
2773
    if (Date.parse(request.dueAt) <= Date.now()) throw new Error('Feedback request has expired');
2774
    if (
2775
      request.requestedDimensions.some(
2776
        (dimension) => typeof feedback.ratings[dimension] !== 'number',
2777
      )
2778
    )
2779
      throw new Error('Feedback must rate every requested dimension');
2780
    const profiles = await this.sql`
2781
      SELECT payload FROM openclasp_records
2782
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
2783
      LIMIT 1
2784
    `;
2785
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
2786
    if (!profile || profile.agentVersion !== feedback.reviewerAgentVersion)
2787
      throw new Error('Feedback reviewer version does not match the registered agent');
2788
    let verifiedSubmissionMethod: InteractionFeedback['submissionMethod'] = submissionMethod;
2789
    if (feedback.signature) {
2790
      if (feedback.submissionMethod !== 'agent_signature')
2791
        throw new Error('Signed feedback must declare agent_signature submission');
2792
      const identities = await this.sql`
2793
        SELECT payload FROM openclasp_records
2794
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
2795
        LIMIT 1
2796
      `;
2797
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
2798
      if (
2799
        !identity.success ||
2800
        !verifyObject(feedback as unknown as Record<string, unknown>, identity.data.publicKey)
2801
      )
2802
        throw new Error('Feedback agent signature is invalid or unverifiable');
2803
      verifiedSubmissionMethod = 'agent_signature';
2804
    }
2805
    const unattested = InteractionFeedbackSchema.parse({
2806
      ...feedback,
2807
      submissionMethod: verifiedSubmissionMethod,
2808
    });
2809
    const stored = InteractionFeedbackSchema.parse({
2810
      ...unattested,
2811
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unattested),
2812
    });
2813
    const existing = await this.sql`
2814
      SELECT payload FROM openclasp_records
2815
      WHERE operator_id = ${operatorId}
2816
        AND kind = 'interaction_feedback'
2817
        AND record_id = ${stored.feedbackId}
2818
      LIMIT 1
2819
    `;
2820
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
2821
      throw new Error('Conflicting feedback ID');
2822
    await this.upsert(operatorId, 'interaction_feedback', stored.feedbackId, stored);
2823
    const requestBase = { ...request, status: 'submitted' as const };
2824
    delete requestBase.platformAttestation;
2825
    const updatedRequest = FeedbackRequestSchema.parse({
2826
      ...requestBase,
2827
      platformAttestation: attestSessionRecord(this.gatewaySecret(), requestBase),
2828
    });
2829
    await this.upsert(operatorId, 'feedback_request', request.requestId, updatedRequest);
2830
    const release = await this.finalizeInteractionConclusion(feedback.interactionId);
2831
    return {
2832
      feedbackId: stored.feedbackId,
2833
      status: 'submitted' as const,
2834
      revealed: release.released && release.feedbackRevealed,
2835
      ...(release.released ? { conclusion: release.conclusion } : {}),
2836
    };
2837
  }
2838
2839
  async processDueFeedback(now = new Date()) {
2840
    await this.ensureSchema();
2841
    const expiredInteractions = await this.sql`
2842
      UPDATE openclasp_federated_interactions
2843
      SET status = 'expired',
2844
        payload = jsonb_set(
2845
          jsonb_set(payload, '{status}', '"expired"'::jsonb),
2846
          '{updatedAt}', to_jsonb(${now.toISOString()}::text)
2847
        ),
2848
        updated_at = NOW()
2849
      WHERE status IN ('pending', 'active') AND expires_at <= ${now.toISOString()}
2850
      RETURNING interaction_id
2851
    `;
2852
    if (expiredInteractions.length) {
2853
      await this.sql`
2854
        UPDATE openclasp_live_sessions
2855
        SET status = 'failed', completed_at = COALESCE(completed_at, NOW()),
2856
          last_error = COALESCE(last_error, 'Interaction expired before bilateral completion')
2857
        WHERE status IN ('preparing', 'active')
2858
          AND interaction_id IN (
2859
            SELECT interaction_id FROM openclasp_federated_interactions
2860
            WHERE status = 'expired' AND expires_at <= ${now.toISOString()}
2861
          )
2862
      `;
2863
      await Promise.all(
2864
        expiredInteractions.flatMap((row) => {
2865
          const interactionId = String(row.interaction_id);
2866
          return [
2867
            this.journalFederatedInteraction(interactionId),
2868
            this.journalLiveSessionState(interactionId),
2869
          ];
2870
        }),
2871
      );
2872
    }
2873
    const [rows, backfillRows] = await Promise.all([
2874
      this.sql`
2875
        SELECT operator_id, record_id, payload FROM openclasp_records
2876
        WHERE kind = 'feedback_request'
2877
          AND payload->>'status' = 'pending'
2878
          AND (payload->>'dueAt')::timestamptz <= ${now.toISOString()}
2879
        LIMIT 500
2880
      `,
2881
      this.sql`
2882
        SELECT source.interaction_id
2883
        FROM (
2884
          SELECT conclusion.payload->>'interactionId' AS interaction_id
2885
          FROM openclasp_records conclusion
2886
          WHERE conclusion.kind = 'interaction_conclusion'
2887
            AND NOT EXISTS (
2888
              SELECT 1 FROM openclasp_records eligibility
2889
              WHERE eligibility.operator_id = conclusion.operator_id
2890
                AND eligibility.kind = 'learning_eligibility'
2891
                AND eligibility.payload->>'interactionId' = conclusion.payload->>'interactionId'
2892
            )
2893
          UNION
2894
          SELECT report.payload->>'interactionId' AS interaction_id
2895
          FROM openclasp_records report
2896
          WHERE report.kind = 'completion_report'
2897
            AND NOT EXISTS (
2898
              SELECT 1 FROM openclasp_records conclusion
2899
              WHERE conclusion.kind = 'interaction_conclusion'
2900
                AND conclusion.payload->>'interactionId' = report.payload->>'interactionId'
2901
            )
2902
        ) source
2903
        WHERE source.interaction_id IS NOT NULL
2904
        GROUP BY source.interaction_id
2905
        ORDER BY source.interaction_id ASC
2906
        LIMIT 200
2907
      `,
2908
    ]);
2909
    const interactions = new Set<string>();
2910
    for (const row of rows) {
2911
      const request = FeedbackRequestSchema.parse(row.payload);
2912
      const base = { ...request, status: 'expired' as const };
2913
      delete base.platformAttestation;
2914
      const expired = FeedbackRequestSchema.parse({
2915
        ...base,
2916
        platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
2917
      });
2918
      await this.upsert(
2919
        String(row.operator_id),
2920
        'feedback_request',
2921
        String(row.record_id),
2922
        expired,
2923
      );
2924
      interactions.add(request.interactionId);
2925
    }
2926
    for (const row of backfillRows) interactions.add(String(row.interaction_id));
2927
    let released = 0;
2928
    for (const interactionId of interactions) {
2929
      const result = await this.finalizeInteractionConclusion(interactionId);
2930
      if (result.released) released += 1;
2931
    }
2932
    return {
2933
      expired: rows.length,
2934
      interactionsExpired: expiredInteractions.length,
2935
      released,
2936
      backfilled: backfillRows.length,
2937
    };
2938
  }
2939
2940
  async recordSessionFeedback(token: string, value: InteractionFeedback) {
2941
    const feedback = InteractionFeedbackSchema.parse(value);
2942
    const grant = verifySessionGrant(this.gatewaySecret(), token);
2943
    if (
2944
      grant.interactionId !== feedback.interactionId ||
2945
      grant.senderAgentId !== feedback.reviewerAgentId ||
2946
      grant.recipientAgentId !== feedback.subjectAgentId
2947
    )
2948
      throw new Error('Session credential does not match the feedback');
2949
    const owners = await this.sql`
2950
      SELECT initiator_operator_id, responder_operator_id,
2951
        initiator_agent_id, responder_agent_id
2952
      FROM openclasp_federated_interactions
2953
      WHERE interaction_id = ${feedback.interactionId}
2954
      LIMIT 1
2955
    `;
2956
    const row = owners[0];
2957
    if (!row) throw new Error('Interaction not found');
2958
    const operatorId = String(
2959
      row.initiator_agent_id === feedback.reviewerAgentId
2960
        ? row.initiator_operator_id
2961
        : row.responder_operator_id,
2962
    );
2963
    return this.submitInteractionFeedback(
2964
      operatorId,
2965
      feedback.reviewerAgentId,
2966
      feedback,
2967
      'runtime_session',
2968
    );
2969
  }
2970
2971
  private async ownedTemporaryAgent(operatorId: string, agentId: string) {
2972
    const rows = await this.sql`
2973
      SELECT payload FROM openclasp_records
2974
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
2975
      LIMIT 1
2976
    `;
2977
    const profile = rows[0]?.payload as Partial<AgentProfile> | undefined;
2978
    if (!profile) throw new Error('Agent is not owned by this account');
2979
    const mode =
2980
      profile.agentMode ?? (profile.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat');
2981
    if (mode !== 'temporary_chat')
2982
      throw new Error('Hosted conversations are available only to temporary chat agents');
2983
    return profile;
2984
  }
2985
2986
  private hostedMessage(row: Record<string, any>): HostedMessage {
2987
    const decrypted = decryptGatewayPayload(this.gatewaySecret(), {
2988
      ciphertext: String(row.content_ciphertext),
2989
      iv: String(row.content_iv),
2990
      authTag: String(row.content_auth_tag),
2991
    }) as { content?: unknown };
2992
    return HostedMessageSchema.parse({
2993
      messageId: String(row.message_id),
2994
      threadId: String(row.thread_id),
2995
      interactionId: String(row.interaction_id),
2996
      senderAgentId: String(row.sender_agent_id),
2997
      recipientAgentId: String(row.recipient_agent_id),
2998
      contentType: 'text/plain',
2999
      content: decrypted.content,
3000
      contentHash: String(row.content_hash),
3001
      delivery: row.delivery,
3002
      createdAt: new Date(String(row.created_at)).toISOString(),
3003
      ...(row.read_at ? { readAt: new Date(String(row.read_at)).toISOString() } : {}),
3004
    });
3005
  }
3006
3007
  private async storeHostedMessage(input: {
3008
    interactionId: string;
3009
    senderAgentId: string;
3010
    recipientAgentId: string;
3011
    requestKey: string;
3012
    content: string;
3013
    delivery: 'accepted' | 'delivered';
3014
  }) {
3015
    const content = input.content.trim();
3016
    if (!content || content.length > 20_000)
3017
      throw new Error('Message must contain 1-20000 characters');
3018
    const sessions = await this.sql`
3019
      SELECT session.status, interaction.status AS interaction_status,
3020
        session.initiator_agent_id, session.responder_agent_id
3021
      FROM openclasp_live_sessions session
3022
      INNER JOIN openclasp_federated_interactions interaction
3023
        ON interaction.interaction_id = session.interaction_id
3024
      WHERE session.interaction_id = ${input.interactionId}
3025
      LIMIT 1
3026
    `;
3027
    const session = sessions[0];
3028
    if (!session || session.status !== 'active' || session.interaction_status !== 'active')
3029
      throw new Error('Active interaction not found');
3030
    const participants = new Set([
3031
      String(session.initiator_agent_id),
3032
      String(session.responder_agent_id),
3033
    ]);
3034
    if (!participants.has(input.senderAgentId) || !participants.has(input.recipientAgentId))
3035
      throw new Error('Message participants do not match the active interaction');
3036
    const now = new Date();
3037
    const threadId = input.interactionId;
3038
    const expiresAt = new Date(now.getTime() + 30 * 86_400_000).toISOString();
3039
    await this.sql`
3040
      INSERT INTO openclasp_hosted_threads(
3041
        thread_id, interaction_id, participant_a_agent_id, participant_b_agent_id,
3042
        status, expires_at
3043
      ) VALUES (
3044
        ${threadId}, ${input.interactionId}, ${String(session.initiator_agent_id)},
3045
        ${String(session.responder_agent_id)}, 'open', ${expiresAt}
3046
      )
3047
      ON CONFLICT (thread_id) DO UPDATE SET updated_at = NOW()
3048
      WHERE openclasp_hosted_threads.status = 'open'
3049
    `;
3050
    const thread = await this.sql`
3051
      SELECT status, expires_at FROM openclasp_hosted_threads WHERE thread_id = ${threadId}
3052
    `;
3053
    if (thread[0]?.status !== 'open') throw new Error('Hosted thread is closed');
3054
    if (Date.parse(String(thread[0].expires_at)) <= Date.now())
3055
      throw new Error('Hosted thread has expired');
3056
    const encrypted = encryptGatewayPayload(this.gatewaySecret(), { content });
3057
    const messageId = crypto.randomUUID();
3058
    const contentHash = canonicalHash(content);
3059
    const inserted = await this.sql`
3060
      INSERT INTO openclasp_hosted_messages(
3061
        message_id, thread_id, interaction_id, sender_agent_id, recipient_agent_id,
3062
        request_key, content_ciphertext, content_iv, content_auth_tag, content_hash, delivery
3063
      ) VALUES (
3064
        ${messageId}, ${threadId}, ${input.interactionId}, ${input.senderAgentId},
3065
        ${input.recipientAgentId}, ${input.requestKey}, ${encrypted.ciphertext}, ${encrypted.iv},
3066
        ${encrypted.authTag}, ${contentHash}, ${input.delivery}
3067
      )
3068
      ON CONFLICT (thread_id, sender_agent_id, request_key) DO NOTHING
3069
      RETURNING *
3070
    `;
3071
    if (!inserted[0]) {
3072
      const existing = await this.sql`
3073
        SELECT * FROM openclasp_hosted_messages
3074
        WHERE thread_id = ${threadId} AND sender_agent_id = ${input.senderAgentId}
3075
          AND request_key = ${input.requestKey}
3076
        LIMIT 1
3077
      `;
3078
      return { message: this.hostedMessage(existing[0]!), deduplicated: true };
3079
    }
3080
    const sequenceRows = await this.sql`
3081
      SELECT COALESCE(MAX(sequence), -1) + 1 AS sequence
3082
      FROM openclasp_live_session_events
3083
      WHERE interaction_id = ${input.interactionId} AND agent_id = ${input.senderAgentId}
3084
    `;
3085
    const event = LiveSessionEventSchema.parse({
3086
      eventId: crypto.randomUUID(),
3087
      interactionId: input.interactionId,
3088
      agentId: input.senderAgentId,
3089
      sequence: Number(sequenceRows[0]?.sequence ?? 0),
3090
      type: 'message_sent',
3091
      occurredAt: now.toISOString(),
3092
      messageHash: contentHash,
3093
      evidenceReferences: [],
3094
      details: { labels: ['hosted_temporary'], metrics: {}, flags: { rawStoredEncrypted: true } },
3095
    });
3096
    const eventWithAttestation = JSON.stringify({
3097
      ...event,
3098
      attestation: attestSessionRecord(this.gatewaySecret(), event),
3099
    });
3100
    await this.sql`
3101
      INSERT INTO openclasp_live_session_events(interaction_id, event_id, agent_id, sequence, event)
3102
      VALUES (${event.interactionId}, ${event.eventId}, ${event.agentId}, ${event.sequence}, ${eventWithAttestation}::jsonb)
3103
      ON CONFLICT DO NOTHING
3104
    `;
3105
    await this.journalLiveSessionEvent(JSON.parse(eventWithAttestation) as Record<string, unknown>);
3106
    return { message: this.hostedMessage(inserted[0]), deduplicated: false };
3107
  }
3108
3109
  async receiveTemporaryMessage(
3110
    token: string,
3111
    recipientAgentId: string,
3112
    requestKey: string,
3113
    content: string,
3114
  ) {
3115
    await this.ensureSchema();
3116
    const grant = verifySessionGrant(this.gatewaySecret(), token);
3117
    if (grant.recipientAgentId !== recipientAgentId)
3118
      throw new Error('Session credential is not valid for this temporary agent');
3119
    const recipient = await this.sessionParticipant(recipientAgentId);
3120
    if (recipient.mode !== 'temporary_chat')
3121
      throw new Error('Target is not a temporary chat agent');
3122
    const stored = await this.storeHostedMessage({
3123
      interactionId: grant.interactionId,
3124
      senderAgentId: grant.senderAgentId,
3125
      recipientAgentId,
3126
      requestKey,
3127
      content,
3128
      delivery: 'delivered',
3129
    });
3130
    return stored;
3131
  }
3132
3133
  async sendTemporaryMessage(
3134
    operatorId: string,
3135
    senderAgentId: string,
3136
    interactionId: string,
3137
    content: string,
3138
  ) {
3139
    await this.ensureSchema();
3140
    await this.ownedTemporaryAgent(operatorId, senderAgentId);
3141
    const session = await this.getLiveSession(operatorId, interactionId, senderAgentId);
3142
    const peer = await this.sessionParticipant(session.peer.agentId);
3143
    if (peer.mode !== 'persistent_runtime')
3144
      throw new Error('Temporary-to-temporary hosted conversations are not supported');
3145
    const requestKey = crypto.randomUUID();
3146
    const stored = await this.storeHostedMessage({
3147
      interactionId,
3148
      senderAgentId,
3149
      recipientAgentId: session.peer.agentId,
3150
      requestKey,
3151
      content,
3152
      delivery: 'accepted',
3153
    });
3154
    const response = await postRuntimeJson(
3155
      session.peer.endpoint,
3156
      {
3157
        jsonrpc: '2.0',
3158
        id: requestKey,
3159
        method: 'message/send',
3160
        params: {
3161
          message: {
3162
            role: 'user',
3163
            parts: [{ kind: 'text', text: content.trim() }],
3164
            metadata: {
3165
              [DEFAULT_EXTENSION_URI]: {
3166
                interactionId,
3167
                termsHash: session.contractHash,
3168
                initiatorAgentId:
3169
                  session.role === 'initiator' ? senderAgentId : session.peer.agentId,
3170
                responderAgentId:
3171
                  session.role === 'responder' ? senderAgentId : session.peer.agentId,
3172
              },
3173
            },
3174
          },
3175
        },
3176
      },
3177
      {
3178
        authorization: `Bearer ${session.peer.bearerToken}`,
3179
        'A2A-Extensions': DEFAULT_EXTENSION_URI,
3180
      },
3181
    );
3182
    if (response.status < 200 || response.status >= 300)
3183
      throw new Error(`Peer A2A endpoint returned HTTP ${response.status}`);
3184
    await this.sql`
3185
      UPDATE openclasp_hosted_messages SET delivery = 'delivered'
3186
      WHERE message_id = ${stored.message.messageId}
3187
    `;
3188
    return {
3189
      ...stored,
3190
      message: { ...stored.message, delivery: 'delivered' as const },
3191
      peerResponse: response.body,
3192
    };
3193
  }
3194
3195
  async listHostedThreads(operatorId: string, agentId: string): Promise<HostedThread[]> {
3196
    await this.ensureSchema();
3197
    await this.ownedTemporaryAgent(operatorId, agentId);
3198
    await this.sql`DELETE FROM openclasp_hosted_threads WHERE expires_at <= NOW()`;
3199
    const rows = await this.sql`
3200
      SELECT thread.*,
3201
        (SELECT COUNT(*) FROM openclasp_hosted_messages message
3202
          WHERE message.thread_id = thread.thread_id
3203
            AND message.recipient_agent_id = ${agentId} AND message.read_at IS NULL) AS unread_count
3204
      FROM openclasp_hosted_threads thread
3205
      WHERE ${agentId} IN (thread.participant_a_agent_id, thread.participant_b_agent_id)
3206
      ORDER BY thread.updated_at DESC
3207
    `;
3208
    return rows.map((row) =>
3209
      HostedThreadSchema.parse({
3210
        threadId: String(row.thread_id),
3211
        interactionId: String(row.interaction_id),
3212
        participantAgentIds: [
3213
          String(row.participant_a_agent_id),
3214
          String(row.participant_b_agent_id),
3215
        ],
3216
        status: row.status,
3217
        privacyMode: 'openclasp_hosted_temporary',
3218
        unreadCount: Number(row.unread_count ?? 0),
3219
        createdAt: new Date(String(row.created_at)).toISOString(),
3220
        updatedAt: new Date(String(row.updated_at)).toISOString(),
3221
        expiresAt: new Date(String(row.expires_at)).toISOString(),
3222
      }),
3223
    );
3224
  }
3225
3226
  async getHostedThread(operatorId: string, agentId: string, threadId: string) {
3227
    const threads = await this.listHostedThreads(operatorId, agentId);
3228
    const thread = threads.find((value) => value.threadId === threadId);
3229
    if (!thread) throw new Error('Hosted thread not found');
3230
    const rows = await this.sql`
3231
      SELECT * FROM openclasp_hosted_messages
3232
      WHERE thread_id = ${threadId} ORDER BY created_at ASC
3233
    `;
3234
    const peerAgentId = thread.participantAgentIds.find((value) => value !== agentId);
3235
    if (!peerAgentId) throw new Error('Hosted thread has invalid participants');
3236
    const peer = await this.sessionParticipant(peerAgentId);
3237
    const interactions = await this.sql`
3238
      SELECT payload FROM openclasp_federated_interactions
3239
      WHERE interaction_id = ${thread.interactionId} LIMIT 1
3240
    `;
3241
    const interaction = FederatedInteractionSchema.parse(interactions[0]?.payload);
3242
    const insights = await this.privateCounterpartyInsights(
3243
      operatorId,
3244
      peer,
3245
      interaction.contract.taskCategory,
3246
    );
3247
    return { thread, messages: rows.map((row) => this.hostedMessage(row)), insights };
3248
  }
3249
3250
  async markHostedThreadRead(operatorId: string, agentId: string, threadId: string) {
3251
    await this.getHostedThread(operatorId, agentId, threadId);
3252
    const rows = await this.sql`
3253
      UPDATE openclasp_hosted_messages
3254
      SET delivery = 'read', read_at = COALESCE(read_at, NOW())
3255
      WHERE thread_id = ${threadId} AND recipient_agent_id = ${agentId} AND read_at IS NULL
3256
      RETURNING message_id
3257
    `;
3258
    return { threadId, markedRead: rows.length };
3259
  }
3260
3261
  async closeHostedThread(operatorId: string, agentId: string, threadId: string) {
3262
    await this.getHostedThread(operatorId, agentId, threadId);
3263
    await this.sql`
3264
      UPDATE openclasp_hosted_threads SET status = 'closed', updated_at = NOW()
3265
      WHERE thread_id = ${threadId}
3266
    `;
3267
    return { threadId, status: 'closed' as const };
3268
  }
3269
3270
  async recordLiveSessionEvent(token: string, value: LiveSessionEvent) {
3271
    await this.ensureSchema();
3272
    const event = LiveSessionEventSchema.parse(value);
3273
    const grant = verifySessionGrant(this.gatewaySecret(), token);
3274
    if (grant.interactionId !== event.interactionId || grant.senderAgentId !== event.agentId)
3275
      throw new Error('Session credential does not match the event');
3276
    const sessions = await this.sql`
3277
      SELECT 1 FROM openclasp_live_sessions
3278
      WHERE interaction_id = ${event.interactionId}
3279
        AND status = 'active'
3280
        AND ${event.agentId} IN (initiator_agent_id, responder_agent_id)
3281
    `;
3282
    if (!sessions.length) throw new Error('Active live session not found');
3283
    const attestation = attestSessionRecord(this.gatewaySecret(), event);
3284
    const encoded = JSON.stringify({ ...event, attestation });
3285
    const rows = await this.sql`
3286
      INSERT INTO openclasp_live_session_events(
3287
        interaction_id, event_id, agent_id, sequence, event
3288
      ) SELECT ${event.interactionId}, ${event.eventId}, ${event.agentId}, ${event.sequence}, ${encoded}::jsonb
3289
      WHERE (
3290
        SELECT COUNT(*) FROM openclasp_live_session_events
3291
        WHERE interaction_id = ${event.interactionId}
3292
      ) < 1000
3293
      ON CONFLICT DO NOTHING
3294
      RETURNING event_id
3295
    `;
3296
    if (rows.length)
3297
      await this.journalLiveSessionEvent({
3298
        ...event,
3299
        attestation,
3300
      });
3301
    if (event.type === 'session_completed' || event.type === 'session_failed') {
3302
      const terminal = await this.sql`
3303
        SELECT COUNT(DISTINCT agent_id) AS count
3304
        FROM openclasp_live_session_events
3305
        WHERE interaction_id = ${event.interactionId}
3306
          AND event->>'type' IN ('session_completed', 'session_failed')
3307
      `;
3308
      if (Number(terminal[0]?.count ?? 0) >= 2) {
3309
        await this.sql`
3310
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
3311
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
3312
        `;
3313
        const completedAt = new Date().toISOString();
3314
        await this.sql`
3315
          UPDATE openclasp_federated_interactions
3316
          SET status = 'completed',
3317
            payload = jsonb_set(
3318
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
3319
              '{updatedAt}', to_jsonb(${completedAt}::text)
3320
            ),
3321
            updated_at = NOW()
3322
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
3323
        `;
3324
        await Promise.all([
3325
          this.journalLiveSessionState(event.interactionId),
3326
          this.journalFederatedInteraction(event.interactionId),
3327
        ]);
3328
      }
3329
    }
3330
    return {
3331
      recorded: rows.length > 0,
3332
      deduplicated: rows.length === 0,
3333
      eventId: event.eventId,
3334
      attestation,
3335
    };
3336
  }
3337
3338
  async createFederatedInteraction(
3339
    operatorId: string,
3340
    value: FederatedInteraction,
3341
  ): Promise<FederatedInteraction> {
3342
    await this.ensureSchema();
3343
    let interaction = FederatedInteractionSchema.parse(value);
3344
    if (interaction.status !== 'pending') throw new Error('New interactions must be pending');
3345
    if (interaction.contractRevision !== 1 || interaction.contractRevisions.length)
3346
      throw new Error('New interactions cannot supply contract revision history');
3347
    if (interaction.contract.interactionId !== interaction.interactionId)
3348
      throw new Error('Contract interaction ID does not match');
3349
    if (canonicalHash(interaction.contract) !== interaction.termsHash)
3350
      throw new Error('Contract hash does not match the immutable terms');
3351
    if (
3352
      interaction.contract.parties.length !== 2 ||
3353
      interaction.contract.parties[0] !== interaction.initiatorAgentId ||
3354
      interaction.contract.parties[1] !== interaction.responderAgentId
3355
    )
3356
      throw new Error('Contract parties do not match the interaction participants');
3357
    const acceptanceEntries = Object.entries(interaction.acceptances);
3358
    if (
3359
      acceptanceEntries.length !== 1 ||
3360
      acceptanceEntries[0]?.[0] !== interaction.initiatorAgentId ||
3361
      acceptanceEntries[0]?.[1].agentId !== interaction.initiatorAgentId ||
3362
      acceptanceEntries[0]?.[1].termsHash !== interaction.termsHash ||
3363
      !['oauth_installation', 'oauth_account'].includes(acceptanceEntries[0]?.[1].method ?? '')
3364
    )
3365
      throw new Error('A pending interaction requires only the initiator acceptance');
3366
    if (Date.parse(interaction.expiresAt) <= Date.now())
3367
      throw new Error('Interaction expiry must be in the future');
3368
    interaction = withContractRevisionHistory(interaction);
3369
    const owners = await this.sql`
3370
      SELECT agent_id, operator_id, card FROM openclasp_public_agents
3371
      WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
3372
    `;
3373
    const initiator = owners.find((row) => row.agent_id === interaction.initiatorAgentId);
3374
    const responder = owners.find((row) => row.agent_id === interaction.responderAgentId);
3375
    if (!initiator || initiator.operator_id !== operatorId)
3376
      throw new Error('The initiating agent must be published and owned by this account');
3377
    if (!responder) throw new Error('Responder is not a published OpenClasp agent');
3378
    if (responder.operator_id === operatorId)
3379
      throw new Error('Federated interactions require agents from different accounts');
3380
    const responderCard = normalizePublicAgentCard(responder.card);
3381
    if (
3382
      !responderCard.transports.some(
3383
        (transport) =>
3384
          transport.endpoint === interaction.responderTransport.endpoint &&
3385
          transport.protocolBinding === interaction.responderTransport.protocolBinding,
3386
      )
3387
    )
3388
      throw new Error('Responder transport does not match its published Agent Card');
3389
    const encoded = JSON.stringify(interaction);
3390
    const rows = await this.sql`
3391
      INSERT INTO openclasp_federated_interactions(
3392
        interaction_id, initiator_operator_id, responder_operator_id,
3393
        initiator_agent_id, responder_agent_id, status, payload, expires_at
3394
      ) VALUES (
3395
        ${interaction.interactionId}, ${operatorId}, ${responder.operator_id},
3396
        ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
3397
        ${interaction.status}, ${encoded}::jsonb, ${interaction.expiresAt}
3398
      )
3399
      RETURNING payload
3400
    `;
3401
    const stored = FederatedInteractionSchema.parse(rows[0]?.payload);
3402
    await this.journalFederatedInteraction(stored.interactionId);
3403
    const profiles = await this.sql`
3404
      SELECT payload FROM openclasp_records
3405
      WHERE operator_id = ${responder.operator_id}
3406
        AND kind = 'agent_profile'
3407
        AND record_id = ${interaction.responderAgentId}
3408
      LIMIT 1
3409
    `;
3410
    const responderProfile = profiles[0]?.payload as AgentProfile | undefined;
3411
    if (responderProfile && canAutoAcceptInteraction(responderProfile, stored))
3412
      return this.respondToFederatedInteraction(
3413
        String(responder.operator_id),
3414
        stored.interactionId,
3415
        stored.responderAgentId,
3416
        'accept',
3417
        'policy_auto_accept',
3418
      );
3419
    return stored;
3420
  }
3421
3422
  async listFederatedInteractions(operatorId: string): Promise<FederatedInteraction[]> {
3423
    await this.ensureSchema();
3424
    const expired = await this.sql`
3425
      UPDATE openclasp_federated_interactions
3426
      SET status = 'expired', payload = jsonb_set(payload, '{status}', '"expired"'::jsonb), updated_at = NOW()
3427
      WHERE status = 'pending' AND expires_at <= NOW()
3428
      RETURNING interaction_id
3429
    `;
3430
    await Promise.all(
3431
      expired.map((row) => this.journalFederatedInteraction(String(row.interaction_id))),
3432
    );
3433
    const rows = await this.sql`
3434
      SELECT payload FROM openclasp_federated_interactions
3435
      WHERE initiator_operator_id = ${operatorId} OR responder_operator_id = ${operatorId}
3436
      ORDER BY updated_at DESC
3437
    `;
3438
    return rows.map((row) =>
3439
      withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload)),
3440
    );
3441
  }
3442
3443
  async getFederatedInteraction(
3444
    operatorId: string,
3445
    interactionId: string,
3446
  ): Promise<FederatedInteraction | undefined> {
3447
    const values = await this.listFederatedInteractions(operatorId);
3448
    return values.find((value) => value.interactionId === interactionId);
3449
  }
3450
3451
  async respondToFederatedInteraction(
3452
    operatorId: string,
3453
    interactionId: string,
3454
    agentId: string,
3455
    decision: 'accept' | 'reject',
3456
    method: 'oauth_installation' | 'oauth_account' | 'policy_auto_accept' = 'oauth_account',
3457
  ): Promise<FederatedInteraction> {
3458
    await this.ensureSchema();
3459
    const rows = await this.sql`
3460
      SELECT payload, responder_operator_id, responder_agent_id
3461
      FROM openclasp_federated_interactions WHERE interaction_id = ${interactionId}
3462
    `;
3463
    const row = rows[0];
3464
    if (!row) throw new Error('Interaction invitation not found');
3465
    if (row.responder_operator_id !== operatorId || row.responder_agent_id !== agentId)
3466
      throw new Error('Only the invited agent may respond');
3467
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
3468
    if (current.status !== 'pending') throw new Error('Invitation is no longer pending');
3469
    const now = new Date().toISOString();
3470
    if (Date.parse(current.expiresAt) <= Date.now()) {
3471
      const expired = { ...current, status: 'expired' as const, updatedAt: now };
3472
      await this.updateFederatedInteraction(
3473
        interactionId,
3474
        current.status,
3475
        expired,
3476
        current.updatedAt,
3477
      );
3478
      throw new Error('Invitation has expired');
3479
    }
3480
    const proposed = openContractRevision(current);
3481
    if (!proposed || proposed.termsHash !== current.termsHash)
3482
      throw new Error('No current contract proposal is available');
3483
    const acceptance = {
3484
      agentId,
3485
      method,
3486
      termsHash: current.termsHash,
3487
      acceptedAt: now,
3488
    };
3489
    const proposalBase: ContractRevision = {
3490
      ...proposed,
3491
      status: decision === 'accept' ? 'accepted' : 'rejected',
3492
      acceptances:
3493
        decision === 'accept'
3494
          ? { ...proposed.acceptances, [agentId]: acceptance }
3495
          : proposed.acceptances,
3496
      updatedAt: now,
3497
    };
3498
    const resolvedProposal =
3499
      decision === 'accept'
3500
        ? ContractRevisionSchema.parse({
3501
            ...proposalBase,
3502
            platformAttestation: attestSessionRecord(this.gatewaySecret(), proposalBase),
3503
          })
3504
        : proposalBase;
3505
    const next = FederatedInteractionSchema.parse({
3506
      ...current,
3507
      status: decision === 'accept' ? 'active' : 'rejected',
3508
      updatedAt: now,
3509
      acceptances: decision === 'accept' ? resolvedProposal.acceptances : current.acceptances,
3510
      contractRevisions: current.contractRevisions.map((revision) =>
3511
        revision.revisionId === resolvedProposal.revisionId ? resolvedProposal : revision,
3512
      ),
3513
    });
3514
    if (decision === 'accept') {
3515
      try {
3516
        await this.brokerLiveSession(next);
3517
      } catch (error) {
3518
        const reason = error instanceof Error ? error.message.slice(0, 500) : 'Live session failed';
3519
        await this.sql`
3520
          UPDATE openclasp_agent_runtimes SET last_error = ${reason}, updated_at = NOW()
3521
          WHERE agent_id IN (${current.initiatorAgentId}, ${current.responderAgentId})
3522
        `;
3523
        throw error;
3524
      }
3525
    }
3526
    const updated = await this.updateFederatedInteraction(
3527
      interactionId,
3528
      current.status,
3529
      next,
3530
      current.updatedAt,
3531
    );
3532
    if (!updated) throw new Error('Invitation was already handled');
3533
    return next;
3534
  }
3535
3536
  async proposeContractRevision(
3537
    operatorId: string,
3538
    interactionId: string,
3539
    agentId: string,
3540
    contract: InteractionContract,
3541
    expectedTermsHash?: string,
3542
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
3543
  ): Promise<FederatedInteraction> {
3544
    await this.ensureSchema();
3545
    const rows = await this.sql`
3546
      SELECT payload, initiator_operator_id, responder_operator_id,
3547
        initiator_agent_id, responder_agent_id
3548
      FROM openclasp_federated_interactions
3549
      WHERE interaction_id = ${interactionId}
3550
    `;
3551
    const row = rows[0];
3552
    if (!row) throw new Error('Interaction not found');
3553
    const ownsParticipant =
3554
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
3555
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
3556
    if (!ownsParticipant) throw new Error('Only a participating agent may propose contract terms');
3557
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
3558
    if (!['pending', 'active'].includes(current.status))
3559
      throw new Error('This interaction no longer accepts contract proposals');
3560
    const parsedContract = InteractionContractSchema.parse(contract);
3561
    if (parsedContract.interactionId !== interactionId)
3562
      throw new Error('Contract interaction ID does not match');
3563
    if (
3564
      parsedContract.parties.length !== 2 ||
3565
      parsedContract.parties[0] !== current.initiatorAgentId ||
3566
      parsedContract.parties[1] !== current.responderAgentId
3567
    )
3568
      throw new Error('Contract parties cannot change during negotiation');
3569
    const open = openContractRevision(current);
3570
    const expected = open?.termsHash ?? current.termsHash;
3571
    if (expectedTermsHash && expectedTermsHash !== expected)
3572
      throw new Error('Contract terms changed; fetch the interaction before countering');
3573
    const termsHash = canonicalHash(parsedContract);
3574
    if (termsHash === expected) throw new Error('Proposed terms are unchanged');
3575
    const now = new Date().toISOString();
3576
    const revisionNumber =
3577
      Math.max(
3578
        current.contractRevision,
3579
        ...current.contractRevisions.map((item) => item.revision),
3580
      ) + 1;
3581
    const acceptance = {
3582
      agentId,
3583
      method,
3584
      termsHash,
3585
      acceptedAt: now,
3586
    } as const;
3587
    const revision = ContractRevisionSchema.parse({
3588
      revisionId: crypto.randomUUID(),
3589
      interactionId,
3590
      revision: revisionNumber,
3591
      previousTermsHash: expected,
3592
      termsHash,
3593
      contract: parsedContract,
3594
      proposedByAgentId: agentId,
3595
      status: 'proposed',
3596
      acceptances: { [agentId]: acceptance },
3597
      createdAt: now,
3598
      updatedAt: now,
3599
    });
3600
    const revisions = [
3601
      ...current.contractRevisions.map((item) =>
3602
        item.status === 'proposed'
3603
          ? ContractRevisionSchema.parse({ ...item, status: 'superseded', updatedAt: now })
3604
          : item,
3605
      ),
3606
      revision,
3607
    ];
3608
    const next = FederatedInteractionSchema.parse({
3609
      ...current,
3610
      ...(current.status === 'pending'
3611
        ? {
3612
            contract: parsedContract,
3613
            termsHash,
3614
            acceptances: revision.acceptances,
3615
            contractRevision: revisionNumber,
3616
          }
3617
        : {}),
3618
      contractRevisions: revisions,
3619
      updatedAt: now,
3620
    });
3621
    const updated = await this.updateFederatedInteraction(
3622
      interactionId,
3623
      current.status,
3624
      next,
3625
      current.updatedAt,
3626
    );
3627
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
3628
    return next;
3629
  }
3630
3631
  async respondToContractRevision(
3632
    operatorId: string,
3633
    interactionId: string,
3634
    agentId: string,
3635
    revisionId: string,
3636
    decision: 'accept' | 'reject',
3637
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
3638
  ): Promise<FederatedInteraction> {
3639
    await this.ensureSchema();
3640
    const rows = await this.sql`
3641
      SELECT payload, initiator_operator_id, responder_operator_id,
3642
        initiator_agent_id, responder_agent_id
3643
      FROM openclasp_federated_interactions
3644
      WHERE interaction_id = ${interactionId}
3645
    `;
3646
    const row = rows[0];
3647
    if (!row) throw new Error('Interaction not found');
3648
    const ownsParticipant =
3649
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
3650
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
3651
    if (!ownsParticipant) throw new Error('Only a participating agent may respond to terms');
3652
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
3653
    if (!['pending', 'active'].includes(current.status))
3654
      throw new Error('This interaction no longer accepts contract responses');
3655
    const proposal = openContractRevision(current);
3656
    if (!proposal || proposal.revisionId !== revisionId)
3657
      throw new Error('Contract proposal is no longer current');
3658
    if (proposal.acceptances[agentId]) throw new Error('Agent already accepted this proposal');
3659
    const now = new Date().toISOString();
3660
    const acceptances =
3661
      decision === 'accept'
3662
        ? {
3663
            ...proposal.acceptances,
3664
            [agentId]: { agentId, method, termsHash: proposal.termsHash, acceptedAt: now },
3665
          }
3666
        : proposal.acceptances;
3667
    const fullyAccepted = current.contract.parties.every((party) => party in acceptances);
3668
    const revisionBase = ContractRevisionSchema.parse({
3669
      ...proposal,
3670
      status: decision === 'reject' ? 'rejected' : fullyAccepted ? 'accepted' : 'proposed',
3671
      acceptances,
3672
      updatedAt: now,
3673
    });
3674
    const revision =
3675
      fullyAccepted && decision === 'accept'
3676
        ? ContractRevisionSchema.parse({
3677
            ...revisionBase,
3678
            platformAttestation: attestSessionRecord(this.gatewaySecret(), revisionBase),
3679
          })
3680
        : revisionBase;
3681
    const next = FederatedInteractionSchema.parse({
3682
      ...current,
3683
      status:
3684
        current.status === 'pending'
3685
          ? decision === 'reject'
3686
            ? 'rejected'
3687
            : fullyAccepted
3688
              ? 'active'
3689
              : 'pending'
3690
          : 'active',
3691
      ...(fullyAccepted
3692
        ? {
3693
            contract: revision.contract,
3694
            termsHash: revision.termsHash,
3695
            acceptances: revision.acceptances,
3696
            contractRevision: revision.revision,
3697
          }
3698
        : {}),
3699
      contractRevisions: current.contractRevisions.map((item) =>
3700
        item.revisionId === revision.revisionId ? revision : item,
3701
      ),
3702
      updatedAt: now,
3703
    });
3704
    if (current.status === 'pending' && decision === 'accept' && fullyAccepted)
3705
      await this.brokerLiveSession(next);
3706
    const updated = await this.updateFederatedInteraction(
3707
      interactionId,
3708
      current.status,
3709
      next,
3710
      current.updatedAt,
3711
    );
3712
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
3713
    return next;
3714
  }
3715
3716
  private async updateFederatedInteraction(
3717
    interactionId: string,
3718
    expectedStatus: string,
3719
    value: FederatedInteraction,
3720
    expectedUpdatedAt?: string,
3721
  ): Promise<boolean> {
3722
    const encoded = JSON.stringify(FederatedInteractionSchema.parse(value));
3723
    const rows = await this.sql`
3724
      UPDATE openclasp_federated_interactions
3725
      SET status = ${value.status}, payload = ${encoded}::jsonb, updated_at = NOW()
3726
      WHERE interaction_id = ${interactionId} AND status = ${expectedStatus}
3727
        AND (${expectedUpdatedAt ?? null}::text IS NULL OR payload->>'updatedAt' = ${expectedUpdatedAt ?? null})
3728
      RETURNING interaction_id
3729
    `;
3730
    if (rows.length) await this.journalFederatedInteraction(interactionId);
3731
    return rows.length > 0;
3732
  }
3733
3734
  async getSettings(operatorId: string): Promise<AccountSettings> {
3735
    await this.ensureSchema();
3736
    const rows = await this.sql`
3737
      SELECT display_name, contribution_enabled, retention_days, evidence_sharing
3738
      FROM openclasp_account_settings
3739
      WHERE operator_id = ${operatorId}
3740
    `;
3741
    const row = rows[0] as
3742
      | {
3743
          display_name: string;
3744
          contribution_enabled: boolean;
3745
          retention_days: number;
3746
          evidence_sharing: AccountSettings['evidenceSharing'];
3747
        }
3748
      | undefined;
3749
    return row
3750
      ? {
3751
          displayName: row.display_name,
3752
          contributionEnabled: row.contribution_enabled,
3753
          retentionDays: row.retention_days,
3754
          evidenceSharing: row.evidence_sharing,
3755
          rawConversationsStored: false,
3756
        }
3757
      : defaults;
3758
  }
3759
3760
  async saveSettings(
3761
    operatorId: string,
3762
    settings: Omit<AccountSettings, 'rawConversationsStored'>,
3763
  ): Promise<AccountSettings> {
3764
    await this.ensureSchema();
3765
    await this.sql`
3766
      INSERT INTO openclasp_account_settings(
3767
        operator_id, display_name, contribution_enabled, retention_days, evidence_sharing
3768
      ) VALUES (
3769
        ${operatorId}, ${settings.displayName}, ${settings.contributionEnabled},
3770
        ${settings.retentionDays}, ${settings.evidenceSharing}
3771
      )
3772
      ON CONFLICT (operator_id) DO UPDATE SET
3773
        display_name = EXCLUDED.display_name,
3774
        contribution_enabled = EXCLUDED.contribution_enabled,
3775
        retention_days = EXCLUDED.retention_days,
3776
        evidence_sharing = EXCLUDED.evidence_sharing,
3777
        updated_at = NOW()
3778
    `;
3779
    return { ...settings, rawConversationsStored: false };
3780
  }
3781
}