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

Viewing files at commit c099f79dc606Commit detailsView latest

packages/persistence/src/hosted-migrations.ts 562 lines · 20.7 KB · typescript
1
import type { NeonQueryFunction } from '@neondatabase/serverless';
2
3
type Sql = NeonQueryFunction<false, false>;
4
5
export const HOSTED_MIGRATIONS = [
6
  { version: 1, name: 'hosted_baseline' },
7
  { version: 2, name: 'append_only_source_records' },
8
  { version: 3, name: 'remove_hosted_conversations' },
9
  { version: 4, name: 'adaptive_assurance_probes' },
10
  { version: 5, name: 'assurance_decision_learning' },
11
  { version: 6, name: 'connector_claims' },
12
  { version: 7, name: 'provider_connections' },
13
] as const;
14
15
async function applyBaseline(sql: Sql) {
16
  await sql`
17
    CREATE TABLE IF NOT EXISTS openclasp_records (
18
      operator_id TEXT NOT NULL,
19
      kind TEXT NOT NULL,
20
      record_id TEXT NOT NULL,
21
      payload JSONB NOT NULL,
22
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
23
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
24
      PRIMARY KEY (operator_id, kind, record_id)
25
    )
26
  `;
27
  await sql`
28
    CREATE INDEX IF NOT EXISTS openclasp_records_operator_created
29
    ON openclasp_records(operator_id, created_at DESC)
30
  `;
31
  await sql`
32
    CREATE TABLE IF NOT EXISTS openclasp_account_settings (
33
      operator_id TEXT PRIMARY KEY,
34
      display_name TEXT NOT NULL DEFAULT '',
35
      contribution_enabled BOOLEAN NOT NULL DEFAULT FALSE,
36
      retention_days INTEGER NOT NULL DEFAULT 30 CHECK (retention_days BETWEEN 0 AND 3650),
37
      evidence_sharing TEXT NOT NULL DEFAULT 'ask'
38
        CHECK (evidence_sharing IN ('never', 'ask', 'contract_only')),
39
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
40
    )
41
  `;
42
  await sql`
43
    CREATE TABLE IF NOT EXISTS openclasp_public_agents (
44
      agent_id TEXT PRIMARY KEY,
45
      operator_id TEXT NOT NULL,
46
      card JSONB NOT NULL,
47
      published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
48
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
49
    )
50
  `;
51
  await sql`
52
    CREATE INDEX IF NOT EXISTS openclasp_public_agents_updated
53
    ON openclasp_public_agents(updated_at DESC)
54
  `;
55
  await sql`
56
    CREATE TABLE IF NOT EXISTS openclasp_federated_interactions (
57
      interaction_id TEXT PRIMARY KEY,
58
      initiator_operator_id TEXT NOT NULL,
59
      responder_operator_id TEXT NOT NULL,
60
      initiator_agent_id TEXT NOT NULL,
61
      responder_agent_id TEXT NOT NULL,
62
      status TEXT NOT NULL,
63
      payload JSONB NOT NULL,
64
      expires_at TIMESTAMPTZ NOT NULL,
65
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
66
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
67
    )
68
  `;
69
  await sql`
70
    CREATE INDEX IF NOT EXISTS openclasp_federated_interactions_participants
71
    ON openclasp_federated_interactions(
72
      initiator_operator_id,
73
      responder_operator_id,
74
      updated_at DESC
75
    )
76
  `;
77
  await sql`
78
    CREATE TABLE IF NOT EXISTS openclasp_agent_runtimes (
79
      agent_id TEXT PRIMARY KEY,
80
      operator_id TEXT NOT NULL,
81
      endpoint TEXT NOT NULL,
82
      secret_ciphertext TEXT NOT NULL,
83
      secret_iv TEXT NOT NULL,
84
      secret_auth_tag TEXT NOT NULL,
85
      status TEXT NOT NULL CHECK (status IN ('verified', 'disabled')),
86
      verified_at TIMESTAMPTZ NOT NULL,
87
      last_delivery_at TIMESTAMPTZ,
88
      last_error TEXT,
89
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
90
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
91
    )
92
  `;
93
  await sql`
94
    CREATE TABLE IF NOT EXISTS openclasp_agent_access_tokens (
95
      token_id TEXT PRIMARY KEY,
96
      operator_id TEXT NOT NULL,
97
      agent_id TEXT NOT NULL,
98
      name TEXT NOT NULL,
99
      token_hash TEXT NOT NULL,
100
      scopes JSONB NOT NULL,
101
      expires_at TIMESTAMPTZ NOT NULL,
102
      last_used_at TIMESTAMPTZ,
103
      revoked_at TIMESTAMPTZ,
104
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
105
    )
106
  `;
107
  await sql`
108
    CREATE INDEX IF NOT EXISTS openclasp_agent_access_tokens_owner_agent
109
    ON openclasp_agent_access_tokens(operator_id, agent_id, created_at DESC)
110
  `;
111
  await sql`
112
    ALTER TABLE openclasp_agent_runtimes
113
    ADD COLUMN IF NOT EXISTS a2a_endpoint TEXT,
114
    ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ
115
  `;
116
  await sql`
117
    CREATE TABLE IF NOT EXISTS openclasp_live_sessions (
118
      interaction_id TEXT PRIMARY KEY,
119
      initiator_agent_id TEXT NOT NULL,
120
      responder_agent_id TEXT NOT NULL,
121
      initiator_session_id TEXT NOT NULL,
122
      responder_session_id TEXT NOT NULL,
123
      initiator_endpoint TEXT NOT NULL,
124
      responder_endpoint TEXT NOT NULL,
125
      status TEXT NOT NULL CHECK (status IN ('preparing', 'active', 'completed', 'failed')),
126
      expires_at TIMESTAMPTZ NOT NULL,
127
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
128
      activated_at TIMESTAMPTZ,
129
      completed_at TIMESTAMPTZ,
130
      last_error TEXT
131
    )
132
  `;
133
  await sql`
134
    CREATE TABLE IF NOT EXISTS openclasp_live_session_events (
135
      interaction_id TEXT NOT NULL,
136
      event_id TEXT NOT NULL,
137
      agent_id TEXT NOT NULL,
138
      sequence INTEGER NOT NULL,
139
      event JSONB NOT NULL,
140
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
141
      PRIMARY KEY (interaction_id, event_id),
142
      UNIQUE (interaction_id, agent_id, sequence)
143
    )
144
  `;
145
  await sql`
146
    CREATE TABLE IF NOT EXISTS openclasp_hosted_threads (
147
      thread_id TEXT PRIMARY KEY,
148
      interaction_id TEXT NOT NULL UNIQUE,
149
      participant_a_agent_id TEXT NOT NULL,
150
      participant_b_agent_id TEXT NOT NULL,
151
      status TEXT NOT NULL CHECK (status IN ('open', 'closed')) DEFAULT 'open',
152
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
153
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
154
      expires_at TIMESTAMPTZ NOT NULL
155
    )
156
  `;
157
  await sql`
158
    CREATE INDEX IF NOT EXISTS openclasp_hosted_threads_participants
159
    ON openclasp_hosted_threads(
160
      participant_a_agent_id,
161
      participant_b_agent_id,
162
      updated_at DESC
163
    )
164
  `;
165
  await sql`
166
    CREATE TABLE IF NOT EXISTS openclasp_hosted_messages (
167
      message_id TEXT PRIMARY KEY,
168
      thread_id TEXT NOT NULL REFERENCES openclasp_hosted_threads(thread_id) ON DELETE CASCADE,
169
      interaction_id TEXT NOT NULL,
170
      sender_agent_id TEXT NOT NULL,
171
      recipient_agent_id TEXT NOT NULL,
172
      request_key TEXT NOT NULL,
173
      content_ciphertext TEXT NOT NULL,
174
      content_iv TEXT NOT NULL,
175
      content_auth_tag TEXT NOT NULL,
176
      content_hash TEXT NOT NULL,
177
      delivery TEXT NOT NULL CHECK (delivery IN ('accepted', 'delivered', 'read')),
178
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
179
      read_at TIMESTAMPTZ
180
    )
181
  `;
182
  await sql`
183
    CREATE UNIQUE INDEX IF NOT EXISTS openclasp_hosted_messages_dedup
184
    ON openclasp_hosted_messages(thread_id, sender_agent_id, request_key)
185
  `;
186
  await sql`
187
    CREATE INDEX IF NOT EXISTS openclasp_hosted_messages_thread_created
188
    ON openclasp_hosted_messages(thread_id, created_at ASC)
189
  `;
190
}
191
192
async function applyAppendOnlySourceRecords(sql: Sql) {
193
  await sql`
194
    CREATE TABLE IF NOT EXISTS openclasp_source_records (
195
      event_id UUID PRIMARY KEY,
196
      operator_id TEXT NOT NULL,
197
      kind TEXT NOT NULL,
198
      record_id TEXT NOT NULL,
199
      schema_name TEXT NOT NULL,
200
      schema_version TEXT NOT NULL,
201
      payload JSONB NOT NULL,
202
      payload_digest TEXT NOT NULL,
203
      entity_refs JSONB NOT NULL DEFAULT '{}'::jsonb,
204
      provenance TEXT NOT NULL,
205
      visibility TEXT NOT NULL,
206
      retention_class TEXT NOT NULL,
207
      learning_scope TEXT NOT NULL,
208
      reported_at TIMESTAMPTZ NOT NULL,
209
      ingested_at TIMESTAMPTZ NOT NULL,
210
      UNIQUE (operator_id, kind, record_id, payload_digest),
211
      CHECK (provenance IN (
212
        'self_declared',
213
        'operator_attested',
214
        'cryptographically_verified',
215
        'domain_verified',
216
        'third_party_verified',
217
        'observed',
218
        'disputed'
219
      )),
220
      CHECK (visibility IN (
221
        'local_only',
222
        'private_requester',
223
        'private_responder',
224
        'shared_participants',
225
        'network_aggregate'
226
      )),
227
      CHECK (retention_class IN ('account', 'audit', 'operational', 'temporary')),
228
      CHECK (learning_scope IN (
229
        'not_evaluated',
230
        'excluded',
231
        'local_only',
232
        'network_aggregate'
233
      ))
234
    )
235
  `;
236
  await sql`
237
    CREATE INDEX IF NOT EXISTS openclasp_source_records_operator_ingested
238
    ON openclasp_source_records(operator_id, ingested_at DESC)
239
  `;
240
  await sql`
241
    CREATE INDEX IF NOT EXISTS openclasp_source_records_history
242
    ON openclasp_source_records(operator_id, kind, record_id, ingested_at ASC)
243
  `;
244
  await sql`
245
    CREATE INDEX IF NOT EXISTS openclasp_source_records_entities
246
    ON openclasp_source_records USING GIN(entity_refs)
247
  `;
248
}
249
250
async function removeHostedConversations(sql: Sql) {
251
  await sql`DROP TABLE IF EXISTS openclasp_hosted_messages`;
252
  await sql`DROP TABLE IF EXISTS openclasp_hosted_threads`;
253
}
254
255
async function addAdaptiveAssuranceProbes(sql: Sql) {
256
  await sql`
257
    CREATE TABLE IF NOT EXISTS openclasp_ai_generations (
258
      generation_id UUID PRIMARY KEY,
259
      operator_id TEXT NOT NULL,
260
      interaction_id TEXT NOT NULL,
261
      phase TEXT NOT NULL CHECK (phase IN ('pre_task', 'post_task')),
262
      model TEXT NOT NULL,
263
      prompt_version TEXT NOT NULL,
264
      status TEXT NOT NULL CHECK (status IN ('pending', 'complete', 'fallback', 'error')),
265
      input JSONB NOT NULL,
266
      input_digest TEXT NOT NULL,
267
      output JSONB,
268
      token_usage JSONB,
269
      error_code TEXT,
270
      started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
271
      completed_at TIMESTAMPTZ
272
    )
273
  `;
274
  await sql`
275
    CREATE INDEX IF NOT EXISTS openclasp_ai_generations_interaction
276
    ON openclasp_ai_generations(operator_id, interaction_id, started_at DESC)
277
  `;
278
  await sql`
279
    CREATE TABLE IF NOT EXISTS openclasp_assurance_probe_plans (
280
      plan_id UUID PRIMARY KEY,
281
      generation_id UUID NOT NULL REFERENCES openclasp_ai_generations(generation_id),
282
      operator_id TEXT NOT NULL,
283
      interaction_id TEXT NOT NULL,
284
      contract_hash TEXT NOT NULL,
285
      phase TEXT NOT NULL CHECK (phase IN ('pre_task', 'post_task')),
286
      generated_for_agent_id TEXT NOT NULL,
287
      target_agent_id TEXT NOT NULL,
288
      payload JSONB NOT NULL,
289
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
290
      expires_at TIMESTAMPTZ NOT NULL
291
    )
292
  `;
293
  await sql`
294
    CREATE INDEX IF NOT EXISTS openclasp_assurance_probe_plans_interaction
295
    ON openclasp_assurance_probe_plans(interaction_id, phase, created_at DESC)
296
  `;
297
  await sql`
298
    CREATE TABLE IF NOT EXISTS openclasp_assurance_probe_responses (
299
      response_id UUID PRIMARY KEY,
300
      plan_id UUID NOT NULL REFERENCES openclasp_assurance_probe_plans(plan_id),
301
      operator_id TEXT NOT NULL,
302
      interaction_id TEXT NOT NULL,
303
      contract_hash TEXT NOT NULL,
304
      phase TEXT NOT NULL CHECK (phase IN ('pre_task', 'post_task')),
305
      agent_id TEXT NOT NULL,
306
      payload JSONB NOT NULL,
307
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
308
      UNIQUE(plan_id, agent_id)
309
    )
310
  `;
311
  await sql`
312
    CREATE INDEX IF NOT EXISTS openclasp_assurance_probe_responses_interaction
313
    ON openclasp_assurance_probe_responses(interaction_id, agent_id, created_at DESC)
314
  `;
315
  await sql`
316
    CREATE TABLE IF NOT EXISTS openclasp_assurance_claim_comparisons (
317
      comparison_id UUID PRIMARY KEY,
318
      operator_id TEXT NOT NULL,
319
      interaction_id TEXT NOT NULL,
320
      target_agent_id TEXT NOT NULL,
321
      pre_task_response_id UUID NOT NULL REFERENCES openclasp_assurance_probe_responses(response_id),
322
      post_task_response_id UUID REFERENCES openclasp_assurance_probe_responses(response_id),
323
      payload JSONB NOT NULL,
324
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
325
      UNIQUE(operator_id, interaction_id, target_agent_id, pre_task_response_id)
326
    )
327
  `;
328
  await sql`
329
    CREATE INDEX IF NOT EXISTS openclasp_assurance_claim_comparisons_interaction
330
    ON openclasp_assurance_claim_comparisons(operator_id, interaction_id, created_at DESC)
331
  `;
332
}
333
334
async function addAssuranceDecisionLearning(sql: Sql) {
335
  await sql`
336
    CREATE TABLE IF NOT EXISTS openclasp_assurance_assessments (
337
      assessment_id UUID PRIMARY KEY,
338
      generation_id UUID NOT NULL REFERENCES openclasp_ai_generations(generation_id),
339
      operator_id TEXT NOT NULL,
340
      interaction_id TEXT NOT NULL,
341
      phase TEXT NOT NULL CHECK (phase IN ('pre_task', 'post_task')),
342
      round INTEGER NOT NULL CHECK (round BETWEEN 1 AND 3),
343
      target_agent_id TEXT NOT NULL,
344
      target_agent_version TEXT NOT NULL,
345
      payload JSONB NOT NULL,
346
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
347
      UNIQUE(operator_id, interaction_id, phase, round, target_agent_id)
348
    )
349
  `;
350
  await sql`
351
    CREATE INDEX IF NOT EXISTS openclasp_assurance_assessments_interaction
352
    ON openclasp_assurance_assessments(operator_id, interaction_id, created_at DESC)
353
  `;
354
  await sql`
355
    CREATE TABLE IF NOT EXISTS openclasp_assurance_predictions (
356
      prediction_id UUID PRIMARY KEY,
357
      assessment_id UUID REFERENCES openclasp_assurance_assessments(assessment_id),
358
      operator_id TEXT NOT NULL,
359
      interaction_id TEXT NOT NULL,
360
      target_agent_id TEXT NOT NULL,
361
      target_agent_version TEXT NOT NULL,
362
      task_category TEXT NOT NULL,
363
      stage TEXT NOT NULL CHECK (stage IN ('baseline', 'after_probe', 'after_safeguard', 'final')),
364
      success_probability DOUBLE PRECISION NOT NULL CHECK (success_probability BETWEEN 0.05 AND 0.95),
365
      payload JSONB NOT NULL,
366
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
367
    )
368
  `;
369
  await sql`
370
    CREATE INDEX IF NOT EXISTS openclasp_assurance_predictions_context
371
    ON openclasp_assurance_predictions(operator_id, target_agent_id, target_agent_version, task_category, created_at DESC)
372
  `;
373
  await sql`
374
    CREATE TABLE IF NOT EXISTS openclasp_assurance_safeguards (
375
      safeguard_id UUID PRIMARY KEY,
376
      assessment_id UUID NOT NULL REFERENCES openclasp_assurance_assessments(assessment_id),
377
      operator_id TEXT NOT NULL,
378
      interaction_id TEXT NOT NULL,
379
      target_agent_id TEXT NOT NULL,
380
      status TEXT NOT NULL CHECK (status IN ('recommended', 'accepted', 'rejected', 'modified')),
381
      payload JSONB NOT NULL,
382
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
383
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
384
    )
385
  `;
386
  await sql`
387
    CREATE INDEX IF NOT EXISTS openclasp_assurance_safeguards_interaction
388
    ON openclasp_assurance_safeguards(operator_id, interaction_id, created_at DESC)
389
  `;
390
  await sql`
391
    CREATE TABLE IF NOT EXISTS openclasp_assurance_evaluations (
392
      evaluation_id UUID PRIMARY KEY,
393
      operator_id TEXT NOT NULL,
394
      interaction_id TEXT NOT NULL,
395
      target_agent_id TEXT NOT NULL,
396
      target_agent_version TEXT NOT NULL,
397
      task_category TEXT NOT NULL,
398
      completion_report_id UUID NOT NULL,
399
      payload JSONB NOT NULL,
400
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
401
      UNIQUE(operator_id, completion_report_id)
402
    )
403
  `;
404
  await sql`
405
    CREATE INDEX IF NOT EXISTS openclasp_assurance_evaluations_learning
406
    ON openclasp_assurance_evaluations(operator_id, target_agent_id, target_agent_version, task_category, created_at DESC)
407
  `;
408
}
409
410
async function addConnectorClaims(sql: Sql) {
411
  await sql`
412
    CREATE TABLE IF NOT EXISTS openclasp_connector_claims (
413
      claim_id UUID PRIMARY KEY,
414
      secret_hash TEXT NOT NULL,
415
      runtime_endpoint TEXT NOT NULL,
416
      credential_public_key TEXT NOT NULL,
417
      profile JSONB NOT NULL,
418
      status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected', 'connected', 'expired')),
419
      operator_id TEXT,
420
      agent_id TEXT,
421
      credential_ciphertext TEXT,
422
      expires_at TIMESTAMPTZ NOT NULL,
423
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
424
      decided_at TIMESTAMPTZ,
425
      connected_at TIMESTAMPTZ
426
    )
427
  `;
428
  await sql`
429
    CREATE INDEX IF NOT EXISTS openclasp_connector_claims_expiry
430
    ON openclasp_connector_claims(status, expires_at)
431
  `;
432
  await sql`
433
    CREATE INDEX IF NOT EXISTS openclasp_connector_claims_owner
434
    ON openclasp_connector_claims(operator_id, created_at DESC)
435
  `;
436
}
437
438
async function addProviderConnections(sql: Sql) {
439
  await sql`
440
    CREATE TABLE IF NOT EXISTS openclasp_provider_connections (
441
      connection_id UUID PRIMARY KEY,
442
      operator_id TEXT NOT NULL,
443
      provider TEXT NOT NULL CHECK (provider IN ('botpress')),
444
      agent_name TEXT NOT NULL,
445
      code_hash TEXT NOT NULL UNIQUE,
446
      status TEXT NOT NULL CHECK (status IN ('pending', 'connected', 'expired')),
447
      agent_id TEXT,
448
      runtime_endpoint TEXT,
449
      credential_public_key TEXT,
450
      credential_ciphertext TEXT,
451
      expires_at TIMESTAMPTZ NOT NULL,
452
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
453
      connected_at TIMESTAMPTZ
454
    )
455
  `;
456
  await sql`
457
    CREATE INDEX IF NOT EXISTS openclasp_provider_connections_owner
458
    ON openclasp_provider_connections(operator_id, created_at DESC)
459
  `;
460
  await sql`
461
    CREATE INDEX IF NOT EXISTS openclasp_provider_connections_expiry
462
    ON openclasp_provider_connections(status, expires_at)
463
  `;
464
}
465
466
export async function runHostedMigrations(sql: Sql): Promise<void> {
467
  await sql`
468
    CREATE TABLE IF NOT EXISTS openclasp_schema_migrations (
469
      version INTEGER PRIMARY KEY,
470
      name TEXT NOT NULL UNIQUE,
471
      applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
472
    )
473
  `;
474
  const appliedRows = await sql`
475
    SELECT version, name FROM openclasp_schema_migrations ORDER BY version ASC
476
  `;
477
  const applied = new Map(appliedRows.map((row) => [Number(row.version), String(row.name)]));
478
  for (const migration of HOSTED_MIGRATIONS) {
479
    const appliedName = applied.get(migration.version);
480
    if (appliedName && appliedName !== migration.name)
481
      throw new Error(
482
        `Hosted migration ${migration.version} name mismatch: ${appliedName} != ${migration.name}`,
483
      );
484
    if (appliedName) continue;
485
    if (migration.version === 1) await applyBaseline(sql);
486
    if (migration.version === 2) await applyAppendOnlySourceRecords(sql);
487
    if (migration.version === 3) await removeHostedConversations(sql);
488
    if (migration.version === 4) await addAdaptiveAssuranceProbes(sql);
489
    if (migration.version === 5) await addAssuranceDecisionLearning(sql);
490
    if (migration.version === 6) await addConnectorClaims(sql);
491
    if (migration.version === 7) await addProviderConnections(sql);
492
    await sql`
493
      INSERT INTO openclasp_schema_migrations(version, name)
494
      VALUES (${migration.version}, ${migration.name})
495
      ON CONFLICT (version) DO NOTHING
496
    `;
497
  }
498
}
499
500
export async function verifyHostedMigrations(sql: Sql): Promise<void> {
501
  const tableRows = await sql`
502
    SELECT to_regclass('public.openclasp_schema_migrations') AS table_name
503
  `;
504
  if (!tableRows[0]?.table_name)
505
    throw new Error('Hosted database is not migrated; run `pnpm migrate` before starting the API');
506
  const appliedRows = await sql`
507
    SELECT version, name FROM openclasp_schema_migrations ORDER BY version ASC
508
  `;
509
  const applied = new Map(appliedRows.map((row) => [Number(row.version), String(row.name)]));
510
  for (const migration of HOSTED_MIGRATIONS) {
511
    const name = applied.get(migration.version);
512
    if (name !== migration.name)
513
      throw new Error(
514
        `Hosted database migration ${migration.version} is missing or invalid; run \`pnpm migrate\``,
515
      );
516
  }
517
  const removedConversationTables = await sql`
518
    SELECT
519
      to_regclass('public.openclasp_hosted_messages') AS messages,
520
      to_regclass('public.openclasp_hosted_threads') AS threads
521
  `;
522
  if (removedConversationTables[0]?.messages || removedConversationTables[0]?.threads)
523
    throw new Error('Hosted conversation tables still exist; run `pnpm migrate`');
524
  const assuranceTables = await sql`
525
    SELECT
526
      to_regclass('public.openclasp_ai_generations') AS generations,
527
      to_regclass('public.openclasp_assurance_probe_plans') AS plans,
528
      to_regclass('public.openclasp_assurance_probe_responses') AS responses,
529
      to_regclass('public.openclasp_assurance_claim_comparisons') AS comparisons
530
  `;
531
  if (
532
    !assuranceTables[0]?.generations ||
533
    !assuranceTables[0]?.plans ||
534
    !assuranceTables[0]?.responses ||
535
    !assuranceTables[0]?.comparisons
536
  )
537
    throw new Error('Adaptive assurance probe tables are missing; run `pnpm migrate`');
538
  const decisionTables = await sql`
539
    SELECT
540
      to_regclass('public.openclasp_assurance_assessments') AS assessments,
541
      to_regclass('public.openclasp_assurance_predictions') AS predictions,
542
      to_regclass('public.openclasp_assurance_safeguards') AS safeguards,
543
      to_regclass('public.openclasp_assurance_evaluations') AS evaluations
544
  `;
545
  if (
546
    !decisionTables[0]?.assessments ||
547
    !decisionTables[0]?.predictions ||
548
    !decisionTables[0]?.safeguards ||
549
    !decisionTables[0]?.evaluations
550
  )
551
    throw new Error('Assurance decision learning tables are missing; run `pnpm migrate`');
552
  const connectorTables = await sql`
553
    SELECT to_regclass('public.openclasp_connector_claims') AS claims
554
  `;
555
  if (!connectorTables[0]?.claims)
556
    throw new Error('Connector claim table is missing; run `pnpm migrate`');
557
  const providerTables = await sql`
558
    SELECT to_regclass('public.openclasp_provider_connections') AS connections
559
  `;
560
  if (!providerTables[0]?.connections)
561
    throw new Error('Provider connection table is missing; run `pnpm migrate`');
562
}