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

Viewing files at commit 4818f40a6317Commit detailsView latest

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