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

Viewing files at commit 3f140fdd2001Commit detailsView latest

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