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

Viewing files at commit 28cda791c50eCommit detailsView latest

packages/persistence/src/hosted.ts 3829 lines · 146.9 KB · typescript
1
import { neon, type NeonQueryFunction } from '@neondatabase/serverless';
2
import { createHash, randomBytes } from 'node:crypto';
3
import {
4
  buildCounterpartyBrief,
5
  buildInteractionConclusion,
6
  deriveBehaviouralObservations,
7
  evaluateLearningEligibility,
8
  summarizeContextualReliability,
9
  updateContextualBehaviouralProfile,
10
} from '../../core/src/index.js';
11
import {
12
  AgentIdentitySchema,
13
  AgentResolutionSchema,
14
  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(
1320
        (card) =>
1321
          card.agentMode === 'persistent_runtime' &&
1322
          card.transports.some((transport) => transport.managedBy === 'agent'),
1323
      )
1324
      .filter((card) => !ownedIds.has(card.agentId))
1325
      .map((card) => {
1326
        const intelligence = summaryFor(card.agentId);
1327
        const capabilityMatch = card.capabilities.some((capability) =>
1328
          capability.toLowerCase().includes(normalizedCategory),
1329
        );
1330
        const online = card.presence?.status === 'online';
1331
        const fitScore = Math.min(
1332
          1,
1333
          0.25 +
1334
            (capabilityMatch ? 0.35 : 0) +
1335
            (online ? 0.1 : 0) +
1336
            (intelligence ? intelligence.score * intelligence.confidence.value * 0.3 : 0),
1337
        );
1338
        return {
1339
          card,
1340
          taskCategory,
1341
          ...(intelligence ? { contextualReliability: intelligence } : {}),
1342
          match: {
1343
            score: fitScore,
1344
            label: fitScore >= 0.72 ? 'strong' : fitScore >= 0.48 ? 'possible' : 'unproven',
1345
            reasons: [
1346
              capabilityMatch
1347
                ? `Published capability matches ${taskCategory}`
1348
                : `No explicit ${taskCategory} capability`,
1349
              intelligence
1350
                ? `${intelligence.confidence.evidenceCount} verified outcomes in private history`
1351
                : 'No verified private history for this task',
1352
              online ? 'Currently online' : 'Currently offline',
1353
            ],
1354
          },
1355
        };
1356
      })
1357
      .sort((left, right) => right.match.score - left.match.score);
1358
  }
1359
1360
  async touchAgentPresence(operatorId: string, agentId: string): Promise<AgentPresence> {
1361
    await this.ensureSchema();
1362
    const owned = await this.sql`
1363
      SELECT 1 FROM openclasp_records
1364
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1365
    `;
1366
    if (!owned.length) throw new Error('Agent is not owned by this account');
1367
    const lastSeenAt = new Date().toISOString();
1368
    await this.upsert(operatorId, 'presence', agentId, { lastSeenAt });
1369
    await this.sql`
1370
      UPDATE openclasp_agent_runtimes SET last_seen_at = ${lastSeenAt}, updated_at = NOW()
1371
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId} AND status = 'verified'
1372
    `;
1373
    return resolveAgentPresence(lastSeenAt);
1374
  }
1375
1376
  async getAgentPresence(agentId: string): Promise<AgentPresence> {
1377
    await this.ensureSchema();
1378
    const rows = await this.sql`
1379
      SELECT records.payload
1380
      FROM openclasp_public_agents agents
1381
      LEFT JOIN openclasp_records records
1382
        ON records.operator_id = agents.operator_id
1383
        AND records.kind = 'presence'
1384
        AND records.record_id = agents.agent_id
1385
      WHERE agents.agent_id = ${agentId}
1386
      LIMIT 1
1387
    `;
1388
    if (!rows.length) throw new Error('Published agent not found');
1389
    const lastSeenAt = rows[0]?.payload?.lastSeenAt;
1390
    return resolveAgentPresence(typeof lastSeenAt === 'string' ? lastSeenAt : undefined);
1391
  }
1392
1393
  async listAgentRuntimes(operatorId: string) {
1394
    await this.ensureSchema();
1395
    const rows = await this.sql`
1396
      SELECT agent_id, endpoint, a2a_endpoint, status, verified_at, last_seen_at, last_error, updated_at
1397
      FROM openclasp_agent_runtimes
1398
      WHERE operator_id = ${operatorId}
1399
      ORDER BY updated_at DESC
1400
    `;
1401
    return rows.map((row) => ({
1402
      agentId: String(row.agent_id),
1403
      endpoint: String(row.endpoint),
1404
      a2aEndpoint: String(row.a2a_endpoint ?? row.endpoint),
1405
      status: row.status as 'verified' | 'disabled',
1406
      verifiedAt: new Date(String(row.verified_at)).toISOString(),
1407
      ...(row.last_seen_at ? { lastSeenAt: new Date(String(row.last_seen_at)).toISOString() } : {}),
1408
      ...(row.last_error ? { lastError: String(row.last_error) } : {}),
1409
      updatedAt: new Date(String(row.updated_at)).toISOString(),
1410
    }));
1411
  }
1412
1413
  async registerAgentRuntime(operatorId: string, agentId: string, endpoint: string) {
1414
    await this.ensureSchema();
1415
    const owned = await this.sql`
1416
      SELECT 1 FROM openclasp_records
1417
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1418
    `;
1419
    if (!owned.length) throw new Error('Agent is not owned by this account');
1420
    const challenge = crypto.randomUUID();
1421
    const verification = await postRuntimeJson(endpoint, {
1422
      type: 'openclasp.runtime.verify',
1423
      version: '1',
1424
      agentId,
1425
      challenge,
1426
    });
1427
    if (
1428
      verification.status < 200 ||
1429
      verification.status >= 300 ||
1430
      !verification.body ||
1431
      typeof verification.body !== 'object' ||
1432
      (verification.body as { type?: unknown }).type !== 'openclasp.runtime.verified' ||
1433
      (verification.body as { version?: unknown }).version !== '1' ||
1434
      (verification.body as { agentId?: unknown }).agentId !== agentId ||
1435
      (verification.body as { challenge?: unknown }).challenge !== challenge
1436
    )
1437
      throw new Error('Runtime endpoint did not return the verification challenge');
1438
    const a2aEndpointValue = (verification.body as { a2aEndpoint?: unknown }).a2aEndpoint;
1439
    const a2aEndpoint =
1440
      typeof a2aEndpointValue === 'string' && a2aEndpointValue ? a2aEndpointValue : endpoint;
1441
    await resolvePublicRuntimeEndpoint(a2aEndpoint);
1442
    const runtimeSecret = randomBytes(32).toString('base64url');
1443
    const encrypted = encryptGatewayPayload(this.gatewaySecret(), runtimeSecret);
1444
    const verifiedAt = new Date().toISOString();
1445
    const saved = await this.sql`
1446
      INSERT INTO openclasp_agent_runtimes(
1447
        agent_id, operator_id, endpoint, a2a_endpoint, secret_ciphertext, secret_iv, secret_auth_tag,
1448
        status, verified_at, last_seen_at, updated_at
1449
      ) VALUES (
1450
        ${agentId}, ${operatorId}, ${endpoint}, ${a2aEndpoint}, ${encrypted.ciphertext},
1451
        ${encrypted.iv}, ${encrypted.authTag}, 'verified', ${verifiedAt}, ${verifiedAt}, NOW()
1452
      )
1453
      ON CONFLICT (agent_id) DO UPDATE SET
1454
        operator_id = EXCLUDED.operator_id,
1455
        endpoint = EXCLUDED.endpoint,
1456
        a2a_endpoint = EXCLUDED.a2a_endpoint,
1457
        secret_ciphertext = EXCLUDED.secret_ciphertext,
1458
        secret_iv = EXCLUDED.secret_iv,
1459
        secret_auth_tag = EXCLUDED.secret_auth_tag,
1460
        status = 'verified',
1461
        verified_at = EXCLUDED.verified_at,
1462
        last_seen_at = EXCLUDED.last_seen_at,
1463
        last_error = NULL,
1464
        updated_at = NOW()
1465
      WHERE openclasp_agent_runtimes.operator_id = ${operatorId}
1466
      RETURNING agent_id
1467
    `;
1468
    if (!saved.length) throw new Error('Agent runtime is owned by another account');
1469
    await this.sql`
1470
      UPDATE openclasp_public_agents
1471
      SET card = jsonb_set(
1472
          jsonb_set(
1473
            card,
1474
            '{transports}',
1475
            ${JSON.stringify([
1476
              {
1477
                protocol: 'A2A/1.0',
1478
                protocolBinding: 'JSONRPC',
1479
                endpoint: a2aEndpoint,
1480
                managedBy: 'agent',
1481
              },
1482
            ])}::jsonb
1483
          ),
1484
          '{agentMode}', '"persistent_runtime"'::jsonb
1485
        ), updated_at = NOW()
1486
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
1487
    `;
1488
    await this.sql`
1489
      UPDATE openclasp_records
1490
      SET payload = jsonb_set(
1491
          jsonb_set(payload, '{a2aEndpoint}', to_jsonb(${a2aEndpoint}::text)),
1492
          '{agentMode}', '"persistent_runtime"'::jsonb
1493
        ),
1494
        updated_at = NOW()
1495
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1496
    `;
1497
    await this.touchAgentPresence(operatorId, agentId);
1498
    return {
1499
      agentId,
1500
      endpoint,
1501
      a2aEndpoint,
1502
      status: 'verified' as const,
1503
      verifiedAt,
1504
      verificationKey: getSessionVerificationKey(this.gatewaySecret()),
1505
    };
1506
  }
1507
1508
  async disableAgentRuntime(operatorId: string, agentId: string) {
1509
    await this.ensureSchema();
1510
    const rows = await this.sql`
1511
      UPDATE openclasp_agent_runtimes
1512
      SET status = 'disabled', updated_at = NOW()
1513
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1514
      RETURNING agent_id
1515
    `;
1516
    if (!rows.length) throw new Error('Agent runtime not found');
1517
    return { agentId, status: 'disabled' as const };
1518
  }
1519
1520
  async deleteAgent(operatorId: string, agentId: string) {
1521
    await this.ensureSchema();
1522
    const owned = await this.sql`
1523
      SELECT 1 FROM openclasp_records
1524
      WHERE operator_id = ${operatorId}
1525
        AND kind = 'agent_profile'
1526
        AND record_id = ${agentId}
1527
      LIMIT 1
1528
    `;
1529
    if (!owned.length) throw new Error('Owned agent not found');
1530
    const openInteractions = await this.sql`
1531
      SELECT interaction_id FROM openclasp_federated_interactions
1532
      WHERE (initiator_agent_id = ${agentId} OR responder_agent_id = ${agentId})
1533
        AND status IN ('pending', 'active')
1534
      LIMIT 1
1535
    `;
1536
    if (openInteractions.length) {
1537
      const error = new Error('Finish or reject the agent’s open interactions before deleting it');
1538
      Object.assign(error, { statusCode: 409 });
1539
      throw error;
1540
    }
1541
    await this.sql`
1542
      DELETE FROM openclasp_hosted_threads
1543
      WHERE participant_a_agent_id = ${agentId} OR participant_b_agent_id = ${agentId}
1544
    `;
1545
    await this.sql`
1546
      DELETE FROM openclasp_public_agents
1547
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1548
    `;
1549
    await this.sql`
1550
      DELETE FROM openclasp_agent_runtimes
1551
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1552
    `;
1553
    await this.sql`
1554
      DELETE FROM openclasp_agent_access_tokens
1555
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1556
    `;
1557
    await this.sql`
1558
      DELETE FROM openclasp_records
1559
      WHERE operator_id = ${operatorId}
1560
        AND (
1561
          (record_id = ${agentId} AND kind IN ('agent', 'agent_profile', 'publication', 'presence'))
1562
          OR (kind = 'installation' AND payload->>'agentId' = ${agentId})
1563
          OR (kind = 'setup_request' AND payload->>'existingAgentId' = ${agentId})
1564
        )
1565
    `;
1566
    return {
1567
      agentId,
1568
      deleted: true as const,
1569
      historyRetained: true as const,
1570
    };
1571
  }
1572
1573
  private async sessionParticipant(agentId: string) {
1574
    const rows = await this.sql`
1575
      SELECT agents.operator_id, agents.card, profile.payload AS profile,
1576
        runtime.endpoint, runtime.a2a_endpoint
1577
      FROM openclasp_public_agents agents
1578
      LEFT JOIN openclasp_records profile
1579
        ON profile.operator_id = agents.operator_id
1580
        AND profile.kind = 'agent_profile'
1581
        AND profile.record_id = agents.agent_id
1582
      LEFT JOIN openclasp_agent_runtimes runtime
1583
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
1584
      WHERE agents.agent_id = ${agentId}
1585
      LIMIT 1
1586
    `;
1587
    const row = rows[0];
1588
    if (!row) throw new Error(`Published agent ${agentId} was not found`);
1589
    const card = normalizePublicAgentCard(row.card);
1590
    const profile = row.profile as Partial<AgentProfile> | undefined;
1591
    const mode =
1592
      profile?.agentMode ??
1593
      (row.endpoint || row.a2a_endpoint ? 'persistent_runtime' : 'temporary_chat');
1594
    if (mode === 'persistent_runtime' && !row.endpoint)
1595
      throw new Error(`Agent ${agentId} does not have a verified live runtime`);
1596
    const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
1597
      /\/$/,
1598
      '',
1599
    );
1600
    return {
1601
      agentId,
1602
      operatorId: String(row.operator_id),
1603
      mode: mode as 'persistent_runtime' | 'temporary_chat',
1604
      ...(row.endpoint ? { callbackEndpoint: String(row.endpoint) } : {}),
1605
      a2aEndpoint:
1606
        mode === 'temporary_chat'
1607
          ? `${baseUrl}/a2a/temporary/${encodeURIComponent(agentId)}`
1608
          : String(row.a2a_endpoint ?? row.endpoint),
1609
      card: PublicAgentCardSchema.parse({ ...card, agentMode: mode }),
1610
    };
1611
  }
1612
1613
  private async signedRuntimeRequest(
1614
    runtime: { callbackEndpoint: string },
1615
    requestId: string,
1616
    value: unknown,
1617
  ) {
1618
    const body = JSON.stringify(value);
1619
    const timestamp = new Date().toISOString();
1620
    return postRuntimeJson(runtime.callbackEndpoint, value, {
1621
      'openclasp-request-id': requestId,
1622
      'openclasp-timestamp': timestamp,
1623
      'openclasp-signature': `v1=${signSessionControl(this.gatewaySecret(), requestId, timestamp, body)}`,
1624
    });
1625
  }
1626
1627
  private sessionGrant(
1628
    interaction: FederatedInteraction,
1629
    senderAgentId: string,
1630
    recipientAgentId: string,
1631
  ) {
1632
    return issueSessionGrant(this.gatewaySecret(), {
1633
      interactionId: interaction.interactionId,
1634
      senderAgentId,
1635
      recipientAgentId,
1636
      expiresAt: Math.min(Date.parse(interaction.expiresAt), Date.now() + 60 * 60_000),
1637
    });
1638
  }
1639
1640
  private async privateCounterpartyInsights(
1641
    viewerOperatorId: string,
1642
    counterparty: { agentId: string; card: PublicAgentCard },
1643
    taskCategory: string,
1644
  ): Promise<{
1645
    insights: LiveSessionInsight[];
1646
    relevantSampleSize: number;
1647
    historyConfidence: number;
1648
  }> {
1649
    const rows = await this.sql`
1650
      SELECT record_id, payload
1651
      FROM openclasp_records
1652
      WHERE operator_id = ${viewerOperatorId}
1653
        AND kind = 'profile'
1654
        AND payload->>'agentId' = ${counterparty.agentId}
1655
        AND payload->>'taskCategory' = ${taskCategory}
1656
      ORDER BY (payload->>'sampleSize')::integer DESC, updated_at DESC
1657
    `;
1658
    const profiles: ContextualProfile[] = rows
1659
      .map<Record<string, unknown>>((row) => ({
1660
        recordId: String(row.record_id),
1661
        ...(row.payload as Record<string, unknown>),
1662
      }))
1663
      .filter(
1664
        (profile) =>
1665
          typeof profile.agentVersion === 'string' &&
1666
          typeof profile.sampleSize === 'number' &&
1667
          typeof profile.updatedAt === 'string' &&
1668
          typeof profile.completion === 'number' &&
1669
          typeof profile.acceptance === 'number' &&
1670
          typeof profile.specification === 'number' &&
1671
          typeof profile.deadline === 'number' &&
1672
          typeof profile.communication === 'number' &&
1673
          typeof profile.evidence === 'number' &&
1674
          typeof profile.scope === 'number' &&
1675
          typeof profile.disputes === 'number',
1676
      ) as ContextualProfile[];
1677
    const exact = profiles.find(
1678
      (profile) => profile.agentVersion === counterparty.card.agentVersion,
1679
    );
1680
    const current = exact ?? profiles[0];
1681
    const insights: LiveSessionInsight[] = [];
1682
    let relevantSampleSize = 0;
1683
    let historyConfidence = 0;
1684
    if (!current) {
1685
      insights.push({
1686
        code: 'limited_verified_history',
1687
        severity: 'caution',
1688
        message: `No eligible ${taskCategory} history is available for this agent version. Ask for task-relevant evidence.`,
1689
        evidenceReferences: [],
1690
        requirementReferences: [],
1691
      });
1692
    } else {
1693
      const ageDays = Math.max(
1694
        0,
1695
        (Date.now() - Date.parse(String(current.updatedAt))) / 86_400_000,
1696
      );
1697
      const freshness = Math.exp(-ageDays / 180);
1698
      const sampleSize = Number(current.sampleSize);
1699
      const effectiveSampleSize = Number(current.effectiveSampleSize ?? sampleSize);
1700
      const versionChanged = current.agentVersion !== counterparty.card.agentVersion;
1701
      const confidence = Math.min(
1702
        0.95,
1703
        (effectiveSampleSize / (effectiveSampleSize + 5)) * freshness * (versionChanged ? 0.35 : 1),
1704
      );
1705
      relevantSampleSize = sampleSize;
1706
      historyConfidence = confidence;
1707
      const reference = `openclasp:profile:${current.recordId}`;
1708
      insights.push({
1709
        code: versionChanged ? 'version_history_only' : 'contextual_history',
1710
        severity: versionChanged || confidence < 0.25 ? 'caution' : 'info',
1711
        message: versionChanged
1712
          ? `Only prior-version ${taskCategory} history is available (${sampleSize} eligible interaction${sampleSize === 1 ? '' : 's'}); confidence is reduced for version ${counterparty.card.agentVersion}.`
1713
          : `Based on ${sampleSize} eligible ${taskCategory} interaction${sampleSize === 1 ? '' : 's'}; confidence ${(confidence * 100).toFixed(0)}%.`,
1714
        evidenceReferences: [reference],
1715
        requirementReferences: [],
1716
      });
1717
      const weakDimensions = [
1718
        ['completion', Number(current.completion)],
1719
        ['output acceptance', Number(current.acceptance)],
1720
        ['specification adherence', Number(current.specification)],
1721
        ['deadline reliability', Number(current.deadline)],
1722
        ['communication quality', Number(current.communication)],
1723
        ['evidence quality', Number(current.evidence)],
1724
        ['scope adherence', Number(current.scope)],
1725
      ].filter(([, score]) => Number(score) < 0.7);
1726
      if (weakDimensions.length)
1727
        insights.push({
1728
          code: 'historical_weaknesses',
1729
          severity: weakDimensions.some(([, score]) => Number(score) < 0.5) ? 'high' : 'caution',
1730
          message: `Observed weaker areas: ${weakDimensions
1731
            .map(([name, score]) => `${String(name)} ${(Number(score) * 100).toFixed(0)}%`)
1732
            .join(', ')}.`,
1733
          evidenceReferences: [reference],
1734
          requirementReferences: [],
1735
        });
1736
      if (Number(current.disputes) > 0.25)
1737
        insights.push({
1738
          code: 'elevated_dispute_history',
1739
          severity: Number(current.disputes) >= 0.5 ? 'high' : 'caution',
1740
          message: `Eligible feedback shows a ${(Number(current.disputes) * 100).toFixed(0)}% dispute rate in this task category.`,
1741
          evidenceReferences: [reference],
1742
          requirementReferences: [],
1743
        });
1744
    }
1745
    if (counterparty.card.limitations.length)
1746
      insights.push({
1747
        code: 'declared_limitations',
1748
        severity: 'info',
1749
        message: `Counterparty declares: ${counterparty.card.limitations.join('; ')}`,
1750
        evidenceReferences: [counterparty.card.cardUrl],
1751
        requirementReferences: [],
1752
      });
1753
    return { insights, relevantSampleSize, historyConfidence };
1754
  }
1755
1756
  async brokerLiveSession(interaction: FederatedInteraction) {
1757
    await this.ensureSchema();
1758
    const existing = await this.sql`
1759
      SELECT status FROM openclasp_live_sessions
1760
      WHERE interaction_id = ${interaction.interactionId}
1761
    `;
1762
    if (existing[0]?.status === 'active') return;
1763
    const [initiator, responder] = await Promise.all([
1764
      this.sessionParticipant(interaction.initiatorAgentId),
1765
      this.sessionParticipant(interaction.responderAgentId),
1766
    ]);
1767
    if (initiator.mode === 'temporary_chat' && responder.mode === 'temporary_chat')
1768
      throw new Error('Temporary-to-temporary hosted conversations are not supported in this MVP');
1769
    const [initiatorContext, responderContext] = await Promise.all([
1770
      this.privateCounterpartyInsights(
1771
        initiator.operatorId,
1772
        responder,
1773
        interaction.contract.taskCategory,
1774
      ),
1775
      this.privateCounterpartyInsights(
1776
        responder.operatorId,
1777
        initiator,
1778
        interaction.contract.taskCategory,
1779
      ),
1780
    ]);
1781
    const issuedAt = new Date().toISOString();
1782
    const initiatorBrief = buildCounterpartyBrief({
1783
      interactionId: interaction.interactionId,
1784
      contractHash: interaction.termsHash,
1785
      contract: interaction.contract,
1786
      recipientAgentId: initiator.agentId,
1787
      subject: responder.card,
1788
      historyInsights: initiatorContext.insights,
1789
      relevantSampleSize: initiatorContext.relevantSampleSize,
1790
      historyConfidence: initiatorContext.historyConfidence,
1791
      generatedAt: issuedAt,
1792
      expiresAt: interaction.expiresAt,
1793
    });
1794
    const responderBrief = buildCounterpartyBrief({
1795
      interactionId: interaction.interactionId,
1796
      contractHash: interaction.termsHash,
1797
      contract: interaction.contract,
1798
      recipientAgentId: responder.agentId,
1799
      subject: initiator.card,
1800
      historyInsights: responderContext.insights,
1801
      relevantSampleSize: responderContext.relevantSampleSize,
1802
      historyConfidence: responderContext.historyConfidence,
1803
      generatedAt: issuedAt,
1804
      expiresAt: interaction.expiresAt,
1805
    });
1806
    await Promise.all([
1807
      this.upsert(
1808
        initiator.operatorId,
1809
        'counterparty_brief',
1810
        initiatorBrief.briefId,
1811
        initiatorBrief,
1812
      ),
1813
      this.upsert(
1814
        responder.operatorId,
1815
        'counterparty_brief',
1816
        responderBrief.briefId,
1817
        responderBrief,
1818
      ),
1819
    ]);
1820
    const makeOffer = (
1821
      runtime: typeof initiator,
1822
      counterparty: typeof responder,
1823
      role: 'initiator' | 'responder',
1824
      privateInsights: LiveSessionInsight[],
1825
      counterpartyBrief: CounterpartyBrief,
1826
    ) =>
1827
      LiveSessionOfferSchema.parse({
1828
        type: 'openclasp.session.offer',
1829
        version: '1',
1830
        offerId: crypto.randomUUID(),
1831
        interactionId: interaction.interactionId,
1832
        agentId: runtime.agentId,
1833
        role,
1834
        counterparty: {
1835
          agentId: counterparty.agentId,
1836
          name: counterparty.card.name,
1837
          agentVersion: counterparty.card.agentVersion,
1838
          capabilities: counterparty.card.capabilities,
1839
        },
1840
        contract: interaction.contract,
1841
        contractHash: interaction.termsHash,
1842
        privateInsights,
1843
        counterpartyBrief,
1844
        issuedAt,
1845
        expiresAt: interaction.expiresAt,
1846
      });
1847
    const initiatorOffer = makeOffer(
1848
      initiator,
1849
      responder,
1850
      'initiator',
1851
      initiatorBrief.insights,
1852
      initiatorBrief,
1853
    );
1854
    const responderOffer = makeOffer(
1855
      responder,
1856
      initiator,
1857
      'responder',
1858
      responderBrief.insights,
1859
      responderBrief,
1860
    );
1861
    const prepare = async (
1862
      participant: typeof initiator,
1863
      offer: typeof initiatorOffer,
1864
      label: 'Initiator' | 'Responder',
1865
    ) => {
1866
      if (participant.mode === 'temporary_chat')
1867
        return LiveSessionAcceptanceSchema.parse({
1868
          type: 'openclasp.session.accepted',
1869
          version: '1',
1870
          offerId: offer.offerId,
1871
          interactionId: interaction.interactionId,
1872
          agentId: participant.agentId,
1873
          sessionId: crypto.randomUUID(),
1874
          a2aEndpoint: participant.a2aEndpoint,
1875
          expiresAt: interaction.expiresAt,
1876
        });
1877
      if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
1878
      const response = await this.signedRuntimeRequest(
1879
        { callbackEndpoint: participant.callbackEndpoint },
1880
        offer.offerId,
1881
        offer,
1882
      );
1883
      if (response.status < 200 || response.status >= 300)
1884
        throw new Error(`${label} runtime is not live (HTTP ${response.status})`);
1885
      return LiveSessionAcceptanceSchema.parse(response.body);
1886
    };
1887
    const [initiatorAcceptance, responderAcceptance] = await Promise.all([
1888
      prepare(initiator, initiatorOffer, 'Initiator'),
1889
      prepare(responder, responderOffer, 'Responder'),
1890
    ]);
1891
    if (
1892
      initiatorAcceptance.offerId !== initiatorOffer.offerId ||
1893
      initiatorAcceptance.agentId !== initiator.agentId ||
1894
      responderAcceptance.offerId !== responderOffer.offerId ||
1895
      responderAcceptance.agentId !== responder.agentId
1896
    )
1897
      throw new Error('Runtime returned a mismatched live-session acceptance');
1898
    await Promise.all([
1899
      ...(initiator.mode === 'persistent_runtime'
1900
        ? [resolvePublicRuntimeEndpoint(initiatorAcceptance.a2aEndpoint)]
1901
        : []),
1902
      ...(responder.mode === 'persistent_runtime'
1903
        ? [resolvePublicRuntimeEndpoint(responderAcceptance.a2aEndpoint)]
1904
        : []),
1905
    ]);
1906
    await this.sql`
1907
      INSERT INTO openclasp_live_sessions(
1908
        interaction_id, initiator_agent_id, responder_agent_id,
1909
        initiator_session_id, responder_session_id,
1910
        initiator_endpoint, responder_endpoint, status, expires_at
1911
      ) VALUES (
1912
        ${interaction.interactionId}, ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
1913
        ${initiatorAcceptance.sessionId}, ${responderAcceptance.sessionId},
1914
        ${initiatorAcceptance.a2aEndpoint}, ${responderAcceptance.a2aEndpoint},
1915
        'preparing', ${interaction.expiresAt}
1916
      )
1917
      ON CONFLICT (interaction_id) DO UPDATE SET
1918
        initiator_session_id = EXCLUDED.initiator_session_id,
1919
        responder_session_id = EXCLUDED.responder_session_id,
1920
        initiator_endpoint = EXCLUDED.initiator_endpoint,
1921
        responder_endpoint = EXCLUDED.responder_endpoint,
1922
        status = 'preparing', expires_at = EXCLUDED.expires_at,
1923
        activated_at = NULL, completed_at = NULL, last_error = NULL
1924
    `;
1925
    await this.journalLiveSessionState(interaction.interactionId);
1926
    const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
1927
      /\/$/,
1928
      '',
1929
    );
1930
    const activatedAt = new Date().toISOString();
1931
    const verificationKey = getSessionVerificationKey(this.gatewaySecret());
1932
    const makeActivation = (
1933
      agentId: string,
1934
      sessionId: string,
1935
      role: 'initiator' | 'responder',
1936
      peerAgentId: string,
1937
      peerSessionId: string,
1938
      peerEndpoint: string,
1939
      privateInsights: LiveSessionInsight[],
1940
      counterpartyBrief: CounterpartyBrief,
1941
    ): LiveSessionActivation => {
1942
      const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
1943
      return LiveSessionActivationSchema.parse({
1944
        type: 'openclasp.session.activation',
1945
        version: '1',
1946
        activationId: crypto.randomUUID(),
1947
        interactionId: interaction.interactionId,
1948
        agentId,
1949
        sessionId,
1950
        role,
1951
        peer: {
1952
          agentId: peerAgentId,
1953
          sessionId: peerSessionId,
1954
          endpoint: peerEndpoint,
1955
          bearerToken,
1956
          verificationKey,
1957
        },
1958
        reporting: {
1959
          endpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/events`,
1960
          completionEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/completion-reports`,
1961
          feedbackEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/feedback`,
1962
          bearerToken,
1963
        },
1964
        privateInsights,
1965
        counterpartyBrief,
1966
        contractHash: interaction.termsHash,
1967
        activatedAt,
1968
        expiresAt: interaction.expiresAt,
1969
      });
1970
    };
1971
    const initiatorActivation = makeActivation(
1972
      initiator.agentId,
1973
      initiatorAcceptance.sessionId,
1974
      'initiator',
1975
      responder.agentId,
1976
      responderAcceptance.sessionId,
1977
      responderAcceptance.a2aEndpoint,
1978
      initiatorBrief.insights,
1979
      initiatorBrief,
1980
    );
1981
    const responderActivation = makeActivation(
1982
      responder.agentId,
1983
      responderAcceptance.sessionId,
1984
      'responder',
1985
      initiator.agentId,
1986
      initiatorAcceptance.sessionId,
1987
      initiatorAcceptance.a2aEndpoint,
1988
      responderBrief.insights,
1989
      responderBrief,
1990
    );
1991
    try {
1992
      const activate = async (
1993
        participant: typeof initiator,
1994
        activation: LiveSessionActivation,
1995
        label: 'Initiator' | 'Responder',
1996
      ) => {
1997
        if (participant.mode === 'temporary_chat') return;
1998
        if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
1999
        const response = await this.signedRuntimeRequest(
2000
          { callbackEndpoint: participant.callbackEndpoint },
2001
          activation.activationId,
2002
          activation,
2003
        );
2004
        if (response.status < 200 || response.status >= 300)
2005
          throw new Error(`${label} activation failed with HTTP ${response.status}`);
2006
      };
2007
      await activate(responder, responderActivation, 'Responder');
2008
      await activate(initiator, initiatorActivation, 'Initiator');
2009
      await this.sql`
2010
        UPDATE openclasp_live_sessions
2011
        SET status = 'active', activated_at = NOW(), last_error = NULL
2012
        WHERE interaction_id = ${interaction.interactionId}
2013
      `;
2014
      await this.journalLiveSessionState(interaction.interactionId);
2015
      await this.sql`
2016
        UPDATE openclasp_agent_runtimes
2017
        SET last_seen_at = NOW(), last_error = NULL, updated_at = NOW()
2018
        WHERE agent_id IN (${initiator.agentId}, ${responder.agentId})
2019
      `;
2020
      await Promise.all([
2021
        this.touchAgentPresence(initiator.operatorId, initiator.agentId),
2022
        this.touchAgentPresence(responder.operatorId, responder.agentId),
2023
      ]);
2024
    } catch (error) {
2025
      const reason =
2026
        error instanceof Error ? error.message.slice(0, 500) : 'Session activation failed';
2027
      await this.sql`
2028
        UPDATE openclasp_live_sessions SET status = 'failed', last_error = ${reason}
2029
        WHERE interaction_id = ${interaction.interactionId}
2030
      `;
2031
      await this.journalLiveSessionState(interaction.interactionId);
2032
      throw error;
2033
    }
2034
  }
2035
2036
  async getLiveSession(operatorId: string, interactionId: string, agentId: string) {
2037
    await this.ensureSchema();
2038
    const rows = await this.sql`
2039
      SELECT session.*, interaction.payload,
2040
        initiator.operator_id AS initiator_operator_id,
2041
        responder.operator_id AS responder_operator_id
2042
      FROM openclasp_live_sessions session
2043
      INNER JOIN openclasp_federated_interactions interaction
2044
        ON interaction.interaction_id = session.interaction_id
2045
      INNER JOIN openclasp_public_agents initiator
2046
        ON initiator.agent_id = session.initiator_agent_id
2047
      INNER JOIN openclasp_public_agents responder
2048
        ON responder.agent_id = session.responder_agent_id
2049
      WHERE session.interaction_id = ${interactionId} AND session.status = 'active'
2050
      LIMIT 1
2051
    `;
2052
    const row = rows[0];
2053
    if (!row) throw new Error('Active live session not found');
2054
    const isInitiator = row.initiator_agent_id === agentId;
2055
    const isResponder = row.responder_agent_id === agentId;
2056
    if (!isInitiator && !isResponder) throw new Error('Agent is not a session participant');
2057
    const owner = isInitiator ? row.initiator_operator_id : row.responder_operator_id;
2058
    if (owner !== operatorId) throw new Error('Agent is not owned by this account');
2059
    const interaction = FederatedInteractionSchema.parse(row.payload);
2060
    const peerAgentId = String(isInitiator ? row.responder_agent_id : row.initiator_agent_id);
2061
    const peer = await this.sessionParticipant(peerAgentId);
2062
    const privateContext = await this.privateCounterpartyInsights(
2063
      String(owner),
2064
      peer,
2065
      interaction.contract.taskCategory,
2066
    );
2067
    const briefRows = await this.sql`
2068
      SELECT payload FROM openclasp_records
2069
      WHERE operator_id = ${String(owner)}
2070
        AND kind = 'counterparty_brief'
2071
        AND payload->>'interactionId' = ${interactionId}
2072
        AND payload->>'recipientAgentId' = ${agentId}
2073
      ORDER BY updated_at DESC
2074
      LIMIT 1
2075
    `;
2076
    const storedBrief = CounterpartyBriefSchema.safeParse(briefRows[0]?.payload);
2077
    const counterpartyBrief = storedBrief.success
2078
      ? storedBrief.data
2079
      : buildCounterpartyBrief({
2080
          interactionId,
2081
          contractHash: interaction.termsHash,
2082
          contract: interaction.contract,
2083
          recipientAgentId: agentId,
2084
          subject: peer.card,
2085
          historyInsights: privateContext.insights,
2086
          relevantSampleSize: privateContext.relevantSampleSize,
2087
          historyConfidence: privateContext.historyConfidence,
2088
          expiresAt: new Date(String(row.expires_at)).toISOString(),
2089
        });
2090
    const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
2091
    return {
2092
      type: 'openclasp.session.activation' as const,
2093
      version: '1' as const,
2094
      activationId: crypto.randomUUID(),
2095
      interactionId,
2096
      agentId,
2097
      sessionId: String(isInitiator ? row.initiator_session_id : row.responder_session_id),
2098
      role: isInitiator ? ('initiator' as const) : ('responder' as const),
2099
      peer: {
2100
        agentId: peerAgentId,
2101
        sessionId: String(isInitiator ? row.responder_session_id : row.initiator_session_id),
2102
        endpoint: String(isInitiator ? row.responder_endpoint : row.initiator_endpoint),
2103
        bearerToken,
2104
        verificationKey: getSessionVerificationKey(this.gatewaySecret()),
2105
      },
2106
      reporting: {
2107
        endpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/events`,
2108
        completionEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/completion-reports`,
2109
        feedbackEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/feedback`,
2110
        bearerToken,
2111
      },
2112
      privateInsights: privateContext.insights,
2113
      counterpartyBrief,
2114
      contractHash: interaction.termsHash,
2115
      activatedAt: row.activated_at
2116
        ? new Date(String(row.activated_at)).toISOString()
2117
        : new Date().toISOString(),
2118
      expiresAt: new Date(String(row.expires_at)).toISOString(),
2119
    };
2120
  }
2121
2122
  private async interactionParticipant(operatorId: string, interactionId: string, agentId: string) {
2123
    await this.ensureSchema();
2124
    const rows = await this.sql`
2125
      SELECT payload, initiator_operator_id, responder_operator_id,
2126
        initiator_agent_id, responder_agent_id
2127
      FROM openclasp_federated_interactions
2128
      WHERE interaction_id = ${interactionId}
2129
        AND (
2130
          (initiator_operator_id = ${operatorId} AND initiator_agent_id = ${agentId})
2131
          OR (responder_operator_id = ${operatorId} AND responder_agent_id = ${agentId})
2132
        )
2133
      LIMIT 1
2134
    `;
2135
    const row = rows[0];
2136
    if (!row) throw new Error('Interaction participant not found for this account');
2137
    const interaction = FederatedInteractionSchema.parse(row.payload);
2138
    const isInitiator = interaction.initiatorAgentId === agentId;
2139
    return {
2140
      interaction,
2141
      counterpartyAgentId: isInitiator
2142
        ? interaction.responderAgentId
2143
        : interaction.initiatorAgentId,
2144
      participantOperatorId: String(
2145
        isInitiator ? row.initiator_operator_id : row.responder_operator_id,
2146
      ),
2147
      counterpartyOperatorId: String(
2148
        isInitiator ? row.responder_operator_id : row.initiator_operator_id,
2149
      ),
2150
    };
2151
  }
2152
2153
  async getCounterpartyBrief(operatorId: string, interactionId: string, agentId: string) {
2154
    await this.interactionParticipant(operatorId, interactionId, agentId);
2155
    const rows = await this.sql`
2156
      SELECT payload FROM openclasp_records
2157
      WHERE operator_id = ${operatorId}
2158
        AND kind = 'counterparty_brief'
2159
        AND payload->>'interactionId' = ${interactionId}
2160
        AND payload->>'recipientAgentId' = ${agentId}
2161
      ORDER BY updated_at DESC
2162
      LIMIT 1
2163
    `;
2164
    const parsed = CounterpartyBriefSchema.safeParse(rows[0]?.payload);
2165
    if (!parsed.success) throw new Error('Counterparty brief is not available');
2166
    return parsed.data;
2167
  }
2168
2169
  private async ensureFeedbackRequest(
2170
    operatorId: string,
2171
    interactionId: string,
2172
    reviewerAgentId: string,
2173
    subjectAgentId: string,
2174
  ) {
2175
    const existing = await this.sql`
2176
      SELECT payload FROM openclasp_records
2177
      WHERE operator_id = ${operatorId}
2178
        AND kind = 'feedback_request'
2179
        AND payload->>'interactionId' = ${interactionId}
2180
        AND payload->>'reviewerAgentId' = ${reviewerAgentId}
2181
      ORDER BY updated_at DESC
2182
      LIMIT 1
2183
    `;
2184
    const parsed = FeedbackRequestSchema.safeParse(existing[0]?.payload);
2185
    if (parsed.success) return parsed.data;
2186
    const requestedAt = new Date();
2187
    const base = FeedbackRequestSchema.parse({
2188
      requestId: deterministicUuid(`feedback-request:${interactionId}:${reviewerAgentId}`),
2189
      interactionId,
2190
      reviewerAgentId,
2191
      subjectAgentId,
2192
      status: 'pending',
2193
      requestedDimensions: FEEDBACK_DIMENSIONS,
2194
      requestedAt: requestedAt.toISOString(),
2195
      dueAt: new Date(requestedAt.getTime() + feedbackWindowMilliseconds()).toISOString(),
2196
    });
2197
    const request = FeedbackRequestSchema.parse({
2198
      ...base,
2199
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
2200
    });
2201
    await this.upsert(operatorId, 'feedback_request', request.requestId, request);
2202
    return request;
2203
  }
2204
2205
  private async requestRuntimeFinalization(
2206
    interactionId: string,
2207
    reportingAgentId: string,
2208
    counterpartyAgentId: string,
2209
    contractHash: string,
2210
  ) {
2211
    const rows = await this.sql`
2212
      SELECT endpoint FROM openclasp_agent_runtimes
2213
      WHERE agent_id = ${counterpartyAgentId} AND status = 'verified'
2214
      LIMIT 1
2215
    `;
2216
    if (!rows[0]?.endpoint) return false;
2217
    const requestId = crypto.randomUUID();
2218
    const value = {
2219
      type: 'openclasp.session.finalization_request',
2220
      version: '1',
2221
      requestId,
2222
      interactionId,
2223
      agentId: counterpartyAgentId,
2224
      peerAgentId: reportingAgentId,
2225
      contractHash,
2226
      requestedAt: new Date().toISOString(),
2227
    };
2228
    const response = await this.signedRuntimeRequest(
2229
      { callbackEndpoint: String(rows[0].endpoint) },
2230
      requestId,
2231
      value,
2232
    );
2233
    return response.status >= 200 && response.status < 300;
2234
  }
2235
2236
  async listFeedbackRequests(operatorId: string, agentId: string) {
2237
    const rows = await this.sql`
2238
      SELECT payload FROM openclasp_records
2239
      WHERE operator_id = ${operatorId}
2240
        AND kind = 'feedback_request'
2241
        AND payload->>'reviewerAgentId' = ${agentId}
2242
      ORDER BY updated_at DESC
2243
    `;
2244
    return rows.map((row) => FeedbackRequestSchema.parse(row.payload));
2245
  }
2246
2247
  async submitCompletionReport(
2248
    operatorId: string,
2249
    agentId: string,
2250
    value: InteractionCompletionReport,
2251
    submissionMethod:
2252
      'oauth_account' | 'oauth_installation' | 'agent_access_token' | 'runtime_session',
2253
  ) {
2254
    const participant = await this.interactionParticipant(operatorId, value.interactionId, agentId);
2255
    const report = InteractionCompletionReportSchema.parse(value);
2256
    if (report.platformAttestation)
2257
      throw new Error('Platform attestation is assigned by OpenClasp');
2258
    if (report.reportingAgentId !== agentId)
2259
      throw new Error('Completion report identity does not match the authenticated agent');
2260
    if (report.counterpartyAgentId !== participant.counterpartyAgentId)
2261
      throw new Error('Completion report counterparty does not match the interaction');
2262
    if (report.contractHash !== participant.interaction.termsHash)
2263
      throw new Error('Completion report contract hash does not match the interaction');
2264
    if (report.requestedOutcome !== participant.interaction.contract.requestedOutcome)
2265
      throw new Error('Completion report requested outcome does not match the contract');
2266
    if (
2267
      report.criteria.some(
2268
        (criterion) =>
2269
          !participant.interaction.contract.successCriteria.includes(criterion.criterion),
2270
      )
2271
    )
2272
      throw new Error('Completion report contains a criterion outside the contract');
2273
    if (!['active', 'completed'].includes(participant.interaction.status))
2274
      throw new Error('Only active or completed interactions accept completion reports');
2275
    const profiles = await this.sql`
2276
      SELECT payload FROM openclasp_records
2277
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
2278
      LIMIT 1
2279
    `;
2280
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
2281
    if (!profile || profile.agentVersion !== report.agentVersion)
2282
      throw new Error('Completion report agent version does not match the registered agent');
2283
    let verifiedSubmissionMethod: InteractionCompletionReport['submissionMethod'] =
2284
      submissionMethod;
2285
    if (report.signature) {
2286
      if (report.submissionMethod !== 'agent_signature')
2287
        throw new Error('Signed completion reports must declare agent_signature submission');
2288
      const identities = await this.sql`
2289
        SELECT payload FROM openclasp_records
2290
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
2291
        LIMIT 1
2292
      `;
2293
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
2294
      if (
2295
        !identity.success ||
2296
        !verifyObject(report as unknown as Record<string, unknown>, identity.data.publicKey)
2297
      )
2298
        throw new Error('Completion report agent signature is invalid or unverifiable');
2299
      verifiedSubmissionMethod = 'agent_signature';
2300
    }
2301
    const unsigned = InteractionCompletionReportSchema.parse({
2302
      ...report,
2303
      submissionMethod: verifiedSubmissionMethod,
2304
    });
2305
    const stored = InteractionCompletionReportSchema.parse({
2306
      ...unsigned,
2307
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unsigned),
2308
    });
2309
    const existing = await this.sql`
2310
      SELECT payload FROM openclasp_records
2311
      WHERE kind = 'completion_report' AND record_id = ${stored.reportId}
2312
        AND payload->>'interactionId' = ${stored.interactionId}
2313
      LIMIT 1
2314
    `;
2315
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
2316
      throw new Error('Conflicting completion report ID');
2317
    await Promise.all([
2318
      this.upsert(participant.participantOperatorId, 'completion_report', stored.reportId, stored),
2319
      this.upsert(participant.counterpartyOperatorId, 'completion_report', stored.reportId, stored),
2320
    ]);
2321
    const [feedbackRequest] = await Promise.all([
2322
      this.ensureFeedbackRequest(
2323
        participant.participantOperatorId,
2324
        stored.interactionId,
2325
        stored.reportingAgentId,
2326
        stored.counterpartyAgentId,
2327
      ),
2328
      this.ensureFeedbackRequest(
2329
        participant.counterpartyOperatorId,
2330
        stored.interactionId,
2331
        stored.counterpartyAgentId,
2332
        stored.reportingAgentId,
2333
      ),
2334
    ]);
2335
    const completed = await this.sql`
2336
      SELECT COUNT(DISTINCT payload->>'reportingAgentId') AS count
2337
      FROM openclasp_records
2338
      WHERE kind = 'completion_report'
2339
        AND payload->>'interactionId' = ${stored.interactionId}
2340
    `;
2341
    let peerReportStatus: InteractionConclusion['peerReportStatus'] = 'received';
2342
    if (Number(completed[0]?.count ?? 0) >= 2) {
2343
      const completedAt = new Date().toISOString();
2344
      await Promise.all([
2345
        this.sql`
2346
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
2347
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
2348
        `,
2349
        this.sql`
2350
          UPDATE openclasp_federated_interactions
2351
          SET status = 'completed',
2352
            payload = jsonb_set(
2353
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
2354
              '{updatedAt}', to_jsonb(${completedAt}::text)
2355
            ),
2356
            updated_at = NOW()
2357
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
2358
        `,
2359
      ]);
2360
      await Promise.all([
2361
        this.journalLiveSessionState(stored.interactionId),
2362
        this.journalFederatedInteraction(stored.interactionId),
2363
      ]);
2364
    } else {
2365
      const requested = await this.requestRuntimeFinalization(
2366
        stored.interactionId,
2367
        stored.reportingAgentId,
2368
        stored.counterpartyAgentId,
2369
        stored.contractHash,
2370
      ).catch(() => false);
2371
      peerReportStatus = requested ? 'awaiting' : 'unreachable';
2372
    }
2373
    const conclusion = await this.finalizeInteractionConclusion(stored.interactionId, {
2374
      peerReportStatus,
2375
    });
2376
    return {
2377
      report: stored,
2378
      feedbackRequest,
2379
      peerReportRequested: peerReportStatus === 'awaiting',
2380
      ...(conclusion.released ? { conclusion: conclusion.conclusion } : {}),
2381
    };
2382
  }
2383
2384
  async recordSessionCompletionReport(token: string, value: InteractionCompletionReport) {
2385
    const report = InteractionCompletionReportSchema.parse(value);
2386
    const grant = this.verifySessionGrant(token);
2387
    if (
2388
      grant.interactionId !== report.interactionId ||
2389
      grant.senderAgentId !== report.reportingAgentId ||
2390
      grant.recipientAgentId !== report.counterpartyAgentId
2391
    )
2392
      throw new Error('Session credential does not match the completion report');
2393
    const owners = await this.sql`
2394
      SELECT initiator_operator_id, responder_operator_id,
2395
        initiator_agent_id, responder_agent_id
2396
      FROM openclasp_federated_interactions
2397
      WHERE interaction_id = ${report.interactionId}
2398
      LIMIT 1
2399
    `;
2400
    const row = owners[0];
2401
    if (!row) throw new Error('Interaction not found');
2402
    const operatorId = String(
2403
      row.initiator_agent_id === report.reportingAgentId
2404
        ? row.initiator_operator_id
2405
        : row.responder_operator_id,
2406
    );
2407
    return this.submitCompletionReport(
2408
      operatorId,
2409
      report.reportingAgentId,
2410
      report,
2411
      'runtime_session',
2412
    );
2413
  }
2414
2415
  private async applyInteractionLearning(input: {
2416
    interaction: FederatedInteraction;
2417
    initiatorOperatorId: string;
2418
    responderOperatorId: string;
2419
    reports: InteractionCompletionReport[];
2420
    feedback: InteractionFeedback[];
2421
    conclusion: InteractionConclusion;
2422
  }) {
2423
    const { interaction, reports, feedback, conclusion } = input;
2424
    const [initiatorSettings, responderSettings, cards, pairActivity] = await Promise.all([
2425
      this.getSettings(input.initiatorOperatorId),
2426
      this.getSettings(input.responderOperatorId),
2427
      this.sql`
2428
        SELECT agent_id, card FROM openclasp_public_agents
2429
        WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
2430
      `,
2431
      this.sql`
2432
        SELECT COUNT(*)::integer AS count
2433
        FROM openclasp_federated_interactions
2434
        WHERE created_at >= NOW() - INTERVAL '24 hours'
2435
          AND (
2436
            (initiator_agent_id = ${interaction.initiatorAgentId}
2437
              AND responder_agent_id = ${interaction.responderAgentId})
2438
            OR
2439
            (initiator_agent_id = ${interaction.responderAgentId}
2440
              AND responder_agent_id = ${interaction.initiatorAgentId})
2441
          )
2442
      `,
2443
    ]);
2444
    const contributionMode =
2445
      initiatorSettings.contributionEnabled && responderSettings.contributionEnabled
2446
        ? ('network_aggregate' as const)
2447
        : ('local_only' as const);
2448
    const eligibilityBase = evaluateLearningEligibility({
2449
      interactionId: interaction.interactionId,
2450
      reports,
2451
      feedback,
2452
      consensus: conclusion.consensus,
2453
      contributionMode,
2454
      reviewerCredibility: Object.fromEntries(
2455
        feedback.map((item) => [item.reviewerAgentId, reviewerCredibility(item)]),
2456
      ),
2457
      manipulationSignals: [
2458
        ...(input.initiatorOperatorId === input.responderOperatorId ? ['same_operator_pair'] : []),
2459
        ...(Number(pairActivity[0]?.count ?? 0) > 10 ? ['rapid_reciprocal_pair_activity'] : []),
2460
      ],
2461
      decisionId: deterministicUuid(`learning-eligibility:${interaction.interactionId}`),
2462
      decidedAt: conclusion.generatedAt,
2463
    });
2464
    const eligibility = LearningEligibilityDecisionSchema.parse({
2465
      ...eligibilityBase,
2466
      platformAttestation: attestSessionRecord(this.gatewaySecret(), eligibilityBase),
2467
    });
2468
    await Promise.all([
2469
      this.upsert(
2470
        input.initiatorOperatorId,
2471
        'learning_eligibility',
2472
        eligibility.decisionId,
2473
        eligibility,
2474
      ),
2475
      this.upsert(
2476
        input.responderOperatorId,
2477
        'learning_eligibility',
2478
        eligibility.decisionId,
2479
        eligibility,
2480
      ),
2481
    ]);
2482
    if (!eligibility.eligible || eligibility.sampleWeight <= 0)
2483
      return { eligibility, profileDeltas: [] as BehaviouralProfileDelta[] };
2484
2485
    const appliedAt = new Date().toISOString();
2486
    const versions = new Map<string, string>();
2487
    for (const report of reports) versions.set(report.reportingAgentId, report.agentVersion);
2488
    for (const row of cards) {
2489
      const card = PublicAgentCardSchema.safeParse(row.card);
2490
      if (card.success && !versions.has(String(row.agent_id)))
2491
        versions.set(String(row.agent_id), card.data.agentVersion);
2492
    }
2493
    const participants = [
2494
      {
2495
        viewerOperatorId: input.initiatorOperatorId,
2496
        reviewerAgentId: interaction.initiatorAgentId,
2497
        subjectAgentId: interaction.responderAgentId,
2498
      },
2499
      {
2500
        viewerOperatorId: input.responderOperatorId,
2501
        reviewerAgentId: interaction.responderAgentId,
2502
        subjectAgentId: interaction.initiatorAgentId,
2503
      },
2504
      {
2505
        viewerOperatorId: input.initiatorOperatorId,
2506
        reviewerAgentId: interaction.responderAgentId,
2507
        subjectAgentId: interaction.initiatorAgentId,
2508
      },
2509
      {
2510
        viewerOperatorId: input.responderOperatorId,
2511
        reviewerAgentId: interaction.initiatorAgentId,
2512
        subjectAgentId: interaction.responderAgentId,
2513
      },
2514
    ];
2515
    const profileDeltas: Array<BehaviouralProfileDelta | undefined> = [];
2516
    for (const participant of participants) {
2517
      const agentVersion = versions.get(participant.subjectAgentId);
2518
      if (!agentVersion) {
2519
        profileDeltas.push(undefined);
2520
        continue;
2521
      }
2522
      const deltaId = deterministicUuid(
2523
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.reviewerAgentId}:${participant.subjectAgentId}`,
2524
      );
2525
      const legacyDeltaId = deterministicUuid(
2526
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.subjectAgentId}`,
2527
      );
2528
      const existingDelta = await this.sql`
2529
          SELECT payload FROM openclasp_records
2530
          WHERE operator_id = ${participant.viewerOperatorId}
2531
            AND kind = 'profile_delta'
2532
            AND (record_id = ${deltaId} OR record_id = ${legacyDeltaId})
2533
          LIMIT 1
2534
        `;
2535
      const parsedDelta = BehaviouralProfileDeltaSchema.safeParse(existingDelta[0]?.payload);
2536
      if (parsedDelta.success) {
2537
        profileDeltas.push(parsedDelta.data);
2538
        continue;
2539
      }
2540
      const profileId = deterministicUuid(
2541
        `contextual-profile:${participant.subjectAgentId}:${agentVersion}:${interaction.contract.taskCategory}`,
2542
      );
2543
      const currentRows = await this.sql`
2544
          SELECT payload FROM openclasp_records
2545
          WHERE operator_id = ${participant.viewerOperatorId}
2546
            AND kind = 'profile' AND record_id = ${profileId}
2547
          LIMIT 1
2548
        `;
2549
      const current = currentRows[0]?.payload as Partial<ContextualProfile> | undefined;
2550
      const reviewerFeedback = feedback.find(
2551
        (item) =>
2552
          item.reviewerAgentId === participant.reviewerAgentId &&
2553
          item.subjectAgentId === participant.subjectAgentId,
2554
      );
2555
      const observations = deriveBehaviouralObservations({
2556
        subjectAgentId: participant.subjectAgentId,
2557
        reviewerAgentId: participant.reviewerAgentId,
2558
        reports,
2559
        ...(reviewerFeedback ? { reviewerFeedback } : {}),
2560
        conclusion,
2561
      });
2562
      if (!Object.keys(observations).length) {
2563
        profileDeltas.push(undefined);
2564
        continue;
2565
      }
2566
      const applied = updateContextualBehaviouralProfile({
2567
        ...(current ? { current } : {}),
2568
        observations,
2569
        sampleWeight: eligibility.sampleWeight,
2570
        appliedAt,
2571
      });
2572
      const profile: ContextualProfile = {
2573
        recordId: profileId,
2574
        agentId: participant.subjectAgentId,
2575
        agentVersion,
2576
        taskCategory: interaction.contract.taskCategory,
2577
        ...applied.profile,
2578
      };
2579
      const deltaBase = BehaviouralProfileDeltaSchema.parse({
2580
        deltaId,
2581
        interactionId: interaction.interactionId,
2582
        agentId: participant.subjectAgentId,
2583
        agentVersion,
2584
        taskCategory: interaction.contract.taskCategory,
2585
        sampleWeight: eligibility.sampleWeight,
2586
        dimensionDeltas: applied.dimensionDeltas,
2587
        explanation:
2588
          'Applied platform-attested structured outcome and eligible reviewer signals; private comments and raw conversation content were excluded.',
2589
        appliedAt,
2590
      });
2591
      const delta = BehaviouralProfileDeltaSchema.parse({
2592
        ...deltaBase,
2593
        platformAttestation: attestSessionRecord(this.gatewaySecret(), deltaBase),
2594
      });
2595
      await Promise.all([
2596
        this.upsert(participant.viewerOperatorId, 'profile', profileId, profile),
2597
        this.upsert(participant.viewerOperatorId, 'profile_delta', deltaId, delta),
2598
      ]);
2599
      profileDeltas.push(delta);
2600
    }
2601
    return {
2602
      eligibility,
2603
      profileDeltas: profileDeltas.filter(
2604
        (delta): delta is BehaviouralProfileDelta => delta !== undefined,
2605
      ),
2606
    };
2607
  }
2608
2609
  private async finalizeInteractionConclusion(
2610
    interactionId: string,
2611
    options: { peerReportStatus?: InteractionConclusion['peerReportStatus'] } = {},
2612
  ) {
2613
    const [interactionRows, reportRows, feedbackRows, requestRows, existingRows] =
2614
      await Promise.all([
2615
        this.sql`
2616
        SELECT payload, initiator_operator_id, responder_operator_id
2617
        FROM openclasp_federated_interactions
2618
        WHERE interaction_id = ${interactionId}
2619
        LIMIT 1
2620
      `,
2621
        this.sql`
2622
        SELECT record_id, payload FROM openclasp_records
2623
        WHERE kind = 'completion_report' AND payload->>'interactionId' = ${interactionId}
2624
      `,
2625
        this.sql`
2626
        SELECT record_id, payload FROM openclasp_records
2627
        WHERE kind = 'interaction_feedback' AND payload->>'interactionId' = ${interactionId}
2628
      `,
2629
        this.sql`
2630
        SELECT record_id, payload FROM openclasp_records
2631
        WHERE kind = 'feedback_request' AND payload->>'interactionId' = ${interactionId}
2632
      `,
2633
        this.sql`
2634
        SELECT payload FROM openclasp_records
2635
        WHERE kind = 'interaction_conclusion' AND payload->>'interactionId' = ${interactionId}
2636
        LIMIT 1
2637
      `,
2638
      ]);
2639
    const interactionRow = interactionRows[0];
2640
    if (!interactionRow) throw new Error('Interaction not found');
2641
    const interaction = FederatedInteractionSchema.parse(interactionRow.payload);
2642
    const reports = [
2643
      ...new Map(
2644
        reportRows
2645
          .map((row) => InteractionCompletionReportSchema.parse(row.payload))
2646
          .map((report) => [report.reportId, report]),
2647
      ).values(),
2648
    ];
2649
    const feedback = [
2650
      ...new Map(
2651
        feedbackRows
2652
          .map((row) => InteractionFeedbackSchema.parse(row.payload))
2653
          .map((item) => [item.feedbackId, item]),
2654
      ).values(),
2655
    ];
2656
    const requests = [
2657
      ...new Map(
2658
        requestRows
2659
          .map((row) => FeedbackRequestSchema.parse(row.payload))
2660
          .map((request) => [request.requestId, request]),
2661
      ).values(),
2662
    ];
2663
    if (!reports.length) return { released: false as const, feedbackRevealed: false as const };
2664
    const existing = InteractionConclusionSchema.safeParse(existingRows[0]?.payload);
2665
    const reportingAgentIds = new Set(reports.map((report) => report.reportingAgentId));
2666
    const missingReportAgentIds = [
2667
      interaction.initiatorAgentId,
2668
      interaction.responderAgentId,
2669
    ].filter((agentId) => !reportingAgentIds.has(agentId));
2670
    const feedbackWindowClosed =
2671
      requests.length >= 2 && requests.every((request) => request.status !== 'pending');
2672
    const lifecycle =
2673
      missingReportAgentIds.length === 0 || feedbackWindowClosed
2674
        ? ('final' as const)
2675
        : ('provisional' as const);
2676
    const visibleFeedback = feedbackWindowClosed ? feedback : [];
2677
    const peerReportStatus: InteractionConclusion['peerReportStatus'] =
2678
      missingReportAgentIds.length === 0
2679
        ? 'received'
2680
        : feedbackWindowClosed
2681
          ? 'timed_out'
2682
          : (options.peerReportStatus ?? existing.data?.peerReportStatus ?? 'awaiting');
2683
    const pendingFeedbackAgentIds = requests
2684
      .filter((request) => request.status === 'pending')
2685
      .map((request) => request.reviewerAgentId);
2686
    if (requests.length < 2) return { released: false as const };
2687
    const base = buildInteractionConclusion({
2688
      interaction,
2689
      reports,
2690
      feedback: visibleFeedback,
2691
      conclusionId: deterministicUuid(`interaction-conclusion:${interactionId}`),
2692
      lifecycle,
2693
      pendingFeedbackAgentIds,
2694
      peerReportStatus,
2695
    });
2696
    const conclusion = InteractionConclusionSchema.parse({
2697
      ...base,
2698
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
2699
    });
2700
    const startedAt = reports
2701
      .map((report) => report.startedAt)
2702
      .filter((value): value is string => typeof value === 'string')
2703
      .sort()[0];
2704
    const receiptBase = {
2705
      receiptId: deterministicUuid(`interaction-receipt:${interactionId}`),
2706
      interactionId,
2707
      participants: [interaction.initiatorAgentId, interaction.responderAgentId],
2708
      agentVersions: Object.fromEntries(
2709
        reports.map((report) => [report.reportingAgentId, report.agentVersion]),
2710
      ),
2711
      contractHash: interaction.termsHash,
2712
      startedAt: startedAt ?? interaction.createdAt,
2713
      completedAt:
2714
        reports
2715
          .map((report) => report.completedAt)
2716
          .sort()
2717
          .at(-1) ?? conclusion.generatedAt,
2718
      outcome: conclusion.outcome,
2719
      commitmentsFulfilled: conclusion.criteria
2720
        .filter((criterion) => criterion.status === 'met')
2721
        .map((criterion) => criterion.criterion),
2722
      commitmentsMissed: conclusion.criteria
2723
        .filter((criterion) => criterion.status !== 'met')
2724
        .map((criterion) => criterion.criterion),
2725
      evidenceHashes: conclusion.evidenceReferences.map((reference) => canonicalHash(reference)),
2726
      policyWarnings: reports.flatMap((report) => report.blockers),
2727
      policyViolations: [],
2728
      disputeStatus: conclusion.consensus === 'conflicting' ? ('open' as const) : ('none' as const),
2729
      delegationChainHash: canonicalHash(interaction.contract.delegationRules),
2730
      unilateral: reports.length < 2,
2731
      provisional: lifecycle === 'provisional',
2732
      confidence: conclusion.confidence,
2733
      signatures: {},
2734
      completionReportIds: conclusion.reportIds,
2735
      conclusionId: conclusion.conclusionId,
2736
    };
2737
    const receipt = ReceiptSchema.parse({
2738
      ...receiptBase,
2739
      platformAttestation: attestSessionRecord(this.gatewaySecret(), receiptBase),
2740
    });
2741
    await Promise.all([
2742
      this.upsert(
2743
        String(interactionRow.initiator_operator_id),
2744
        'interaction_conclusion',
2745
        conclusion.conclusionId,
2746
        conclusion,
2747
      ),
2748
      this.upsert(
2749
        String(interactionRow.responder_operator_id),
2750
        'interaction_conclusion',
2751
        conclusion.conclusionId,
2752
        conclusion,
2753
      ),
2754
      this.upsert(
2755
        String(interactionRow.initiator_operator_id),
2756
        'receipt',
2757
        receipt.receiptId,
2758
        receipt,
2759
      ),
2760
      this.upsert(
2761
        String(interactionRow.responder_operator_id),
2762
        'receipt',
2763
        receipt.receiptId,
2764
        receipt,
2765
      ),
2766
    ]);
2767
    const learning =
2768
      lifecycle === 'final' && feedbackWindowClosed
2769
        ? await this.applyInteractionLearning({
2770
            interaction,
2771
            initiatorOperatorId: String(interactionRow.initiator_operator_id),
2772
            responderOperatorId: String(interactionRow.responder_operator_id),
2773
            reports,
2774
            feedback: visibleFeedback,
2775
            conclusion,
2776
          })
2777
        : undefined;
2778
    return {
2779
      released: true as const,
2780
      feedbackRevealed: feedbackWindowClosed,
2781
      conclusion,
2782
      receipt,
2783
      ...(learning ? { learning } : {}),
2784
    };
2785
  }
2786
2787
  async submitInteractionFeedback(
2788
    operatorId: string,
2789
    agentId: string,
2790
    value: InteractionFeedback,
2791
    submissionMethod:
2792
      'oauth_account' | 'oauth_installation' | 'agent_access_token' | 'runtime_session',
2793
  ) {
2794
    const feedback = InteractionFeedbackSchema.parse(value);
2795
    if (feedback.platformAttestation)
2796
      throw new Error('Platform attestation is assigned by OpenClasp');
2797
    if (feedback.reviewerAgentId !== agentId)
2798
      throw new Error('Feedback reviewer does not match the authenticated agent');
2799
    const participant = await this.interactionParticipant(
2800
      operatorId,
2801
      feedback.interactionId,
2802
      agentId,
2803
    );
2804
    if (feedback.subjectAgentId !== participant.counterpartyAgentId)
2805
      throw new Error('Feedback subject does not match the interaction counterparty');
2806
    const requestRows = await this.sql`
2807
      SELECT payload FROM openclasp_records
2808
      WHERE operator_id = ${operatorId}
2809
        AND kind = 'feedback_request'
2810
        AND record_id = ${feedback.requestId}
2811
      LIMIT 1
2812
    `;
2813
    const request = FeedbackRequestSchema.parse(requestRows[0]?.payload);
2814
    if (
2815
      request.interactionId !== feedback.interactionId ||
2816
      request.reviewerAgentId !== feedback.reviewerAgentId ||
2817
      request.subjectAgentId !== feedback.subjectAgentId
2818
    )
2819
      throw new Error('Feedback does not match its request');
2820
    if (request.status !== 'pending') throw new Error('Feedback request is no longer pending');
2821
    if (Date.parse(request.dueAt) <= Date.now()) throw new Error('Feedback request has expired');
2822
    if (
2823
      request.requestedDimensions.some(
2824
        (dimension) => typeof feedback.ratings[dimension] !== 'number',
2825
      )
2826
    )
2827
      throw new Error('Feedback must rate every requested dimension');
2828
    const profiles = await this.sql`
2829
      SELECT payload FROM openclasp_records
2830
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
2831
      LIMIT 1
2832
    `;
2833
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
2834
    if (!profile || profile.agentVersion !== feedback.reviewerAgentVersion)
2835
      throw new Error('Feedback reviewer version does not match the registered agent');
2836
    let verifiedSubmissionMethod: InteractionFeedback['submissionMethod'] = submissionMethod;
2837
    if (feedback.signature) {
2838
      if (feedback.submissionMethod !== 'agent_signature')
2839
        throw new Error('Signed feedback must declare agent_signature submission');
2840
      const identities = await this.sql`
2841
        SELECT payload FROM openclasp_records
2842
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
2843
        LIMIT 1
2844
      `;
2845
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
2846
      if (
2847
        !identity.success ||
2848
        !verifyObject(feedback as unknown as Record<string, unknown>, identity.data.publicKey)
2849
      )
2850
        throw new Error('Feedback agent signature is invalid or unverifiable');
2851
      verifiedSubmissionMethod = 'agent_signature';
2852
    }
2853
    const unattested = InteractionFeedbackSchema.parse({
2854
      ...feedback,
2855
      submissionMethod: verifiedSubmissionMethod,
2856
    });
2857
    const stored = InteractionFeedbackSchema.parse({
2858
      ...unattested,
2859
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unattested),
2860
    });
2861
    const existing = await this.sql`
2862
      SELECT payload FROM openclasp_records
2863
      WHERE operator_id = ${operatorId}
2864
        AND kind = 'interaction_feedback'
2865
        AND record_id = ${stored.feedbackId}
2866
      LIMIT 1
2867
    `;
2868
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
2869
      throw new Error('Conflicting feedback ID');
2870
    await this.upsert(operatorId, 'interaction_feedback', stored.feedbackId, stored);
2871
    const requestBase = { ...request, status: 'submitted' as const };
2872
    delete requestBase.platformAttestation;
2873
    const updatedRequest = FeedbackRequestSchema.parse({
2874
      ...requestBase,
2875
      platformAttestation: attestSessionRecord(this.gatewaySecret(), requestBase),
2876
    });
2877
    await this.upsert(operatorId, 'feedback_request', request.requestId, updatedRequest);
2878
    const release = await this.finalizeInteractionConclusion(feedback.interactionId);
2879
    return {
2880
      feedbackId: stored.feedbackId,
2881
      status: 'submitted' as const,
2882
      revealed: release.released && release.feedbackRevealed,
2883
      ...(release.released ? { conclusion: release.conclusion } : {}),
2884
    };
2885
  }
2886
2887
  async processDueFeedback(now = new Date()) {
2888
    await this.ensureSchema();
2889
    const expiredInteractions = await this.sql`
2890
      UPDATE openclasp_federated_interactions
2891
      SET status = 'expired',
2892
        payload = jsonb_set(
2893
          jsonb_set(payload, '{status}', '"expired"'::jsonb),
2894
          '{updatedAt}', to_jsonb(${now.toISOString()}::text)
2895
        ),
2896
        updated_at = NOW()
2897
      WHERE status IN ('pending', 'active') AND expires_at <= ${now.toISOString()}
2898
      RETURNING interaction_id
2899
    `;
2900
    if (expiredInteractions.length) {
2901
      await this.sql`
2902
        UPDATE openclasp_live_sessions
2903
        SET status = 'failed', completed_at = COALESCE(completed_at, NOW()),
2904
          last_error = COALESCE(last_error, 'Interaction expired before bilateral completion')
2905
        WHERE status IN ('preparing', 'active')
2906
          AND interaction_id IN (
2907
            SELECT interaction_id FROM openclasp_federated_interactions
2908
            WHERE status = 'expired' AND expires_at <= ${now.toISOString()}
2909
          )
2910
      `;
2911
      await Promise.all(
2912
        expiredInteractions.flatMap((row) => {
2913
          const interactionId = String(row.interaction_id);
2914
          return [
2915
            this.journalFederatedInteraction(interactionId),
2916
            this.journalLiveSessionState(interactionId),
2917
          ];
2918
        }),
2919
      );
2920
    }
2921
    const [rows, backfillRows] = await Promise.all([
2922
      this.sql`
2923
        SELECT operator_id, record_id, payload FROM openclasp_records
2924
        WHERE kind = 'feedback_request'
2925
          AND payload->>'status' = 'pending'
2926
          AND (payload->>'dueAt')::timestamptz <= ${now.toISOString()}
2927
        LIMIT 500
2928
      `,
2929
      this.sql`
2930
        SELECT source.interaction_id
2931
        FROM (
2932
          SELECT conclusion.payload->>'interactionId' AS interaction_id
2933
          FROM openclasp_records conclusion
2934
          WHERE conclusion.kind = 'interaction_conclusion'
2935
            AND NOT EXISTS (
2936
              SELECT 1 FROM openclasp_records eligibility
2937
              WHERE eligibility.operator_id = conclusion.operator_id
2938
                AND eligibility.kind = 'learning_eligibility'
2939
                AND eligibility.payload->>'interactionId' = conclusion.payload->>'interactionId'
2940
            )
2941
          UNION
2942
          SELECT report.payload->>'interactionId' AS interaction_id
2943
          FROM openclasp_records report
2944
          WHERE report.kind = 'completion_report'
2945
            AND NOT EXISTS (
2946
              SELECT 1 FROM openclasp_records conclusion
2947
              WHERE conclusion.kind = 'interaction_conclusion'
2948
                AND conclusion.payload->>'interactionId' = report.payload->>'interactionId'
2949
            )
2950
        ) source
2951
        WHERE source.interaction_id IS NOT NULL
2952
        GROUP BY source.interaction_id
2953
        ORDER BY source.interaction_id ASC
2954
        LIMIT 200
2955
      `,
2956
    ]);
2957
    const interactions = new Set<string>();
2958
    for (const row of rows) {
2959
      const request = FeedbackRequestSchema.parse(row.payload);
2960
      const base = { ...request, status: 'expired' as const };
2961
      delete base.platformAttestation;
2962
      const expired = FeedbackRequestSchema.parse({
2963
        ...base,
2964
        platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
2965
      });
2966
      await this.upsert(
2967
        String(row.operator_id),
2968
        'feedback_request',
2969
        String(row.record_id),
2970
        expired,
2971
      );
2972
      interactions.add(request.interactionId);
2973
    }
2974
    for (const row of backfillRows) interactions.add(String(row.interaction_id));
2975
    let released = 0;
2976
    for (const interactionId of interactions) {
2977
      const result = await this.finalizeInteractionConclusion(interactionId);
2978
      if (result.released) released += 1;
2979
    }
2980
    return {
2981
      expired: rows.length,
2982
      interactionsExpired: expiredInteractions.length,
2983
      released,
2984
      backfilled: backfillRows.length,
2985
    };
2986
  }
2987
2988
  async recordSessionFeedback(token: string, value: InteractionFeedback) {
2989
    const feedback = InteractionFeedbackSchema.parse(value);
2990
    const grant = this.verifySessionGrant(token);
2991
    if (
2992
      grant.interactionId !== feedback.interactionId ||
2993
      grant.senderAgentId !== feedback.reviewerAgentId ||
2994
      grant.recipientAgentId !== feedback.subjectAgentId
2995
    )
2996
      throw new Error('Session credential does not match the feedback');
2997
    const owners = await this.sql`
2998
      SELECT initiator_operator_id, responder_operator_id,
2999
        initiator_agent_id, responder_agent_id
3000
      FROM openclasp_federated_interactions
3001
      WHERE interaction_id = ${feedback.interactionId}
3002
      LIMIT 1
3003
    `;
3004
    const row = owners[0];
3005
    if (!row) throw new Error('Interaction not found');
3006
    const operatorId = String(
3007
      row.initiator_agent_id === feedback.reviewerAgentId
3008
        ? row.initiator_operator_id
3009
        : row.responder_operator_id,
3010
    );
3011
    return this.submitInteractionFeedback(
3012
      operatorId,
3013
      feedback.reviewerAgentId,
3014
      feedback,
3015
      'runtime_session',
3016
    );
3017
  }
3018
3019
  private async ownedTemporaryAgent(operatorId: string, agentId: string) {
3020
    const rows = await this.sql`
3021
      SELECT payload FROM openclasp_records
3022
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
3023
      LIMIT 1
3024
    `;
3025
    const profile = rows[0]?.payload as Partial<AgentProfile> | undefined;
3026
    if (!profile) throw new Error('Agent is not owned by this account');
3027
    const mode =
3028
      profile.agentMode ?? (profile.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat');
3029
    if (mode !== 'temporary_chat')
3030
      throw new Error('Hosted conversations are available only to temporary chat agents');
3031
    return profile;
3032
  }
3033
3034
  private hostedMessage(row: Record<string, any>): HostedMessage {
3035
    const decrypted = this.decryptGatewayPayload({
3036
      ciphertext: String(row.content_ciphertext),
3037
      iv: String(row.content_iv),
3038
      authTag: String(row.content_auth_tag),
3039
    }) as { content?: unknown };
3040
    return HostedMessageSchema.parse({
3041
      messageId: String(row.message_id),
3042
      threadId: String(row.thread_id),
3043
      interactionId: String(row.interaction_id),
3044
      senderAgentId: String(row.sender_agent_id),
3045
      recipientAgentId: String(row.recipient_agent_id),
3046
      contentType: 'text/plain',
3047
      content: decrypted.content,
3048
      contentHash: String(row.content_hash),
3049
      delivery: row.delivery,
3050
      createdAt: new Date(String(row.created_at)).toISOString(),
3051
      ...(row.read_at ? { readAt: new Date(String(row.read_at)).toISOString() } : {}),
3052
    });
3053
  }
3054
3055
  private async storeHostedMessage(input: {
3056
    interactionId: string;
3057
    senderAgentId: string;
3058
    recipientAgentId: string;
3059
    requestKey: string;
3060
    content: string;
3061
    delivery: 'accepted' | 'delivered';
3062
  }) {
3063
    const content = input.content.trim();
3064
    if (!content || content.length > 20_000)
3065
      throw new Error('Message must contain 1-20000 characters');
3066
    const sessions = await this.sql`
3067
      SELECT session.status, interaction.status AS interaction_status,
3068
        session.initiator_agent_id, session.responder_agent_id
3069
      FROM openclasp_live_sessions session
3070
      INNER JOIN openclasp_federated_interactions interaction
3071
        ON interaction.interaction_id = session.interaction_id
3072
      WHERE session.interaction_id = ${input.interactionId}
3073
      LIMIT 1
3074
    `;
3075
    const session = sessions[0];
3076
    if (!session || session.status !== 'active' || session.interaction_status !== 'active')
3077
      throw new Error('Active interaction not found');
3078
    const participants = new Set([
3079
      String(session.initiator_agent_id),
3080
      String(session.responder_agent_id),
3081
    ]);
3082
    if (!participants.has(input.senderAgentId) || !participants.has(input.recipientAgentId))
3083
      throw new Error('Message participants do not match the active interaction');
3084
    const now = new Date();
3085
    const threadId = input.interactionId;
3086
    const expiresAt = new Date(now.getTime() + 30 * 86_400_000).toISOString();
3087
    await this.sql`
3088
      INSERT INTO openclasp_hosted_threads(
3089
        thread_id, interaction_id, participant_a_agent_id, participant_b_agent_id,
3090
        status, expires_at
3091
      ) VALUES (
3092
        ${threadId}, ${input.interactionId}, ${String(session.initiator_agent_id)},
3093
        ${String(session.responder_agent_id)}, 'open', ${expiresAt}
3094
      )
3095
      ON CONFLICT (thread_id) DO UPDATE SET updated_at = NOW()
3096
      WHERE openclasp_hosted_threads.status = 'open'
3097
    `;
3098
    const thread = await this.sql`
3099
      SELECT status, expires_at FROM openclasp_hosted_threads WHERE thread_id = ${threadId}
3100
    `;
3101
    if (thread[0]?.status !== 'open') throw new Error('Hosted thread is closed');
3102
    if (Date.parse(String(thread[0].expires_at)) <= Date.now())
3103
      throw new Error('Hosted thread has expired');
3104
    const encrypted = encryptGatewayPayload(this.gatewaySecret(), { content });
3105
    const messageId = crypto.randomUUID();
3106
    const contentHash = canonicalHash(content);
3107
    const inserted = await this.sql`
3108
      INSERT INTO openclasp_hosted_messages(
3109
        message_id, thread_id, interaction_id, sender_agent_id, recipient_agent_id,
3110
        request_key, content_ciphertext, content_iv, content_auth_tag, content_hash, delivery
3111
      ) VALUES (
3112
        ${messageId}, ${threadId}, ${input.interactionId}, ${input.senderAgentId},
3113
        ${input.recipientAgentId}, ${input.requestKey}, ${encrypted.ciphertext}, ${encrypted.iv},
3114
        ${encrypted.authTag}, ${contentHash}, ${input.delivery}
3115
      )
3116
      ON CONFLICT (thread_id, sender_agent_id, request_key) DO NOTHING
3117
      RETURNING *
3118
    `;
3119
    if (!inserted[0]) {
3120
      const existing = await this.sql`
3121
        SELECT * FROM openclasp_hosted_messages
3122
        WHERE thread_id = ${threadId} AND sender_agent_id = ${input.senderAgentId}
3123
          AND request_key = ${input.requestKey}
3124
        LIMIT 1
3125
      `;
3126
      return { message: this.hostedMessage(existing[0]!), deduplicated: true };
3127
    }
3128
    const sequenceRows = await this.sql`
3129
      SELECT COALESCE(MAX(sequence), -1) + 1 AS sequence
3130
      FROM openclasp_live_session_events
3131
      WHERE interaction_id = ${input.interactionId} AND agent_id = ${input.senderAgentId}
3132
    `;
3133
    const event = LiveSessionEventSchema.parse({
3134
      eventId: crypto.randomUUID(),
3135
      interactionId: input.interactionId,
3136
      agentId: input.senderAgentId,
3137
      sequence: Number(sequenceRows[0]?.sequence ?? 0),
3138
      type: 'message_sent',
3139
      occurredAt: now.toISOString(),
3140
      messageHash: contentHash,
3141
      evidenceReferences: [],
3142
      details: { labels: ['hosted_temporary'], metrics: {}, flags: { rawStoredEncrypted: true } },
3143
    });
3144
    const eventWithAttestation = JSON.stringify({
3145
      ...event,
3146
      attestation: attestSessionRecord(this.gatewaySecret(), event),
3147
    });
3148
    await this.sql`
3149
      INSERT INTO openclasp_live_session_events(interaction_id, event_id, agent_id, sequence, event)
3150
      VALUES (${event.interactionId}, ${event.eventId}, ${event.agentId}, ${event.sequence}, ${eventWithAttestation}::jsonb)
3151
      ON CONFLICT DO NOTHING
3152
    `;
3153
    await this.journalLiveSessionEvent(JSON.parse(eventWithAttestation) as Record<string, unknown>);
3154
    return { message: this.hostedMessage(inserted[0]), deduplicated: false };
3155
  }
3156
3157
  async receiveTemporaryMessage(
3158
    token: string,
3159
    recipientAgentId: string,
3160
    requestKey: string,
3161
    content: string,
3162
  ) {
3163
    await this.ensureSchema();
3164
    const grant = this.verifySessionGrant(token);
3165
    if (grant.recipientAgentId !== recipientAgentId)
3166
      throw new Error('Session credential is not valid for this temporary agent');
3167
    const recipient = await this.sessionParticipant(recipientAgentId);
3168
    if (recipient.mode !== 'temporary_chat')
3169
      throw new Error('Target is not a temporary chat agent');
3170
    const stored = await this.storeHostedMessage({
3171
      interactionId: grant.interactionId,
3172
      senderAgentId: grant.senderAgentId,
3173
      recipientAgentId,
3174
      requestKey,
3175
      content,
3176
      delivery: 'delivered',
3177
    });
3178
    return stored;
3179
  }
3180
3181
  async sendTemporaryMessage(
3182
    operatorId: string,
3183
    senderAgentId: string,
3184
    interactionId: string,
3185
    content: string,
3186
  ) {
3187
    await this.ensureSchema();
3188
    await this.ownedTemporaryAgent(operatorId, senderAgentId);
3189
    const session = await this.getLiveSession(operatorId, interactionId, senderAgentId);
3190
    const peer = await this.sessionParticipant(session.peer.agentId);
3191
    if (peer.mode !== 'persistent_runtime')
3192
      throw new Error('Temporary-to-temporary hosted conversations are not supported');
3193
    const requestKey = crypto.randomUUID();
3194
    const stored = await this.storeHostedMessage({
3195
      interactionId,
3196
      senderAgentId,
3197
      recipientAgentId: session.peer.agentId,
3198
      requestKey,
3199
      content,
3200
      delivery: 'accepted',
3201
    });
3202
    const response = await postRuntimeJson(
3203
      session.peer.endpoint,
3204
      {
3205
        jsonrpc: '2.0',
3206
        id: requestKey,
3207
        method: 'message/send',
3208
        params: {
3209
          message: {
3210
            role: 'user',
3211
            parts: [{ kind: 'text', text: content.trim() }],
3212
            metadata: {
3213
              [DEFAULT_EXTENSION_URI]: {
3214
                interactionId,
3215
                termsHash: session.contractHash,
3216
                initiatorAgentId:
3217
                  session.role === 'initiator' ? senderAgentId : session.peer.agentId,
3218
                responderAgentId:
3219
                  session.role === 'responder' ? senderAgentId : session.peer.agentId,
3220
              },
3221
            },
3222
          },
3223
        },
3224
      },
3225
      {
3226
        authorization: `Bearer ${session.peer.bearerToken}`,
3227
        'A2A-Extensions': DEFAULT_EXTENSION_URI,
3228
      },
3229
    );
3230
    if (response.status < 200 || response.status >= 300)
3231
      throw new Error(`Peer A2A endpoint returned HTTP ${response.status}`);
3232
    await this.sql`
3233
      UPDATE openclasp_hosted_messages SET delivery = 'delivered'
3234
      WHERE message_id = ${stored.message.messageId}
3235
    `;
3236
    return {
3237
      ...stored,
3238
      message: { ...stored.message, delivery: 'delivered' as const },
3239
      peerResponse: response.body,
3240
    };
3241
  }
3242
3243
  async listHostedThreads(operatorId: string, agentId: string): Promise<HostedThread[]> {
3244
    await this.ensureSchema();
3245
    await this.ownedTemporaryAgent(operatorId, agentId);
3246
    await this.sql`DELETE FROM openclasp_hosted_threads WHERE expires_at <= NOW()`;
3247
    const rows = await this.sql`
3248
      SELECT thread.*,
3249
        (SELECT COUNT(*) FROM openclasp_hosted_messages message
3250
          WHERE message.thread_id = thread.thread_id
3251
            AND message.recipient_agent_id = ${agentId} AND message.read_at IS NULL) AS unread_count
3252
      FROM openclasp_hosted_threads thread
3253
      WHERE ${agentId} IN (thread.participant_a_agent_id, thread.participant_b_agent_id)
3254
      ORDER BY thread.updated_at DESC
3255
    `;
3256
    return rows.map((row) =>
3257
      HostedThreadSchema.parse({
3258
        threadId: String(row.thread_id),
3259
        interactionId: String(row.interaction_id),
3260
        participantAgentIds: [
3261
          String(row.participant_a_agent_id),
3262
          String(row.participant_b_agent_id),
3263
        ],
3264
        status: row.status,
3265
        privacyMode: 'openclasp_hosted_temporary',
3266
        unreadCount: Number(row.unread_count ?? 0),
3267
        createdAt: new Date(String(row.created_at)).toISOString(),
3268
        updatedAt: new Date(String(row.updated_at)).toISOString(),
3269
        expiresAt: new Date(String(row.expires_at)).toISOString(),
3270
      }),
3271
    );
3272
  }
3273
3274
  async getHostedThread(operatorId: string, agentId: string, threadId: string) {
3275
    const threads = await this.listHostedThreads(operatorId, agentId);
3276
    const thread = threads.find((value) => value.threadId === threadId);
3277
    if (!thread) throw new Error('Hosted thread not found');
3278
    const rows = await this.sql`
3279
      SELECT * FROM openclasp_hosted_messages
3280
      WHERE thread_id = ${threadId} ORDER BY created_at ASC
3281
    `;
3282
    const peerAgentId = thread.participantAgentIds.find((value) => value !== agentId);
3283
    if (!peerAgentId) throw new Error('Hosted thread has invalid participants');
3284
    const peer = await this.sessionParticipant(peerAgentId);
3285
    const interactions = await this.sql`
3286
      SELECT payload FROM openclasp_federated_interactions
3287
      WHERE interaction_id = ${thread.interactionId} LIMIT 1
3288
    `;
3289
    const interaction = FederatedInteractionSchema.parse(interactions[0]?.payload);
3290
    const insights = await this.privateCounterpartyInsights(
3291
      operatorId,
3292
      peer,
3293
      interaction.contract.taskCategory,
3294
    );
3295
    return { thread, messages: rows.map((row) => this.hostedMessage(row)), insights };
3296
  }
3297
3298
  async markHostedThreadRead(operatorId: string, agentId: string, threadId: string) {
3299
    await this.getHostedThread(operatorId, agentId, threadId);
3300
    const rows = await this.sql`
3301
      UPDATE openclasp_hosted_messages
3302
      SET delivery = 'read', read_at = COALESCE(read_at, NOW())
3303
      WHERE thread_id = ${threadId} AND recipient_agent_id = ${agentId} AND read_at IS NULL
3304
      RETURNING message_id
3305
    `;
3306
    return { threadId, markedRead: rows.length };
3307
  }
3308
3309
  async closeHostedThread(operatorId: string, agentId: string, threadId: string) {
3310
    await this.getHostedThread(operatorId, agentId, threadId);
3311
    await this.sql`
3312
      UPDATE openclasp_hosted_threads SET status = 'closed', updated_at = NOW()
3313
      WHERE thread_id = ${threadId}
3314
    `;
3315
    return { threadId, status: 'closed' as const };
3316
  }
3317
3318
  async recordLiveSessionEvent(token: string, value: LiveSessionEvent) {
3319
    await this.ensureSchema();
3320
    const event = LiveSessionEventSchema.parse(value);
3321
    const grant = this.verifySessionGrant(token);
3322
    if (grant.interactionId !== event.interactionId || grant.senderAgentId !== event.agentId)
3323
      throw new Error('Session credential does not match the event');
3324
    const sessions = await this.sql`
3325
      SELECT 1 FROM openclasp_live_sessions
3326
      WHERE interaction_id = ${event.interactionId}
3327
        AND status = 'active'
3328
        AND ${event.agentId} IN (initiator_agent_id, responder_agent_id)
3329
    `;
3330
    if (!sessions.length) throw new Error('Active live session not found');
3331
    const attestation = attestSessionRecord(this.gatewaySecret(), event);
3332
    const encoded = JSON.stringify({ ...event, attestation });
3333
    const rows = await this.sql`
3334
      INSERT INTO openclasp_live_session_events(
3335
        interaction_id, event_id, agent_id, sequence, event
3336
      ) SELECT ${event.interactionId}, ${event.eventId}, ${event.agentId}, ${event.sequence}, ${encoded}::jsonb
3337
      WHERE (
3338
        SELECT COUNT(*) FROM openclasp_live_session_events
3339
        WHERE interaction_id = ${event.interactionId}
3340
      ) < 1000
3341
      ON CONFLICT DO NOTHING
3342
      RETURNING event_id
3343
    `;
3344
    if (rows.length)
3345
      await this.journalLiveSessionEvent({
3346
        ...event,
3347
        attestation,
3348
      });
3349
    if (event.type === 'session_completed' || event.type === 'session_failed') {
3350
      const terminal = await this.sql`
3351
        SELECT COUNT(DISTINCT agent_id) AS count
3352
        FROM openclasp_live_session_events
3353
        WHERE interaction_id = ${event.interactionId}
3354
          AND event->>'type' IN ('session_completed', 'session_failed')
3355
      `;
3356
      if (Number(terminal[0]?.count ?? 0) >= 2) {
3357
        await this.sql`
3358
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
3359
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
3360
        `;
3361
        const completedAt = new Date().toISOString();
3362
        await this.sql`
3363
          UPDATE openclasp_federated_interactions
3364
          SET status = 'completed',
3365
            payload = jsonb_set(
3366
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
3367
              '{updatedAt}', to_jsonb(${completedAt}::text)
3368
            ),
3369
            updated_at = NOW()
3370
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
3371
        `;
3372
        await Promise.all([
3373
          this.journalLiveSessionState(event.interactionId),
3374
          this.journalFederatedInteraction(event.interactionId),
3375
        ]);
3376
      }
3377
    }
3378
    return {
3379
      recorded: rows.length > 0,
3380
      deduplicated: rows.length === 0,
3381
      eventId: event.eventId,
3382
      attestation,
3383
    };
3384
  }
3385
3386
  async createFederatedInteraction(
3387
    operatorId: string,
3388
    value: FederatedInteraction,
3389
  ): Promise<FederatedInteraction> {
3390
    await this.ensureSchema();
3391
    let interaction = FederatedInteractionSchema.parse(value);
3392
    if (interaction.status !== 'pending') throw new Error('New interactions must be pending');
3393
    if (interaction.contractRevision !== 1 || interaction.contractRevisions.length)
3394
      throw new Error('New interactions cannot supply contract revision history');
3395
    if (interaction.contract.interactionId !== interaction.interactionId)
3396
      throw new Error('Contract interaction ID does not match');
3397
    if (canonicalHash(interaction.contract) !== interaction.termsHash)
3398
      throw new Error('Contract hash does not match the immutable terms');
3399
    if (
3400
      interaction.contract.parties.length !== 2 ||
3401
      interaction.contract.parties[0] !== interaction.initiatorAgentId ||
3402
      interaction.contract.parties[1] !== interaction.responderAgentId
3403
    )
3404
      throw new Error('Contract parties do not match the interaction participants');
3405
    const acceptanceEntries = Object.entries(interaction.acceptances);
3406
    if (
3407
      acceptanceEntries.length !== 1 ||
3408
      acceptanceEntries[0]?.[0] !== interaction.initiatorAgentId ||
3409
      acceptanceEntries[0]?.[1].agentId !== interaction.initiatorAgentId ||
3410
      acceptanceEntries[0]?.[1].termsHash !== interaction.termsHash ||
3411
      !['oauth_installation', 'oauth_account'].includes(acceptanceEntries[0]?.[1].method ?? '')
3412
    )
3413
      throw new Error('A pending interaction requires only the initiator acceptance');
3414
    if (Date.parse(interaction.expiresAt) <= Date.now())
3415
      throw new Error('Interaction expiry must be in the future');
3416
    interaction = withContractRevisionHistory(interaction);
3417
    const owners = await this.sql`
3418
      SELECT agent_id, operator_id, card FROM openclasp_public_agents
3419
      WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
3420
    `;
3421
    const initiator = owners.find((row) => row.agent_id === interaction.initiatorAgentId);
3422
    const responder = owners.find((row) => row.agent_id === interaction.responderAgentId);
3423
    if (!initiator || initiator.operator_id !== operatorId)
3424
      throw new Error('The initiating agent must be published and owned by this account');
3425
    if (!responder) throw new Error('Responder is not a published OpenClasp agent');
3426
    if (responder.operator_id === operatorId)
3427
      throw new Error('Federated interactions require agents from different accounts');
3428
    const responderCard = normalizePublicAgentCard(responder.card);
3429
    if (
3430
      !responderCard.transports.some(
3431
        (transport) =>
3432
          transport.endpoint === interaction.responderTransport.endpoint &&
3433
          transport.protocolBinding === interaction.responderTransport.protocolBinding,
3434
      )
3435
    )
3436
      throw new Error('Responder transport does not match its published Agent Card');
3437
    const encoded = JSON.stringify(interaction);
3438
    const rows = await this.sql`
3439
      INSERT INTO openclasp_federated_interactions(
3440
        interaction_id, initiator_operator_id, responder_operator_id,
3441
        initiator_agent_id, responder_agent_id, status, payload, expires_at
3442
      ) VALUES (
3443
        ${interaction.interactionId}, ${operatorId}, ${responder.operator_id},
3444
        ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
3445
        ${interaction.status}, ${encoded}::jsonb, ${interaction.expiresAt}
3446
      )
3447
      RETURNING payload
3448
    `;
3449
    const stored = FederatedInteractionSchema.parse(rows[0]?.payload);
3450
    await this.journalFederatedInteraction(stored.interactionId);
3451
    const profiles = await this.sql`
3452
      SELECT payload FROM openclasp_records
3453
      WHERE operator_id = ${responder.operator_id}
3454
        AND kind = 'agent_profile'
3455
        AND record_id = ${interaction.responderAgentId}
3456
      LIMIT 1
3457
    `;
3458
    const responderProfile = profiles[0]?.payload as AgentProfile | undefined;
3459
    if (responderProfile && canAutoAcceptInteraction(responderProfile, stored))
3460
      return this.respondToFederatedInteraction(
3461
        String(responder.operator_id),
3462
        stored.interactionId,
3463
        stored.responderAgentId,
3464
        'accept',
3465
        'policy_auto_accept',
3466
      );
3467
    return stored;
3468
  }
3469
3470
  async listFederatedInteractions(operatorId: string): Promise<FederatedInteraction[]> {
3471
    await this.ensureSchema();
3472
    const expired = await this.sql`
3473
      UPDATE openclasp_federated_interactions
3474
      SET status = 'expired', payload = jsonb_set(payload, '{status}', '"expired"'::jsonb), updated_at = NOW()
3475
      WHERE status = 'pending' AND expires_at <= NOW()
3476
      RETURNING interaction_id
3477
    `;
3478
    await Promise.all(
3479
      expired.map((row) => this.journalFederatedInteraction(String(row.interaction_id))),
3480
    );
3481
    const rows = await this.sql`
3482
      SELECT payload FROM openclasp_federated_interactions
3483
      WHERE initiator_operator_id = ${operatorId} OR responder_operator_id = ${operatorId}
3484
      ORDER BY updated_at DESC
3485
    `;
3486
    return rows.map((row) =>
3487
      withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload)),
3488
    );
3489
  }
3490
3491
  async getFederatedInteraction(
3492
    operatorId: string,
3493
    interactionId: string,
3494
  ): Promise<FederatedInteraction | undefined> {
3495
    const values = await this.listFederatedInteractions(operatorId);
3496
    return values.find((value) => value.interactionId === interactionId);
3497
  }
3498
3499
  async respondToFederatedInteraction(
3500
    operatorId: string,
3501
    interactionId: string,
3502
    agentId: string,
3503
    decision: 'accept' | 'reject',
3504
    method: 'oauth_installation' | 'oauth_account' | 'policy_auto_accept' = 'oauth_account',
3505
  ): Promise<FederatedInteraction> {
3506
    await this.ensureSchema();
3507
    const rows = await this.sql`
3508
      SELECT payload, responder_operator_id, responder_agent_id
3509
      FROM openclasp_federated_interactions WHERE interaction_id = ${interactionId}
3510
    `;
3511
    const row = rows[0];
3512
    if (!row) throw new Error('Interaction invitation not found');
3513
    if (row.responder_operator_id !== operatorId || row.responder_agent_id !== agentId)
3514
      throw new Error('Only the invited agent may respond');
3515
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
3516
    if (current.status !== 'pending') throw new Error('Invitation is no longer pending');
3517
    const now = new Date().toISOString();
3518
    if (Date.parse(current.expiresAt) <= Date.now()) {
3519
      const expired = { ...current, status: 'expired' as const, updatedAt: now };
3520
      await this.updateFederatedInteraction(
3521
        interactionId,
3522
        current.status,
3523
        expired,
3524
        current.updatedAt,
3525
      );
3526
      throw new Error('Invitation has expired');
3527
    }
3528
    const proposed = openContractRevision(current);
3529
    if (!proposed || proposed.termsHash !== current.termsHash)
3530
      throw new Error('No current contract proposal is available');
3531
    const acceptance = {
3532
      agentId,
3533
      method,
3534
      termsHash: current.termsHash,
3535
      acceptedAt: now,
3536
    };
3537
    const proposalBase: ContractRevision = {
3538
      ...proposed,
3539
      status: decision === 'accept' ? 'accepted' : 'rejected',
3540
      acceptances:
3541
        decision === 'accept'
3542
          ? { ...proposed.acceptances, [agentId]: acceptance }
3543
          : proposed.acceptances,
3544
      updatedAt: now,
3545
    };
3546
    const resolvedProposal =
3547
      decision === 'accept'
3548
        ? ContractRevisionSchema.parse({
3549
            ...proposalBase,
3550
            platformAttestation: attestSessionRecord(this.gatewaySecret(), proposalBase),
3551
          })
3552
        : proposalBase;
3553
    const next = FederatedInteractionSchema.parse({
3554
      ...current,
3555
      status: decision === 'accept' ? 'active' : 'rejected',
3556
      updatedAt: now,
3557
      acceptances: decision === 'accept' ? resolvedProposal.acceptances : current.acceptances,
3558
      contractRevisions: current.contractRevisions.map((revision) =>
3559
        revision.revisionId === resolvedProposal.revisionId ? resolvedProposal : revision,
3560
      ),
3561
    });
3562
    if (decision === 'accept') {
3563
      try {
3564
        await this.brokerLiveSession(next);
3565
      } catch (error) {
3566
        const reason = error instanceof Error ? error.message.slice(0, 500) : 'Live session failed';
3567
        await this.sql`
3568
          UPDATE openclasp_agent_runtimes SET last_error = ${reason}, updated_at = NOW()
3569
          WHERE agent_id IN (${current.initiatorAgentId}, ${current.responderAgentId})
3570
        `;
3571
        throw error;
3572
      }
3573
    }
3574
    const updated = await this.updateFederatedInteraction(
3575
      interactionId,
3576
      current.status,
3577
      next,
3578
      current.updatedAt,
3579
    );
3580
    if (!updated) throw new Error('Invitation was already handled');
3581
    return next;
3582
  }
3583
3584
  async proposeContractRevision(
3585
    operatorId: string,
3586
    interactionId: string,
3587
    agentId: string,
3588
    contract: InteractionContract,
3589
    expectedTermsHash?: string,
3590
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
3591
  ): Promise<FederatedInteraction> {
3592
    await this.ensureSchema();
3593
    const rows = await this.sql`
3594
      SELECT payload, initiator_operator_id, responder_operator_id,
3595
        initiator_agent_id, responder_agent_id
3596
      FROM openclasp_federated_interactions
3597
      WHERE interaction_id = ${interactionId}
3598
    `;
3599
    const row = rows[0];
3600
    if (!row) throw new Error('Interaction not found');
3601
    const ownsParticipant =
3602
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
3603
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
3604
    if (!ownsParticipant) throw new Error('Only a participating agent may propose contract terms');
3605
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
3606
    if (!['pending', 'active'].includes(current.status))
3607
      throw new Error('This interaction no longer accepts contract proposals');
3608
    const parsedContract = InteractionContractSchema.parse(contract);
3609
    if (parsedContract.interactionId !== interactionId)
3610
      throw new Error('Contract interaction ID does not match');
3611
    if (
3612
      parsedContract.parties.length !== 2 ||
3613
      parsedContract.parties[0] !== current.initiatorAgentId ||
3614
      parsedContract.parties[1] !== current.responderAgentId
3615
    )
3616
      throw new Error('Contract parties cannot change during negotiation');
3617
    const open = openContractRevision(current);
3618
    const expected = open?.termsHash ?? current.termsHash;
3619
    if (expectedTermsHash && expectedTermsHash !== expected)
3620
      throw new Error('Contract terms changed; fetch the interaction before countering');
3621
    const termsHash = canonicalHash(parsedContract);
3622
    if (termsHash === expected) throw new Error('Proposed terms are unchanged');
3623
    const now = new Date().toISOString();
3624
    const revisionNumber =
3625
      Math.max(
3626
        current.contractRevision,
3627
        ...current.contractRevisions.map((item) => item.revision),
3628
      ) + 1;
3629
    const acceptance = {
3630
      agentId,
3631
      method,
3632
      termsHash,
3633
      acceptedAt: now,
3634
    } as const;
3635
    const revision = ContractRevisionSchema.parse({
3636
      revisionId: crypto.randomUUID(),
3637
      interactionId,
3638
      revision: revisionNumber,
3639
      previousTermsHash: expected,
3640
      termsHash,
3641
      contract: parsedContract,
3642
      proposedByAgentId: agentId,
3643
      status: 'proposed',
3644
      acceptances: { [agentId]: acceptance },
3645
      createdAt: now,
3646
      updatedAt: now,
3647
    });
3648
    const revisions = [
3649
      ...current.contractRevisions.map((item) =>
3650
        item.status === 'proposed'
3651
          ? ContractRevisionSchema.parse({ ...item, status: 'superseded', updatedAt: now })
3652
          : item,
3653
      ),
3654
      revision,
3655
    ];
3656
    const next = FederatedInteractionSchema.parse({
3657
      ...current,
3658
      ...(current.status === 'pending'
3659
        ? {
3660
            contract: parsedContract,
3661
            termsHash,
3662
            acceptances: revision.acceptances,
3663
            contractRevision: revisionNumber,
3664
          }
3665
        : {}),
3666
      contractRevisions: revisions,
3667
      updatedAt: now,
3668
    });
3669
    const updated = await this.updateFederatedInteraction(
3670
      interactionId,
3671
      current.status,
3672
      next,
3673
      current.updatedAt,
3674
    );
3675
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
3676
    return next;
3677
  }
3678
3679
  async respondToContractRevision(
3680
    operatorId: string,
3681
    interactionId: string,
3682
    agentId: string,
3683
    revisionId: string,
3684
    decision: 'accept' | 'reject',
3685
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
3686
  ): Promise<FederatedInteraction> {
3687
    await this.ensureSchema();
3688
    const rows = await this.sql`
3689
      SELECT payload, initiator_operator_id, responder_operator_id,
3690
        initiator_agent_id, responder_agent_id
3691
      FROM openclasp_federated_interactions
3692
      WHERE interaction_id = ${interactionId}
3693
    `;
3694
    const row = rows[0];
3695
    if (!row) throw new Error('Interaction not found');
3696
    const ownsParticipant =
3697
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
3698
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
3699
    if (!ownsParticipant) throw new Error('Only a participating agent may respond to terms');
3700
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
3701
    if (!['pending', 'active'].includes(current.status))
3702
      throw new Error('This interaction no longer accepts contract responses');
3703
    const proposal = openContractRevision(current);
3704
    if (!proposal || proposal.revisionId !== revisionId)
3705
      throw new Error('Contract proposal is no longer current');
3706
    if (proposal.acceptances[agentId]) throw new Error('Agent already accepted this proposal');
3707
    const now = new Date().toISOString();
3708
    const acceptances =
3709
      decision === 'accept'
3710
        ? {
3711
            ...proposal.acceptances,
3712
            [agentId]: { agentId, method, termsHash: proposal.termsHash, acceptedAt: now },
3713
          }
3714
        : proposal.acceptances;
3715
    const fullyAccepted = current.contract.parties.every((party) => party in acceptances);
3716
    const revisionBase = ContractRevisionSchema.parse({
3717
      ...proposal,
3718
      status: decision === 'reject' ? 'rejected' : fullyAccepted ? 'accepted' : 'proposed',
3719
      acceptances,
3720
      updatedAt: now,
3721
    });
3722
    const revision =
3723
      fullyAccepted && decision === 'accept'
3724
        ? ContractRevisionSchema.parse({
3725
            ...revisionBase,
3726
            platformAttestation: attestSessionRecord(this.gatewaySecret(), revisionBase),
3727
          })
3728
        : revisionBase;
3729
    const next = FederatedInteractionSchema.parse({
3730
      ...current,
3731
      status:
3732
        current.status === 'pending'
3733
          ? decision === 'reject'
3734
            ? 'rejected'
3735
            : fullyAccepted
3736
              ? 'active'
3737
              : 'pending'
3738
          : 'active',
3739
      ...(fullyAccepted
3740
        ? {
3741
            contract: revision.contract,
3742
            termsHash: revision.termsHash,
3743
            acceptances: revision.acceptances,
3744
            contractRevision: revision.revision,
3745
          }
3746
        : {}),
3747
      contractRevisions: current.contractRevisions.map((item) =>
3748
        item.revisionId === revision.revisionId ? revision : item,
3749
      ),
3750
      updatedAt: now,
3751
    });
3752
    if (current.status === 'pending' && decision === 'accept' && fullyAccepted)
3753
      await this.brokerLiveSession(next);
3754
    const updated = await this.updateFederatedInteraction(
3755
      interactionId,
3756
      current.status,
3757
      next,
3758
      current.updatedAt,
3759
    );
3760
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
3761
    return next;
3762
  }
3763
3764
  private async updateFederatedInteraction(
3765
    interactionId: string,
3766
    expectedStatus: string,
3767
    value: FederatedInteraction,
3768
    expectedUpdatedAt?: string,
3769
  ): Promise<boolean> {
3770
    const encoded = JSON.stringify(FederatedInteractionSchema.parse(value));
3771
    const rows = await this.sql`
3772
      UPDATE openclasp_federated_interactions
3773
      SET status = ${value.status}, payload = ${encoded}::jsonb, updated_at = NOW()
3774
      WHERE interaction_id = ${interactionId} AND status = ${expectedStatus}
3775
        AND (${expectedUpdatedAt ?? null}::text IS NULL OR payload->>'updatedAt' = ${expectedUpdatedAt ?? null})
3776
      RETURNING interaction_id
3777
    `;
3778
    if (rows.length) await this.journalFederatedInteraction(interactionId);
3779
    return rows.length > 0;
3780
  }
3781
3782
  async getSettings(operatorId: string): Promise<AccountSettings> {
3783
    await this.ensureSchema();
3784
    const rows = await this.sql`
3785
      SELECT display_name, contribution_enabled, retention_days, evidence_sharing
3786
      FROM openclasp_account_settings
3787
      WHERE operator_id = ${operatorId}
3788
    `;
3789
    const row = rows[0] as
3790
      | {
3791
          display_name: string;
3792
          contribution_enabled: boolean;
3793
          retention_days: number;
3794
          evidence_sharing: AccountSettings['evidenceSharing'];
3795
        }
3796
      | undefined;
3797
    return row
3798
      ? {
3799
          displayName: row.display_name,
3800
          contributionEnabled: row.contribution_enabled,
3801
          retentionDays: row.retention_days,
3802
          evidenceSharing: row.evidence_sharing,
3803
          rawConversationsStored: false,
3804
        }
3805
      : defaults;
3806
  }
3807
3808
  async saveSettings(
3809
    operatorId: string,
3810
    settings: Omit<AccountSettings, 'rawConversationsStored'>,
3811
  ): Promise<AccountSettings> {
3812
    await this.ensureSchema();
3813
    await this.sql`
3814
      INSERT INTO openclasp_account_settings(
3815
        operator_id, display_name, contribution_enabled, retention_days, evidence_sharing
3816
      ) VALUES (
3817
        ${operatorId}, ${settings.displayName}, ${settings.contributionEnabled},
3818
        ${settings.retentionDays}, ${settings.evidenceSharing}
3819
      )
3820
      ON CONFLICT (operator_id) DO UPDATE SET
3821
        display_name = EXCLUDED.display_name,
3822
        contribution_enabled = EXCLUDED.contribution_enabled,
3823
        retention_days = EXCLUDED.retention_days,
3824
        evidence_sharing = EXCLUDED.evidence_sharing,
3825
        updated_at = NOW()
3826
    `;
3827
    return { ...settings, rawConversationsStored: false };
3828
  }
3829
}