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

Viewing files at commit f1f8fc237d6aCommit detailsView latest

packages/persistence/src/hosted.ts 1380 lines · 53.0 KB · typescript
1
import { neon, type NeonQueryFunction } from '@neondatabase/serverless';
2
import { randomBytes } from 'node:crypto';
3
import {
4
  DEFAULT_EXTENSION_URI,
5
  FederatedInteractionSchema,
6
  LiveSessionAcceptanceSchema,
7
  LiveSessionActivationSchema,
8
  LiveSessionEventSchema,
9
  LiveSessionOfferSchema,
10
  PublicAgentCardSchema,
11
  canonicalHash,
12
  type FederatedInteraction,
13
  type AgentPresence,
14
  type LiveSessionActivation,
15
  type LiveSessionEvent,
16
  type LiveSessionInsight,
17
  type PublicAgentCard,
18
} from '../../protocol/src/index.js';
19
import type { AgentProfile } from './onboarding.js';
20
import {
21
  encryptGatewayPayload,
22
  attestSessionRecord,
23
  getSessionKeyId,
24
  getSessionVerificationKey,
25
  issueSessionGrant,
26
  signSessionControl,
27
  verifySessionGrant,
28
} from './relay.js';
29
import { postRuntimeJson, resolvePublicRuntimeEndpoint } from './runtime.js';
30
31
export type HostedRecordKind =
32
  | 'agent'
33
  | 'delegation'
34
  | 'contract'
35
  | 'interaction'
36
  | 'event'
37
  | 'receipt'
38
  | 'feedback'
39
  | 'conflict'
40
  | 'profile'
41
  | 'consent'
42
  | 'revocation'
43
  | 'project'
44
  | 'agent_profile'
45
  | 'installation'
46
  | 'setup_request'
47
  | 'publication'
48
  | 'presence';
49
50
export type { PublicAgentCard, FederatedInteraction } from '../../protocol/src/index.js';
51
52
type ContextualProfile = {
53
  recordId: string;
54
  agentVersion: string;
55
  sampleSize: number;
56
  updatedAt: string;
57
  completion: number;
58
  acceptance: number;
59
  specification: number;
60
  deadline: number;
61
  communication: number;
62
  evidence: number;
63
  scope: number;
64
  disputes: number;
65
};
66
67
function normalizePublicAgentCard(value: unknown): PublicAgentCard {
68
  const current = PublicAgentCardSchema.safeParse(value);
69
  if (current.success) return current.data;
70
  const legacy = value as Record<string, unknown>;
71
  const agentId = String(legacy.agentId ?? '');
72
  const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
73
    /\/$/,
74
    '',
75
  );
76
  return PublicAgentCardSchema.parse({
77
    protocolVersion: '0.1',
78
    agentId,
79
    name: legacy.name,
80
    description: '',
81
    framework: legacy.framework,
82
    agentVersion: '1.0.0',
83
    capabilities: legacy.capabilities,
84
    limitations: legacy.limitations,
85
    assurance: legacy.assurance,
86
    transports:
87
      typeof legacy.a2aEndpoint === 'string'
88
        ? [
89
            {
90
              protocol: 'A2A/1.0',
91
              protocolBinding: 'JSONRPC',
92
              endpoint: legacy.a2aEndpoint,
93
              managedBy: 'agent',
94
            },
95
          ]
96
        : [],
97
    cardUrl: `${baseUrl}/agents/${encodeURIComponent(agentId)}/card.json`,
98
    a2aAgentCardUrl: `${baseUrl}/agents/${encodeURIComponent(agentId)}/a2a-agent-card.json`,
99
    extensionUri: DEFAULT_EXTENSION_URI,
100
    publishedAt: legacy.publishedAt,
101
    updatedAt: legacy.updatedAt,
102
  });
103
}
104
105
export function buildPublicAgentCard(
106
  agent: AgentProfile,
107
  baseUrl: string,
108
  previous?: PublicAgentCard,
109
): PublicAgentCard {
110
  const now = new Date().toISOString();
111
  const root = baseUrl.replace(/\/$/, '');
112
  return PublicAgentCardSchema.parse({
113
    protocolVersion: '0.1',
114
    agentId: agent.agentId,
115
    name: agent.name,
116
    description: agent.description ?? '',
117
    framework: agent.framework,
118
    agentVersion: agent.agentVersion ?? '1.0.0',
119
    capabilities: agent.capabilities,
120
    limitations: agent.limitations,
121
    assurance: 'oauth_authenticated',
122
    transports: agent.a2aEndpoint
123
      ? [
124
          {
125
            protocol: 'A2A/1.0',
126
            protocolBinding: 'JSONRPC',
127
            endpoint: agent.a2aEndpoint,
128
            managedBy: 'agent',
129
          },
130
        ]
131
      : [],
132
    cardUrl: `${root}/agents/${encodeURIComponent(agent.agentId)}/card.json`,
133
    a2aAgentCardUrl: `${root}/agents/${encodeURIComponent(agent.agentId)}/a2a-agent-card.json`,
134
    extensionUri: DEFAULT_EXTENSION_URI,
135
    publishedAt: previous?.publishedAt ?? now,
136
    updatedAt: now,
137
  });
138
}
139
140
const normalized = (value: string) =>
141
  value
142
    .trim()
143
    .toLowerCase()
144
    .replace(/[^a-z0-9]+/g, '_');
145
146
export const AGENT_ONLINE_WINDOW_MS = 2 * 60_000;
147
148
export function resolveAgentPresence(lastSeenAt?: string, now = new Date()): AgentPresence {
149
  const online = Boolean(
150
    lastSeenAt && now.getTime() - Date.parse(lastSeenAt) <= AGENT_ONLINE_WINDOW_MS,
151
  );
152
  return {
153
    status: online ? 'online' : 'offline',
154
    ...(lastSeenAt ? { lastSeenAt } : {}),
155
    checkedAt: now.toISOString(),
156
  };
157
}
158
159
export function canAutoAcceptInteraction(
160
  agent: AgentProfile,
161
  interaction: FederatedInteraction,
162
): boolean {
163
  if (agent.autoAcceptPolicy !== 'safe_matching' || agent.status !== 'active') return false;
164
  const categories = new Set(
165
    [...(agent.autoAcceptTaskCategories ?? []), ...(agent.capabilities ?? [])].map(normalized),
166
  );
167
  const taskCategory = normalized(interaction.contract.taskCategory);
168
  if (!taskCategory || !categories.has(taskCategory)) return false;
169
  if (interaction.contract.allowedData.length > 0) return false;
170
  if (
171
    /\b(password|secret|api[_ -]?key|access[_ -]?token|credential|bank|payment|medical|health|ssn|personal[_ -]?data|customer[_ -]?record)\b/i.test(
172
      `${interaction.contract.purpose} ${interaction.contract.requestedOutcome}`,
173
    )
174
  )
175
    return false;
176
  if (interaction.contract.humanApprovalRequirements.length > 0) return false;
177
  if (interaction.contract.retentionDays > 30) return false;
178
  if (interaction.contract.allowedActions.length > 0) {
179
    const capabilities = new Set((agent.capabilities ?? []).map(normalized));
180
    if (
181
      !interaction.contract.allowedActions.every((action) => capabilities.has(normalized(action)))
182
    )
183
      return false;
184
  }
185
  return true;
186
}
187
188
export type AccountSettings = {
189
  displayName: string;
190
  contributionEnabled: boolean;
191
  retentionDays: number;
192
  evidenceSharing: 'never' | 'ask' | 'contract_only';
193
  rawConversationsStored: false;
194
};
195
196
const defaults: AccountSettings = {
197
  displayName: '',
198
  contributionEnabled: false,
199
  retentionDays: 30,
200
  evidenceSharing: 'ask',
201
  rawConversationsStored: false,
202
};
203
204
export class HostedRepository {
205
  private readonly sql: NeonQueryFunction<false, false>;
206
  private initialized?: Promise<void>;
207
208
  constructor(databaseUrl: string) {
209
    this.sql = neon(databaseUrl);
210
  }
211
212
  private gatewaySecret() {
213
    const value = process.env.OPENCLASP_RELAY_ENCRYPTION_KEY;
214
    if (!value || value.length < 32) throw new Error('Gateway encryption is not configured');
215
    return value;
216
  }
217
218
  ensureSchema(): Promise<void> {
219
    return (this.initialized ??= (async () => {
220
      await this.sql`
221
        CREATE TABLE IF NOT EXISTS openclasp_records (
222
          operator_id TEXT NOT NULL,
223
          kind TEXT NOT NULL,
224
          record_id TEXT NOT NULL,
225
          payload JSONB NOT NULL,
226
          created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
227
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
228
          PRIMARY KEY (operator_id, kind, record_id)
229
        )
230
      `;
231
      await this.sql`
232
        CREATE INDEX IF NOT EXISTS openclasp_records_operator_created
233
        ON openclasp_records(operator_id, created_at DESC)
234
      `;
235
      await this.sql`
236
        CREATE TABLE IF NOT EXISTS openclasp_account_settings (
237
          operator_id TEXT PRIMARY KEY,
238
          display_name TEXT NOT NULL DEFAULT '',
239
          contribution_enabled BOOLEAN NOT NULL DEFAULT FALSE,
240
          retention_days INTEGER NOT NULL DEFAULT 30 CHECK (retention_days BETWEEN 0 AND 3650),
241
          evidence_sharing TEXT NOT NULL DEFAULT 'ask'
242
            CHECK (evidence_sharing IN ('never', 'ask', 'contract_only')),
243
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
244
        )
245
      `;
246
      await this.sql`
247
        CREATE TABLE IF NOT EXISTS openclasp_public_agents (
248
          agent_id TEXT PRIMARY KEY,
249
          operator_id TEXT NOT NULL,
250
          card JSONB NOT NULL,
251
          published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
252
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
253
        )
254
      `;
255
      await this.sql`
256
        CREATE INDEX IF NOT EXISTS openclasp_public_agents_updated
257
        ON openclasp_public_agents(updated_at DESC)
258
      `;
259
      await this.sql`
260
        CREATE TABLE IF NOT EXISTS openclasp_federated_interactions (
261
          interaction_id TEXT PRIMARY KEY,
262
          initiator_operator_id TEXT NOT NULL,
263
          responder_operator_id TEXT NOT NULL,
264
          initiator_agent_id TEXT NOT NULL,
265
          responder_agent_id TEXT NOT NULL,
266
          status TEXT NOT NULL,
267
          payload JSONB NOT NULL,
268
          expires_at TIMESTAMPTZ NOT NULL,
269
          created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
270
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
271
        )
272
      `;
273
      await this.sql`
274
        CREATE INDEX IF NOT EXISTS openclasp_federated_interactions_participants
275
        ON openclasp_federated_interactions(initiator_operator_id, responder_operator_id, updated_at DESC)
276
      `;
277
      await this.sql`
278
        CREATE TABLE IF NOT EXISTS openclasp_agent_runtimes (
279
          agent_id TEXT PRIMARY KEY,
280
          operator_id TEXT NOT NULL,
281
          endpoint TEXT NOT NULL,
282
          secret_ciphertext TEXT NOT NULL,
283
          secret_iv TEXT NOT NULL,
284
          secret_auth_tag TEXT NOT NULL,
285
          status TEXT NOT NULL CHECK (status IN ('verified', 'disabled')),
286
          verified_at TIMESTAMPTZ NOT NULL,
287
          last_delivery_at TIMESTAMPTZ,
288
          last_error TEXT,
289
          created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
290
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
291
        )
292
      `;
293
      await this.sql`
294
        ALTER TABLE openclasp_agent_runtimes
295
        ADD COLUMN IF NOT EXISTS a2a_endpoint TEXT,
296
        ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ
297
      `;
298
      await this.sql`
299
        CREATE TABLE IF NOT EXISTS openclasp_live_sessions (
300
          interaction_id TEXT PRIMARY KEY,
301
          initiator_agent_id TEXT NOT NULL,
302
          responder_agent_id TEXT NOT NULL,
303
          initiator_session_id TEXT NOT NULL,
304
          responder_session_id TEXT NOT NULL,
305
          initiator_endpoint TEXT NOT NULL,
306
          responder_endpoint TEXT NOT NULL,
307
          status TEXT NOT NULL CHECK (status IN ('preparing', 'active', 'completed', 'failed')),
308
          expires_at TIMESTAMPTZ NOT NULL,
309
          created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
310
          activated_at TIMESTAMPTZ,
311
          completed_at TIMESTAMPTZ,
312
          last_error TEXT
313
        )
314
      `;
315
      await this.sql`
316
        CREATE TABLE IF NOT EXISTS openclasp_live_session_events (
317
          interaction_id TEXT NOT NULL,
318
          event_id TEXT NOT NULL,
319
          agent_id TEXT NOT NULL,
320
          sequence INTEGER NOT NULL,
321
          event JSONB NOT NULL,
322
          created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
323
          PRIMARY KEY (interaction_id, event_id),
324
          UNIQUE (interaction_id, agent_id, sequence)
325
        )
326
      `;
327
    })());
328
  }
329
330
  getRuntimeVerificationKey() {
331
    return {
332
      algorithm: 'Ed25519' as const,
333
      keyId: getSessionKeyId(this.gatewaySecret()),
334
      publicKey: getSessionVerificationKey(this.gatewaySecret()),
335
    };
336
  }
337
338
  async upsert(
339
    operatorId: string,
340
    kind: HostedRecordKind,
341
    recordId: string,
342
    payload: unknown,
343
  ): Promise<void> {
344
    await this.ensureSchema();
345
    const encoded = JSON.stringify(payload);
346
    await this.sql`
347
      INSERT INTO openclasp_records(operator_id, kind, record_id, payload)
348
      VALUES (${operatorId}, ${kind}, ${recordId}, ${encoded}::jsonb)
349
      ON CONFLICT (operator_id, kind, record_id)
350
      DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()
351
    `;
352
  }
353
354
  async list(
355
    operatorId: string,
356
  ): Promise<{ kind: HostedRecordKind; recordId: string; payload: any }[]> {
357
    await this.ensureSchema();
358
    const rows = await this.sql`
359
      SELECT kind, record_id, payload
360
      FROM openclasp_records
361
      WHERE operator_id = ${operatorId}
362
      ORDER BY created_at ASC
363
    `;
364
    return rows.map((row) => ({
365
      kind: row.kind as HostedRecordKind,
366
      recordId: String(row.record_id),
367
      payload: row.payload,
368
    }));
369
  }
370
371
  async dashboard(operatorId: string) {
372
    const rows = await this.list(operatorId);
373
    const [federatedInteractions, runtimes, liveSessionRows, liveEventRows] = await Promise.all([
374
      this.listFederatedInteractions(operatorId),
375
      this.listAgentRuntimes(operatorId),
376
      this.sql`
377
        SELECT session.interaction_id, session.initiator_agent_id, session.responder_agent_id,
378
          session.status, session.created_at, session.activated_at, session.completed_at,
379
          session.expires_at, session.last_error
380
        FROM openclasp_live_sessions session
381
        INNER JOIN openclasp_federated_interactions interaction
382
          ON interaction.interaction_id = session.interaction_id
383
        WHERE interaction.initiator_operator_id = ${operatorId}
384
           OR interaction.responder_operator_id = ${operatorId}
385
        ORDER BY session.created_at DESC
386
      `,
387
      this.sql`
388
        SELECT events.event
389
        FROM openclasp_live_session_events events
390
        INNER JOIN openclasp_federated_interactions interaction
391
          ON interaction.interaction_id = events.interaction_id
392
        WHERE interaction.initiator_operator_id = ${operatorId}
393
           OR interaction.responder_operator_id = ${operatorId}
394
        ORDER BY events.created_at ASC
395
      `,
396
    ]);
397
    const ofKind = (kind: HostedRecordKind) =>
398
      rows.filter((row) => row.kind === kind).map((row) => row.payload);
399
    const presence = new Map(
400
      rows
401
        .filter((row) => row.kind === 'presence')
402
        .map((row) => [row.recordId, String(row.payload.lastSeenAt)]),
403
    );
404
    return {
405
      agents: [...ofKind('agent_profile'), ...ofKind('agent')].map((agent) => ({
406
        ...agent,
407
        presence: resolveAgentPresence(presence.get(String(agent.agentId))),
408
      })),
409
      projects: ofKind('project'),
410
      installations: ofKind('installation'),
411
      setupRequests: ofKind('setup_request'),
412
      publications: ofKind('publication'),
413
      interactions: ofKind('interaction'),
414
      events: [...ofKind('event'), ...liveEventRows.map((row) => row.event)],
415
      conflicts: ofKind('conflict'),
416
      receipts: ofKind('receipt'),
417
      profiles: ofKind('profile'),
418
      federatedInteractions,
419
      liveSessions: liveSessionRows.map((row) => ({
420
        interactionId: String(row.interaction_id),
421
        initiatorAgentId: String(row.initiator_agent_id),
422
        responderAgentId: String(row.responder_agent_id),
423
        status: String(row.status),
424
        createdAt: new Date(String(row.created_at)).toISOString(),
425
        ...(row.activated_at
426
          ? { activatedAt: new Date(String(row.activated_at)).toISOString() }
427
          : {}),
428
        ...(row.completed_at
429
          ? { completedAt: new Date(String(row.completed_at)).toISOString() }
430
          : {}),
431
        expiresAt: new Date(String(row.expires_at)).toISOString(),
432
        ...(row.last_error ? { lastError: String(row.last_error) } : {}),
433
      })),
434
      runtimes,
435
    };
436
  }
437
438
  async publishAgent(operatorId: string, card: PublicAgentCard): Promise<PublicAgentCard> {
439
    await this.ensureSchema();
440
    card = PublicAgentCardSchema.parse(card);
441
    const runtimes = await this.sql`
442
      SELECT a2a_endpoint, endpoint FROM openclasp_agent_runtimes
443
      WHERE operator_id = ${operatorId} AND agent_id = ${card.agentId} AND status = 'verified'
444
    `;
445
    if (runtimes[0]) {
446
      card = PublicAgentCardSchema.parse({
447
        ...card,
448
        transports: [
449
          {
450
            protocol: 'A2A/1.0',
451
            protocolBinding: 'JSONRPC',
452
            endpoint: String(runtimes[0].a2a_endpoint ?? runtimes[0].endpoint),
453
            managedBy: 'agent',
454
          },
455
        ],
456
      });
457
    }
458
    const encoded = JSON.stringify(card);
459
    const rows = await this.sql`
460
      INSERT INTO openclasp_public_agents(agent_id, operator_id, card, published_at, updated_at)
461
      VALUES (${card.agentId}, ${operatorId}, ${encoded}::jsonb, NOW(), NOW())
462
      ON CONFLICT (agent_id) DO UPDATE SET
463
        card = EXCLUDED.card,
464
        updated_at = NOW()
465
      WHERE openclasp_public_agents.operator_id = ${operatorId}
466
      RETURNING card
467
    `;
468
    const published = rows[0]?.card as PublicAgentCard | undefined;
469
    if (!published) throw new Error('Agent ID is already owned by another operator');
470
    return published;
471
  }
472
473
  async unpublishAgent(operatorId: string, agentId: string): Promise<boolean> {
474
    await this.ensureSchema();
475
    const rows = await this.sql`
476
      DELETE FROM openclasp_public_agents
477
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
478
      RETURNING agent_id
479
    `;
480
    return rows.length > 0;
481
  }
482
483
  async getPublishedAgent(agentId: string): Promise<PublicAgentCard | undefined> {
484
    await this.ensureSchema();
485
    const rows = await this.sql`
486
      SELECT agents.card, runtime.a2a_endpoint, runtime.endpoint
487
      FROM openclasp_public_agents agents
488
      LEFT JOIN openclasp_agent_runtimes runtime
489
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
490
      WHERE agents.agent_id = ${agentId}
491
    `;
492
    if (!rows[0]?.card) return undefined;
493
    let card = normalizePublicAgentCard(rows[0].card);
494
    if (rows[0].a2a_endpoint || rows[0].endpoint)
495
      card = PublicAgentCardSchema.parse({
496
        ...card,
497
        transports: [
498
          {
499
            protocol: 'A2A/1.0',
500
            protocolBinding: 'JSONRPC',
501
            endpoint: String(rows[0].a2a_endpoint ?? rows[0].endpoint),
502
            managedBy: 'agent',
503
          },
504
        ],
505
      });
506
    return { ...card, presence: await this.getAgentPresence(agentId) };
507
  }
508
509
  async searchPublishedAgents(input: {
510
    query?: string | undefined;
511
    capability?: string | undefined;
512
    limit?: number | undefined;
513
  }): Promise<PublicAgentCard[]> {
514
    await this.ensureSchema();
515
    const rows = await this.sql`
516
      SELECT agents.card, runtime.a2a_endpoint, runtime.endpoint
517
      FROM openclasp_public_agents agents
518
      LEFT JOIN openclasp_agent_runtimes runtime
519
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
520
      ORDER BY agents.updated_at DESC LIMIT 100
521
    `;
522
    const query = input.query?.trim().toLowerCase();
523
    const capability = input.capability?.trim().toLowerCase();
524
    const presenceRows = await this.sql`
525
      SELECT records.record_id, records.payload
526
      FROM openclasp_records records
527
      INNER JOIN openclasp_public_agents agents ON agents.agent_id = records.record_id
528
      WHERE records.kind = 'presence'
529
    `;
530
    const presence = new Map(
531
      presenceRows.map((row) => [String(row.record_id), String(row.payload.lastSeenAt)]),
532
    );
533
    return rows
534
      .map((row) => {
535
        let card = normalizePublicAgentCard(row.card);
536
        if (row.a2a_endpoint || row.endpoint)
537
          card = PublicAgentCardSchema.parse({
538
            ...card,
539
            transports: [
540
              {
541
                protocol: 'A2A/1.0',
542
                protocolBinding: 'JSONRPC',
543
                endpoint: String(row.a2a_endpoint ?? row.endpoint),
544
                managedBy: 'agent',
545
              },
546
            ],
547
          });
548
        return { ...card, presence: resolveAgentPresence(presence.get(card.agentId)) };
549
      })
550
      .filter(
551
        (card) =>
552
          (!query ||
553
            card.name.toLowerCase().includes(query) ||
554
            card.framework.toLowerCase().includes(query)) &&
555
          (!capability ||
556
            card.capabilities.some((value) => value.toLowerCase().includes(capability))),
557
      )
558
      .slice(0, Math.min(Math.max(input.limit ?? 20, 1), 50));
559
  }
560
561
  async touchAgentPresence(operatorId: string, agentId: string): Promise<AgentPresence> {
562
    await this.ensureSchema();
563
    const owned = await this.sql`
564
      SELECT 1 FROM openclasp_records
565
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
566
    `;
567
    if (!owned.length) throw new Error('Agent is not owned by this account');
568
    const lastSeenAt = new Date().toISOString();
569
    await this.upsert(operatorId, 'presence', agentId, { lastSeenAt });
570
    await this.sql`
571
      UPDATE openclasp_agent_runtimes SET last_seen_at = ${lastSeenAt}, updated_at = NOW()
572
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId} AND status = 'verified'
573
    `;
574
    return resolveAgentPresence(lastSeenAt);
575
  }
576
577
  async getAgentPresence(agentId: string): Promise<AgentPresence> {
578
    await this.ensureSchema();
579
    const rows = await this.sql`
580
      SELECT records.payload
581
      FROM openclasp_public_agents agents
582
      LEFT JOIN openclasp_records records
583
        ON records.operator_id = agents.operator_id
584
        AND records.kind = 'presence'
585
        AND records.record_id = agents.agent_id
586
      WHERE agents.agent_id = ${agentId}
587
      LIMIT 1
588
    `;
589
    if (!rows.length) throw new Error('Published agent not found');
590
    const lastSeenAt = rows[0]?.payload?.lastSeenAt;
591
    return resolveAgentPresence(typeof lastSeenAt === 'string' ? lastSeenAt : undefined);
592
  }
593
594
  async listAgentRuntimes(operatorId: string) {
595
    await this.ensureSchema();
596
    const rows = await this.sql`
597
      SELECT agent_id, endpoint, a2a_endpoint, status, verified_at, last_seen_at, last_error, updated_at
598
      FROM openclasp_agent_runtimes
599
      WHERE operator_id = ${operatorId}
600
      ORDER BY updated_at DESC
601
    `;
602
    return rows.map((row) => ({
603
      agentId: String(row.agent_id),
604
      endpoint: String(row.endpoint),
605
      a2aEndpoint: String(row.a2a_endpoint ?? row.endpoint),
606
      status: row.status as 'verified' | 'disabled',
607
      verifiedAt: new Date(String(row.verified_at)).toISOString(),
608
      ...(row.last_seen_at ? { lastSeenAt: new Date(String(row.last_seen_at)).toISOString() } : {}),
609
      ...(row.last_error ? { lastError: String(row.last_error) } : {}),
610
      updatedAt: new Date(String(row.updated_at)).toISOString(),
611
    }));
612
  }
613
614
  async registerAgentRuntime(operatorId: string, agentId: string, endpoint: string) {
615
    await this.ensureSchema();
616
    const owned = await this.sql`
617
      SELECT 1 FROM openclasp_records
618
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
619
    `;
620
    if (!owned.length) throw new Error('Agent is not owned by this account');
621
    const challenge = crypto.randomUUID();
622
    const verification = await postRuntimeJson(endpoint, {
623
      type: 'openclasp.runtime.verify',
624
      version: '1',
625
      agentId,
626
      challenge,
627
    });
628
    if (
629
      verification.status < 200 ||
630
      verification.status >= 300 ||
631
      !verification.body ||
632
      typeof verification.body !== 'object' ||
633
      (verification.body as { type?: unknown }).type !== 'openclasp.runtime.verified' ||
634
      (verification.body as { version?: unknown }).version !== '1' ||
635
      (verification.body as { agentId?: unknown }).agentId !== agentId ||
636
      (verification.body as { challenge?: unknown }).challenge !== challenge
637
    )
638
      throw new Error('Runtime endpoint did not return the verification challenge');
639
    const a2aEndpointValue = (verification.body as { a2aEndpoint?: unknown }).a2aEndpoint;
640
    const a2aEndpoint =
641
      typeof a2aEndpointValue === 'string' && a2aEndpointValue ? a2aEndpointValue : endpoint;
642
    await resolvePublicRuntimeEndpoint(a2aEndpoint);
643
    const runtimeSecret = randomBytes(32).toString('base64url');
644
    const encrypted = encryptGatewayPayload(this.gatewaySecret(), runtimeSecret);
645
    const verifiedAt = new Date().toISOString();
646
    const saved = await this.sql`
647
      INSERT INTO openclasp_agent_runtimes(
648
        agent_id, operator_id, endpoint, a2a_endpoint, secret_ciphertext, secret_iv, secret_auth_tag,
649
        status, verified_at, last_seen_at, updated_at
650
      ) VALUES (
651
        ${agentId}, ${operatorId}, ${endpoint}, ${a2aEndpoint}, ${encrypted.ciphertext},
652
        ${encrypted.iv}, ${encrypted.authTag}, 'verified', ${verifiedAt}, ${verifiedAt}, NOW()
653
      )
654
      ON CONFLICT (agent_id) DO UPDATE SET
655
        operator_id = EXCLUDED.operator_id,
656
        endpoint = EXCLUDED.endpoint,
657
        a2a_endpoint = EXCLUDED.a2a_endpoint,
658
        secret_ciphertext = EXCLUDED.secret_ciphertext,
659
        secret_iv = EXCLUDED.secret_iv,
660
        secret_auth_tag = EXCLUDED.secret_auth_tag,
661
        status = 'verified',
662
        verified_at = EXCLUDED.verified_at,
663
        last_seen_at = EXCLUDED.last_seen_at,
664
        last_error = NULL,
665
        updated_at = NOW()
666
      WHERE openclasp_agent_runtimes.operator_id = ${operatorId}
667
      RETURNING agent_id
668
    `;
669
    if (!saved.length) throw new Error('Agent runtime is owned by another account');
670
    await this.sql`
671
      UPDATE openclasp_public_agents
672
      SET card = jsonb_set(
673
        card,
674
        '{transports}',
675
        ${JSON.stringify([
676
          {
677
            protocol: 'A2A/1.0',
678
            protocolBinding: 'JSONRPC',
679
            endpoint: a2aEndpoint,
680
            managedBy: 'agent',
681
          },
682
        ])}::jsonb
683
      ), updated_at = NOW()
684
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
685
    `;
686
    await this.sql`
687
      UPDATE openclasp_records
688
      SET payload = jsonb_set(payload, '{a2aEndpoint}', to_jsonb(${a2aEndpoint}::text)),
689
        updated_at = NOW()
690
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
691
    `;
692
    await this.touchAgentPresence(operatorId, agentId);
693
    return {
694
      agentId,
695
      endpoint,
696
      a2aEndpoint,
697
      status: 'verified' as const,
698
      verifiedAt,
699
      verificationKey: getSessionVerificationKey(this.gatewaySecret()),
700
    };
701
  }
702
703
  async disableAgentRuntime(operatorId: string, agentId: string) {
704
    await this.ensureSchema();
705
    const rows = await this.sql`
706
      UPDATE openclasp_agent_runtimes
707
      SET status = 'disabled', updated_at = NOW()
708
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
709
      RETURNING agent_id
710
    `;
711
    if (!rows.length) throw new Error('Agent runtime not found');
712
    return { agentId, status: 'disabled' as const };
713
  }
714
715
  private async liveRuntime(agentId: string) {
716
    const rows = await this.sql`
717
      SELECT runtime.*, agents.card
718
      FROM openclasp_agent_runtimes runtime
719
      INNER JOIN openclasp_public_agents agents ON agents.agent_id = runtime.agent_id
720
      WHERE runtime.agent_id = ${agentId} AND runtime.status = 'verified'
721
      LIMIT 1
722
    `;
723
    const row = rows[0];
724
    if (!row) throw new Error(`Agent ${agentId} does not have a verified live runtime`);
725
    return {
726
      agentId,
727
      operatorId: String(row.operator_id),
728
      callbackEndpoint: String(row.endpoint),
729
      a2aEndpoint: String(row.a2a_endpoint ?? row.endpoint),
730
      card: normalizePublicAgentCard(row.card),
731
    };
732
  }
733
734
  private async signedRuntimeRequest(
735
    runtime: Awaited<ReturnType<HostedRepository['liveRuntime']>>,
736
    requestId: string,
737
    value: unknown,
738
  ) {
739
    const body = JSON.stringify(value);
740
    const timestamp = new Date().toISOString();
741
    return postRuntimeJson(runtime.callbackEndpoint, value, {
742
      'openclasp-request-id': requestId,
743
      'openclasp-timestamp': timestamp,
744
      'openclasp-signature': `v1=${signSessionControl(this.gatewaySecret(), requestId, timestamp, body)}`,
745
    });
746
  }
747
748
  private sessionGrant(
749
    interaction: FederatedInteraction,
750
    senderAgentId: string,
751
    recipientAgentId: string,
752
  ) {
753
    return issueSessionGrant(this.gatewaySecret(), {
754
      interactionId: interaction.interactionId,
755
      senderAgentId,
756
      recipientAgentId,
757
      expiresAt: Math.min(Date.parse(interaction.expiresAt), Date.now() + 60 * 60_000),
758
    });
759
  }
760
761
  private async privateCounterpartyInsights(
762
    viewerOperatorId: string,
763
    counterparty: Awaited<ReturnType<HostedRepository['liveRuntime']>>,
764
    taskCategory: string,
765
  ): Promise<LiveSessionInsight[]> {
766
    const rows = await this.sql`
767
      SELECT record_id, payload
768
      FROM openclasp_records
769
      WHERE operator_id = ${viewerOperatorId}
770
        AND kind = 'profile'
771
        AND payload->>'agentId' = ${counterparty.agentId}
772
        AND payload->>'taskCategory' = ${taskCategory}
773
      ORDER BY (payload->>'sampleSize')::integer DESC, updated_at DESC
774
    `;
775
    const profiles: ContextualProfile[] = rows
776
      .map<Record<string, unknown>>((row) => ({
777
        recordId: String(row.record_id),
778
        ...(row.payload as Record<string, unknown>),
779
      }))
780
      .filter(
781
        (profile) =>
782
          typeof profile.agentVersion === 'string' &&
783
          typeof profile.sampleSize === 'number' &&
784
          typeof profile.updatedAt === 'string' &&
785
          typeof profile.completion === 'number' &&
786
          typeof profile.acceptance === 'number' &&
787
          typeof profile.specification === 'number' &&
788
          typeof profile.deadline === 'number' &&
789
          typeof profile.communication === 'number' &&
790
          typeof profile.evidence === 'number' &&
791
          typeof profile.scope === 'number' &&
792
          typeof profile.disputes === 'number',
793
      ) as ContextualProfile[];
794
    const exact = profiles.find(
795
      (profile) => profile.agentVersion === counterparty.card.agentVersion,
796
    );
797
    const current = exact ?? profiles[0];
798
    const insights: LiveSessionInsight[] = [];
799
    if (!current) {
800
      insights.push({
801
        code: 'limited_verified_history',
802
        severity: 'caution',
803
        message: `No eligible ${taskCategory} history is available for this agent version. Ask for task-relevant evidence.`,
804
        evidenceReferences: [],
805
      });
806
    } else {
807
      const ageDays = Math.max(
808
        0,
809
        (Date.now() - Date.parse(String(current.updatedAt))) / 86_400_000,
810
      );
811
      const freshness = Math.exp(-ageDays / 180);
812
      const sampleSize = Number(current.sampleSize);
813
      const confidence = Math.min(0.95, (sampleSize / (sampleSize + 5)) * freshness);
814
      const versionChanged = current.agentVersion !== counterparty.card.agentVersion;
815
      const reference = `openclasp:profile:${current.recordId}`;
816
      insights.push({
817
        code: versionChanged ? 'version_history_only' : 'contextual_history',
818
        severity: versionChanged || confidence < 0.25 ? 'caution' : 'info',
819
        message: versionChanged
820
          ? `Only prior-version ${taskCategory} history is available (${sampleSize} eligible interaction${sampleSize === 1 ? '' : 's'}); confidence is reduced for version ${counterparty.card.agentVersion}.`
821
          : `Based on ${sampleSize} eligible ${taskCategory} interaction${sampleSize === 1 ? '' : 's'}; confidence ${(confidence * 100).toFixed(0)}%.`,
822
        evidenceReferences: [reference],
823
      });
824
      const weakDimensions = [
825
        ['completion', Number(current.completion)],
826
        ['output acceptance', Number(current.acceptance)],
827
        ['specification adherence', Number(current.specification)],
828
        ['deadline reliability', Number(current.deadline)],
829
        ['communication quality', Number(current.communication)],
830
        ['evidence quality', Number(current.evidence)],
831
        ['scope adherence', Number(current.scope)],
832
      ].filter(([, score]) => Number(score) < 0.7);
833
      if (weakDimensions.length)
834
        insights.push({
835
          code: 'historical_weaknesses',
836
          severity: weakDimensions.some(([, score]) => Number(score) < 0.5) ? 'high' : 'caution',
837
          message: `Observed weaker areas: ${weakDimensions
838
            .map(([name, score]) => `${String(name)} ${(Number(score) * 100).toFixed(0)}%`)
839
            .join(', ')}.`,
840
          evidenceReferences: [reference],
841
        });
842
      if (Number(current.disputes) > 0.25)
843
        insights.push({
844
          code: 'elevated_dispute_history',
845
          severity: Number(current.disputes) >= 0.5 ? 'high' : 'caution',
846
          message: `Eligible feedback shows a ${(Number(current.disputes) * 100).toFixed(0)}% dispute rate in this task category.`,
847
          evidenceReferences: [reference],
848
        });
849
    }
850
    if (counterparty.card.limitations.length)
851
      insights.push({
852
        code: 'declared_limitations',
853
        severity: 'info',
854
        message: `Counterparty declares: ${counterparty.card.limitations.join('; ')}`,
855
        evidenceReferences: [counterparty.card.cardUrl],
856
      });
857
    return insights;
858
  }
859
860
  async brokerLiveSession(interaction: FederatedInteraction) {
861
    await this.ensureSchema();
862
    const existing = await this.sql`
863
      SELECT status FROM openclasp_live_sessions
864
      WHERE interaction_id = ${interaction.interactionId}
865
    `;
866
    if (existing[0]?.status === 'active') return;
867
    const [initiator, responder] = await Promise.all([
868
      this.liveRuntime(interaction.initiatorAgentId),
869
      this.liveRuntime(interaction.responderAgentId),
870
    ]);
871
    const [initiatorInsights, responderInsights] = await Promise.all([
872
      this.privateCounterpartyInsights(
873
        initiator.operatorId,
874
        responder,
875
        interaction.contract.taskCategory,
876
      ),
877
      this.privateCounterpartyInsights(
878
        responder.operatorId,
879
        initiator,
880
        interaction.contract.taskCategory,
881
      ),
882
    ]);
883
    const issuedAt = new Date().toISOString();
884
    const makeOffer = (
885
      runtime: typeof initiator,
886
      counterparty: typeof responder,
887
      role: 'initiator' | 'responder',
888
      privateInsights: LiveSessionInsight[],
889
    ) =>
890
      LiveSessionOfferSchema.parse({
891
        type: 'openclasp.session.offer',
892
        version: '1',
893
        offerId: crypto.randomUUID(),
894
        interactionId: interaction.interactionId,
895
        agentId: runtime.agentId,
896
        role,
897
        counterparty: {
898
          agentId: counterparty.agentId,
899
          name: counterparty.card.name,
900
          agentVersion: counterparty.card.agentVersion,
901
          capabilities: counterparty.card.capabilities,
902
        },
903
        contract: interaction.contract,
904
        contractHash: interaction.termsHash,
905
        privateInsights,
906
        issuedAt,
907
        expiresAt: interaction.expiresAt,
908
      });
909
    const initiatorOffer = makeOffer(initiator, responder, 'initiator', initiatorInsights);
910
    const responderOffer = makeOffer(responder, initiator, 'responder', responderInsights);
911
    const [initiatorResponse, responderResponse] = await Promise.all([
912
      this.signedRuntimeRequest(initiator, initiatorOffer.offerId, initiatorOffer),
913
      this.signedRuntimeRequest(responder, responderOffer.offerId, responderOffer),
914
    ]);
915
    if (initiatorResponse.status < 200 || initiatorResponse.status >= 300)
916
      throw new Error(`Initiator runtime is not live (HTTP ${initiatorResponse.status})`);
917
    if (responderResponse.status < 200 || responderResponse.status >= 300)
918
      throw new Error(`Responder runtime is not live (HTTP ${responderResponse.status})`);
919
    const initiatorAcceptance = LiveSessionAcceptanceSchema.parse(initiatorResponse.body);
920
    const responderAcceptance = LiveSessionAcceptanceSchema.parse(responderResponse.body);
921
    if (
922
      initiatorAcceptance.offerId !== initiatorOffer.offerId ||
923
      initiatorAcceptance.agentId !== initiator.agentId ||
924
      responderAcceptance.offerId !== responderOffer.offerId ||
925
      responderAcceptance.agentId !== responder.agentId
926
    )
927
      throw new Error('Runtime returned a mismatched live-session acceptance');
928
    await Promise.all([
929
      resolvePublicRuntimeEndpoint(initiatorAcceptance.a2aEndpoint),
930
      resolvePublicRuntimeEndpoint(responderAcceptance.a2aEndpoint),
931
    ]);
932
    await this.sql`
933
      INSERT INTO openclasp_live_sessions(
934
        interaction_id, initiator_agent_id, responder_agent_id,
935
        initiator_session_id, responder_session_id,
936
        initiator_endpoint, responder_endpoint, status, expires_at
937
      ) VALUES (
938
        ${interaction.interactionId}, ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
939
        ${initiatorAcceptance.sessionId}, ${responderAcceptance.sessionId},
940
        ${initiatorAcceptance.a2aEndpoint}, ${responderAcceptance.a2aEndpoint},
941
        'preparing', ${interaction.expiresAt}
942
      )
943
      ON CONFLICT (interaction_id) DO UPDATE SET
944
        initiator_session_id = EXCLUDED.initiator_session_id,
945
        responder_session_id = EXCLUDED.responder_session_id,
946
        initiator_endpoint = EXCLUDED.initiator_endpoint,
947
        responder_endpoint = EXCLUDED.responder_endpoint,
948
        status = 'preparing', expires_at = EXCLUDED.expires_at, last_error = NULL
949
    `;
950
    const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
951
      /\/$/,
952
      '',
953
    );
954
    const activatedAt = new Date().toISOString();
955
    const verificationKey = getSessionVerificationKey(this.gatewaySecret());
956
    const makeActivation = (
957
      agentId: string,
958
      sessionId: string,
959
      role: 'initiator' | 'responder',
960
      peerAgentId: string,
961
      peerSessionId: string,
962
      peerEndpoint: string,
963
    ): LiveSessionActivation => {
964
      const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
965
      return LiveSessionActivationSchema.parse({
966
        type: 'openclasp.session.activation',
967
        version: '1',
968
        activationId: crypto.randomUUID(),
969
        interactionId: interaction.interactionId,
970
        agentId,
971
        sessionId,
972
        role,
973
        peer: {
974
          agentId: peerAgentId,
975
          sessionId: peerSessionId,
976
          endpoint: peerEndpoint,
977
          bearerToken,
978
          verificationKey,
979
        },
980
        reporting: {
981
          endpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/events`,
982
          bearerToken,
983
        },
984
        contractHash: interaction.termsHash,
985
        activatedAt,
986
        expiresAt: interaction.expiresAt,
987
      });
988
    };
989
    const initiatorActivation = makeActivation(
990
      initiator.agentId,
991
      initiatorAcceptance.sessionId,
992
      'initiator',
993
      responder.agentId,
994
      responderAcceptance.sessionId,
995
      responderAcceptance.a2aEndpoint,
996
    );
997
    const responderActivation = makeActivation(
998
      responder.agentId,
999
      responderAcceptance.sessionId,
1000
      'responder',
1001
      initiator.agentId,
1002
      initiatorAcceptance.sessionId,
1003
      initiatorAcceptance.a2aEndpoint,
1004
    );
1005
    try {
1006
      const responderActivated = await this.signedRuntimeRequest(
1007
        responder,
1008
        responderActivation.activationId,
1009
        responderActivation,
1010
      );
1011
      if (responderActivated.status < 200 || responderActivated.status >= 300)
1012
        throw new Error(`Responder activation failed with HTTP ${responderActivated.status}`);
1013
      const initiatorActivated = await this.signedRuntimeRequest(
1014
        initiator,
1015
        initiatorActivation.activationId,
1016
        initiatorActivation,
1017
      );
1018
      if (initiatorActivated.status < 200 || initiatorActivated.status >= 300)
1019
        throw new Error(`Initiator activation failed with HTTP ${initiatorActivated.status}`);
1020
      await this.sql`
1021
        UPDATE openclasp_live_sessions
1022
        SET status = 'active', activated_at = NOW(), last_error = NULL
1023
        WHERE interaction_id = ${interaction.interactionId}
1024
      `;
1025
      await this.sql`
1026
        UPDATE openclasp_agent_runtimes
1027
        SET last_seen_at = NOW(), last_error = NULL, updated_at = NOW()
1028
        WHERE agent_id IN (${initiator.agentId}, ${responder.agentId})
1029
      `;
1030
      await Promise.all([
1031
        this.touchAgentPresence(initiator.operatorId, initiator.agentId),
1032
        this.touchAgentPresence(responder.operatorId, responder.agentId),
1033
      ]);
1034
    } catch (error) {
1035
      const reason =
1036
        error instanceof Error ? error.message.slice(0, 500) : 'Session activation failed';
1037
      await this.sql`
1038
        UPDATE openclasp_live_sessions SET status = 'failed', last_error = ${reason}
1039
        WHERE interaction_id = ${interaction.interactionId}
1040
      `;
1041
      throw error;
1042
    }
1043
  }
1044
1045
  async getLiveSession(operatorId: string, interactionId: string, agentId: string) {
1046
    await this.ensureSchema();
1047
    const rows = await this.sql`
1048
      SELECT session.*, interaction.payload,
1049
        initiator.operator_id AS initiator_operator_id,
1050
        responder.operator_id AS responder_operator_id
1051
      FROM openclasp_live_sessions session
1052
      INNER JOIN openclasp_federated_interactions interaction
1053
        ON interaction.interaction_id = session.interaction_id
1054
      INNER JOIN openclasp_agent_runtimes initiator
1055
        ON initiator.agent_id = session.initiator_agent_id
1056
      INNER JOIN openclasp_agent_runtimes responder
1057
        ON responder.agent_id = session.responder_agent_id
1058
      WHERE session.interaction_id = ${interactionId} AND session.status = 'active'
1059
      LIMIT 1
1060
    `;
1061
    const row = rows[0];
1062
    if (!row) throw new Error('Active live session not found');
1063
    const isInitiator = row.initiator_agent_id === agentId;
1064
    const isResponder = row.responder_agent_id === agentId;
1065
    if (!isInitiator && !isResponder) throw new Error('Agent is not a session participant');
1066
    const owner = isInitiator ? row.initiator_operator_id : row.responder_operator_id;
1067
    if (owner !== operatorId) throw new Error('Agent is not owned by this account');
1068
    const interaction = FederatedInteractionSchema.parse(row.payload);
1069
    const peerAgentId = String(isInitiator ? row.responder_agent_id : row.initiator_agent_id);
1070
    const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
1071
    return {
1072
      type: 'openclasp.session.activation' as const,
1073
      version: '1' as const,
1074
      activationId: crypto.randomUUID(),
1075
      interactionId,
1076
      agentId,
1077
      sessionId: String(isInitiator ? row.initiator_session_id : row.responder_session_id),
1078
      role: isInitiator ? ('initiator' as const) : ('responder' as const),
1079
      peer: {
1080
        agentId: peerAgentId,
1081
        sessionId: String(isInitiator ? row.responder_session_id : row.initiator_session_id),
1082
        endpoint: String(isInitiator ? row.responder_endpoint : row.initiator_endpoint),
1083
        bearerToken,
1084
        verificationKey: getSessionVerificationKey(this.gatewaySecret()),
1085
      },
1086
      reporting: {
1087
        endpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/events`,
1088
        bearerToken,
1089
      },
1090
      contractHash: interaction.termsHash,
1091
      activatedAt: row.activated_at
1092
        ? new Date(String(row.activated_at)).toISOString()
1093
        : new Date().toISOString(),
1094
      expiresAt: new Date(String(row.expires_at)).toISOString(),
1095
    };
1096
  }
1097
1098
  async recordLiveSessionEvent(token: string, value: LiveSessionEvent) {
1099
    await this.ensureSchema();
1100
    const event = LiveSessionEventSchema.parse(value);
1101
    const grant = verifySessionGrant(this.gatewaySecret(), token);
1102
    if (grant.interactionId !== event.interactionId || grant.senderAgentId !== event.agentId)
1103
      throw new Error('Session credential does not match the event');
1104
    const sessions = await this.sql`
1105
      SELECT 1 FROM openclasp_live_sessions
1106
      WHERE interaction_id = ${event.interactionId}
1107
        AND status = 'active'
1108
        AND ${event.agentId} IN (initiator_agent_id, responder_agent_id)
1109
    `;
1110
    if (!sessions.length) throw new Error('Active live session not found');
1111
    const attestation = attestSessionRecord(this.gatewaySecret(), event);
1112
    const encoded = JSON.stringify({ ...event, attestation });
1113
    const rows = await this.sql`
1114
      INSERT INTO openclasp_live_session_events(
1115
        interaction_id, event_id, agent_id, sequence, event
1116
      ) SELECT ${event.interactionId}, ${event.eventId}, ${event.agentId}, ${event.sequence}, ${encoded}::jsonb
1117
      WHERE (
1118
        SELECT COUNT(*) FROM openclasp_live_session_events
1119
        WHERE interaction_id = ${event.interactionId}
1120
      ) < 1000
1121
      ON CONFLICT DO NOTHING
1122
      RETURNING event_id
1123
    `;
1124
    if (event.type === 'session_completed' || event.type === 'session_failed') {
1125
      const terminal = await this.sql`
1126
        SELECT COUNT(DISTINCT agent_id) AS count
1127
        FROM openclasp_live_session_events
1128
        WHERE interaction_id = ${event.interactionId}
1129
          AND event->>'type' IN ('session_completed', 'session_failed')
1130
      `;
1131
      if (Number(terminal[0]?.count ?? 0) >= 2) {
1132
        await this.sql`
1133
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
1134
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
1135
        `;
1136
        const completedAt = new Date().toISOString();
1137
        await this.sql`
1138
          UPDATE openclasp_federated_interactions
1139
          SET status = 'completed',
1140
            payload = jsonb_set(
1141
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
1142
              '{updatedAt}', to_jsonb(${completedAt}::text)
1143
            ),
1144
            updated_at = NOW()
1145
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
1146
        `;
1147
      }
1148
    }
1149
    return {
1150
      recorded: rows.length > 0,
1151
      deduplicated: rows.length === 0,
1152
      eventId: event.eventId,
1153
      attestation,
1154
    };
1155
  }
1156
1157
  async createFederatedInteraction(
1158
    operatorId: string,
1159
    value: FederatedInteraction,
1160
  ): Promise<FederatedInteraction> {
1161
    await this.ensureSchema();
1162
    const interaction = FederatedInteractionSchema.parse(value);
1163
    if (interaction.status !== 'pending') throw new Error('New interactions must be pending');
1164
    if (interaction.contract.interactionId !== interaction.interactionId)
1165
      throw new Error('Contract interaction ID does not match');
1166
    if (canonicalHash(interaction.contract) !== interaction.termsHash)
1167
      throw new Error('Contract hash does not match the immutable terms');
1168
    if (
1169
      interaction.contract.parties.length !== 2 ||
1170
      interaction.contract.parties[0] !== interaction.initiatorAgentId ||
1171
      interaction.contract.parties[1] !== interaction.responderAgentId
1172
    )
1173
      throw new Error('Contract parties do not match the interaction participants');
1174
    const acceptanceEntries = Object.entries(interaction.acceptances);
1175
    if (
1176
      acceptanceEntries.length !== 1 ||
1177
      acceptanceEntries[0]?.[0] !== interaction.initiatorAgentId ||
1178
      acceptanceEntries[0]?.[1].agentId !== interaction.initiatorAgentId ||
1179
      acceptanceEntries[0]?.[1].termsHash !== interaction.termsHash ||
1180
      !['oauth_installation', 'oauth_account'].includes(acceptanceEntries[0]?.[1].method ?? '')
1181
    )
1182
      throw new Error('A pending interaction requires only the initiator acceptance');
1183
    if (Date.parse(interaction.expiresAt) <= Date.now())
1184
      throw new Error('Interaction expiry must be in the future');
1185
    const owners = await this.sql`
1186
      SELECT agent_id, operator_id, card FROM openclasp_public_agents
1187
      WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
1188
    `;
1189
    const initiator = owners.find((row) => row.agent_id === interaction.initiatorAgentId);
1190
    const responder = owners.find((row) => row.agent_id === interaction.responderAgentId);
1191
    if (!initiator || initiator.operator_id !== operatorId)
1192
      throw new Error('The initiating agent must be published and owned by this account');
1193
    if (!responder) throw new Error('Responder is not a published OpenClasp agent');
1194
    if (responder.operator_id === operatorId)
1195
      throw new Error('Federated interactions require agents from different accounts');
1196
    const responderCard = normalizePublicAgentCard(responder.card);
1197
    if (
1198
      !responderCard.transports.some(
1199
        (transport) =>
1200
          transport.endpoint === interaction.responderTransport.endpoint &&
1201
          transport.protocolBinding === interaction.responderTransport.protocolBinding,
1202
      )
1203
    )
1204
      throw new Error('Responder transport does not match its published Agent Card');
1205
    const encoded = JSON.stringify(interaction);
1206
    const rows = await this.sql`
1207
      INSERT INTO openclasp_federated_interactions(
1208
        interaction_id, initiator_operator_id, responder_operator_id,
1209
        initiator_agent_id, responder_agent_id, status, payload, expires_at
1210
      ) VALUES (
1211
        ${interaction.interactionId}, ${operatorId}, ${responder.operator_id},
1212
        ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
1213
        ${interaction.status}, ${encoded}::jsonb, ${interaction.expiresAt}
1214
      )
1215
      RETURNING payload
1216
    `;
1217
    const stored = FederatedInteractionSchema.parse(rows[0]?.payload);
1218
    const profiles = await this.sql`
1219
      SELECT payload FROM openclasp_records
1220
      WHERE operator_id = ${responder.operator_id}
1221
        AND kind = 'agent_profile'
1222
        AND record_id = ${interaction.responderAgentId}
1223
      LIMIT 1
1224
    `;
1225
    const responderProfile = profiles[0]?.payload as AgentProfile | undefined;
1226
    if (responderProfile && canAutoAcceptInteraction(responderProfile, stored))
1227
      return this.respondToFederatedInteraction(
1228
        String(responder.operator_id),
1229
        stored.interactionId,
1230
        stored.responderAgentId,
1231
        'accept',
1232
        'policy_auto_accept',
1233
      );
1234
    return stored;
1235
  }
1236
1237
  async listFederatedInteractions(operatorId: string): Promise<FederatedInteraction[]> {
1238
    await this.ensureSchema();
1239
    await this.sql`
1240
      UPDATE openclasp_federated_interactions
1241
      SET status = 'expired', payload = jsonb_set(payload, '{status}', '"expired"'::jsonb), updated_at = NOW()
1242
      WHERE status = 'pending' AND expires_at <= NOW()
1243
    `;
1244
    const rows = await this.sql`
1245
      SELECT payload FROM openclasp_federated_interactions
1246
      WHERE initiator_operator_id = ${operatorId} OR responder_operator_id = ${operatorId}
1247
      ORDER BY updated_at DESC
1248
    `;
1249
    return rows.map((row) => FederatedInteractionSchema.parse(row.payload));
1250
  }
1251
1252
  async getFederatedInteraction(
1253
    operatorId: string,
1254
    interactionId: string,
1255
  ): Promise<FederatedInteraction | undefined> {
1256
    const values = await this.listFederatedInteractions(operatorId);
1257
    return values.find((value) => value.interactionId === interactionId);
1258
  }
1259
1260
  async respondToFederatedInteraction(
1261
    operatorId: string,
1262
    interactionId: string,
1263
    agentId: string,
1264
    decision: 'accept' | 'reject',
1265
    method: 'oauth_installation' | 'oauth_account' | 'policy_auto_accept' = 'oauth_account',
1266
  ): Promise<FederatedInteraction> {
1267
    await this.ensureSchema();
1268
    const rows = await this.sql`
1269
      SELECT payload, responder_operator_id, responder_agent_id
1270
      FROM openclasp_federated_interactions WHERE interaction_id = ${interactionId}
1271
    `;
1272
    const row = rows[0];
1273
    if (!row) throw new Error('Interaction invitation not found');
1274
    if (row.responder_operator_id !== operatorId || row.responder_agent_id !== agentId)
1275
      throw new Error('Only the invited agent may respond');
1276
    const current = FederatedInteractionSchema.parse(row.payload);
1277
    if (current.status !== 'pending') throw new Error('Invitation is no longer pending');
1278
    const now = new Date().toISOString();
1279
    if (Date.parse(current.expiresAt) <= Date.now()) {
1280
      const expired = { ...current, status: 'expired' as const, updatedAt: now };
1281
      await this.updateFederatedInteraction(interactionId, current.status, expired);
1282
      throw new Error('Invitation has expired');
1283
    }
1284
    const next: FederatedInteraction =
1285
      decision === 'reject'
1286
        ? { ...current, status: 'rejected', updatedAt: now }
1287
        : {
1288
            ...current,
1289
            status: 'active',
1290
            updatedAt: now,
1291
            acceptances: {
1292
              ...current.acceptances,
1293
              [agentId]: {
1294
                agentId,
1295
                method,
1296
                termsHash: current.termsHash,
1297
                acceptedAt: now,
1298
              },
1299
            },
1300
          };
1301
    if (decision === 'accept') {
1302
      try {
1303
        await this.brokerLiveSession(next);
1304
      } catch (error) {
1305
        const reason = error instanceof Error ? error.message.slice(0, 500) : 'Live session failed';
1306
        await this.sql`
1307
          UPDATE openclasp_agent_runtimes SET last_error = ${reason}, updated_at = NOW()
1308
          WHERE agent_id IN (${current.initiatorAgentId}, ${current.responderAgentId})
1309
        `;
1310
        throw error;
1311
      }
1312
    }
1313
    const updated = await this.updateFederatedInteraction(interactionId, current.status, next);
1314
    if (!updated) throw new Error('Invitation was already handled');
1315
    return next;
1316
  }
1317
1318
  private async updateFederatedInteraction(
1319
    interactionId: string,
1320
    expectedStatus: string,
1321
    value: FederatedInteraction,
1322
  ): Promise<boolean> {
1323
    const encoded = JSON.stringify(FederatedInteractionSchema.parse(value));
1324
    const rows = await this.sql`
1325
      UPDATE openclasp_federated_interactions
1326
      SET status = ${value.status}, payload = ${encoded}::jsonb, updated_at = NOW()
1327
      WHERE interaction_id = ${interactionId} AND status = ${expectedStatus}
1328
      RETURNING interaction_id
1329
    `;
1330
    return rows.length > 0;
1331
  }
1332
1333
  async getSettings(operatorId: string): Promise<AccountSettings> {
1334
    await this.ensureSchema();
1335
    const rows = await this.sql`
1336
      SELECT display_name, contribution_enabled, retention_days, evidence_sharing
1337
      FROM openclasp_account_settings
1338
      WHERE operator_id = ${operatorId}
1339
    `;
1340
    const row = rows[0] as
1341
      | {
1342
          display_name: string;
1343
          contribution_enabled: boolean;
1344
          retention_days: number;
1345
          evidence_sharing: AccountSettings['evidenceSharing'];
1346
        }
1347
      | undefined;
1348
    return row
1349
      ? {
1350
          displayName: row.display_name,
1351
          contributionEnabled: row.contribution_enabled,
1352
          retentionDays: row.retention_days,
1353
          evidenceSharing: row.evidence_sharing,
1354
          rawConversationsStored: false,
1355
        }
1356
      : defaults;
1357
  }
1358
1359
  async saveSettings(
1360
    operatorId: string,
1361
    settings: Omit<AccountSettings, 'rawConversationsStored'>,
1362
  ): Promise<AccountSettings> {
1363
    await this.ensureSchema();
1364
    await this.sql`
1365
      INSERT INTO openclasp_account_settings(
1366
        operator_id, display_name, contribution_enabled, retention_days, evidence_sharing
1367
      ) VALUES (
1368
        ${operatorId}, ${settings.displayName}, ${settings.contributionEnabled},
1369
        ${settings.retentionDays}, ${settings.evidenceSharing}
1370
      )
1371
      ON CONFLICT (operator_id) DO UPDATE SET
1372
        display_name = EXCLUDED.display_name,
1373
        contribution_enabled = EXCLUDED.contribution_enabled,
1374
        retention_days = EXCLUDED.retention_days,
1375
        evidence_sharing = EXCLUDED.evidence_sharing,
1376
        updated_at = NOW()
1377
    `;
1378
    return { ...settings, rawConversationsStored: false };
1379
  }
1380
}