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

Viewing files at commit 922a6b480fb8Commit detailsView latest

packages/persistence/src/hosted-migrations.ts 291 lines · 9.9 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
] as const;
9
10
async function applyBaseline(sql: Sql) {
11
  await sql`
12
    CREATE TABLE IF NOT EXISTS openclasp_records (
13
      operator_id TEXT NOT NULL,
14
      kind TEXT NOT NULL,
15
      record_id TEXT NOT NULL,
16
      payload JSONB NOT NULL,
17
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
18
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
19
      PRIMARY KEY (operator_id, kind, record_id)
20
    )
21
  `;
22
  await sql`
23
    CREATE INDEX IF NOT EXISTS openclasp_records_operator_created
24
    ON openclasp_records(operator_id, created_at DESC)
25
  `;
26
  await sql`
27
    CREATE TABLE IF NOT EXISTS openclasp_account_settings (
28
      operator_id TEXT PRIMARY KEY,
29
      display_name TEXT NOT NULL DEFAULT '',
30
      contribution_enabled BOOLEAN NOT NULL DEFAULT FALSE,
31
      retention_days INTEGER NOT NULL DEFAULT 30 CHECK (retention_days BETWEEN 0 AND 3650),
32
      evidence_sharing TEXT NOT NULL DEFAULT 'ask'
33
        CHECK (evidence_sharing IN ('never', 'ask', 'contract_only')),
34
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
35
    )
36
  `;
37
  await sql`
38
    CREATE TABLE IF NOT EXISTS openclasp_public_agents (
39
      agent_id TEXT PRIMARY KEY,
40
      operator_id TEXT NOT NULL,
41
      card JSONB NOT NULL,
42
      published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
43
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
44
    )
45
  `;
46
  await sql`
47
    CREATE INDEX IF NOT EXISTS openclasp_public_agents_updated
48
    ON openclasp_public_agents(updated_at DESC)
49
  `;
50
  await sql`
51
    CREATE TABLE IF NOT EXISTS openclasp_federated_interactions (
52
      interaction_id TEXT PRIMARY KEY,
53
      initiator_operator_id TEXT NOT NULL,
54
      responder_operator_id TEXT NOT NULL,
55
      initiator_agent_id TEXT NOT NULL,
56
      responder_agent_id TEXT NOT NULL,
57
      status TEXT NOT NULL,
58
      payload JSONB NOT NULL,
59
      expires_at TIMESTAMPTZ NOT NULL,
60
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
61
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
62
    )
63
  `;
64
  await sql`
65
    CREATE INDEX IF NOT EXISTS openclasp_federated_interactions_participants
66
    ON openclasp_federated_interactions(
67
      initiator_operator_id,
68
      responder_operator_id,
69
      updated_at DESC
70
    )
71
  `;
72
  await sql`
73
    CREATE TABLE IF NOT EXISTS openclasp_agent_runtimes (
74
      agent_id TEXT PRIMARY KEY,
75
      operator_id TEXT NOT NULL,
76
      endpoint TEXT NOT NULL,
77
      secret_ciphertext TEXT NOT NULL,
78
      secret_iv TEXT NOT NULL,
79
      secret_auth_tag TEXT NOT NULL,
80
      status TEXT NOT NULL CHECK (status IN ('verified', 'disabled')),
81
      verified_at TIMESTAMPTZ NOT NULL,
82
      last_delivery_at TIMESTAMPTZ,
83
      last_error TEXT,
84
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
85
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
86
    )
87
  `;
88
  await sql`
89
    CREATE TABLE IF NOT EXISTS openclasp_agent_access_tokens (
90
      token_id TEXT PRIMARY KEY,
91
      operator_id TEXT NOT NULL,
92
      agent_id TEXT NOT NULL,
93
      name TEXT NOT NULL,
94
      token_hash TEXT NOT NULL,
95
      scopes JSONB NOT NULL,
96
      expires_at TIMESTAMPTZ NOT NULL,
97
      last_used_at TIMESTAMPTZ,
98
      revoked_at TIMESTAMPTZ,
99
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
100
    )
101
  `;
102
  await sql`
103
    CREATE INDEX IF NOT EXISTS openclasp_agent_access_tokens_owner_agent
104
    ON openclasp_agent_access_tokens(operator_id, agent_id, created_at DESC)
105
  `;
106
  await sql`
107
    ALTER TABLE openclasp_agent_runtimes
108
    ADD COLUMN IF NOT EXISTS a2a_endpoint TEXT,
109
    ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ
110
  `;
111
  await sql`
112
    CREATE TABLE IF NOT EXISTS openclasp_live_sessions (
113
      interaction_id TEXT PRIMARY KEY,
114
      initiator_agent_id TEXT NOT NULL,
115
      responder_agent_id TEXT NOT NULL,
116
      initiator_session_id TEXT NOT NULL,
117
      responder_session_id TEXT NOT NULL,
118
      initiator_endpoint TEXT NOT NULL,
119
      responder_endpoint TEXT NOT NULL,
120
      status TEXT NOT NULL CHECK (status IN ('preparing', 'active', 'completed', 'failed')),
121
      expires_at TIMESTAMPTZ NOT NULL,
122
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
123
      activated_at TIMESTAMPTZ,
124
      completed_at TIMESTAMPTZ,
125
      last_error TEXT
126
    )
127
  `;
128
  await sql`
129
    CREATE TABLE IF NOT EXISTS openclasp_live_session_events (
130
      interaction_id TEXT NOT NULL,
131
      event_id TEXT NOT NULL,
132
      agent_id TEXT NOT NULL,
133
      sequence INTEGER NOT NULL,
134
      event JSONB NOT NULL,
135
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
136
      PRIMARY KEY (interaction_id, event_id),
137
      UNIQUE (interaction_id, agent_id, sequence)
138
    )
139
  `;
140
  await sql`
141
    CREATE TABLE IF NOT EXISTS openclasp_hosted_threads (
142
      thread_id TEXT PRIMARY KEY,
143
      interaction_id TEXT NOT NULL UNIQUE,
144
      participant_a_agent_id TEXT NOT NULL,
145
      participant_b_agent_id TEXT NOT NULL,
146
      status TEXT NOT NULL CHECK (status IN ('open', 'closed')) DEFAULT 'open',
147
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
148
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
149
      expires_at TIMESTAMPTZ NOT NULL
150
    )
151
  `;
152
  await sql`
153
    CREATE INDEX IF NOT EXISTS openclasp_hosted_threads_participants
154
    ON openclasp_hosted_threads(
155
      participant_a_agent_id,
156
      participant_b_agent_id,
157
      updated_at DESC
158
    )
159
  `;
160
  await sql`
161
    CREATE TABLE IF NOT EXISTS openclasp_hosted_messages (
162
      message_id TEXT PRIMARY KEY,
163
      thread_id TEXT NOT NULL REFERENCES openclasp_hosted_threads(thread_id) ON DELETE CASCADE,
164
      interaction_id TEXT NOT NULL,
165
      sender_agent_id TEXT NOT NULL,
166
      recipient_agent_id TEXT NOT NULL,
167
      request_key TEXT NOT NULL,
168
      content_ciphertext TEXT NOT NULL,
169
      content_iv TEXT NOT NULL,
170
      content_auth_tag TEXT NOT NULL,
171
      content_hash TEXT NOT NULL,
172
      delivery TEXT NOT NULL CHECK (delivery IN ('accepted', 'delivered', 'read')),
173
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
174
      read_at TIMESTAMPTZ
175
    )
176
  `;
177
  await sql`
178
    CREATE UNIQUE INDEX IF NOT EXISTS openclasp_hosted_messages_dedup
179
    ON openclasp_hosted_messages(thread_id, sender_agent_id, request_key)
180
  `;
181
  await sql`
182
    CREATE INDEX IF NOT EXISTS openclasp_hosted_messages_thread_created
183
    ON openclasp_hosted_messages(thread_id, created_at ASC)
184
  `;
185
}
186
187
async function applyAppendOnlySourceRecords(sql: Sql) {
188
  await sql`
189
    CREATE TABLE IF NOT EXISTS openclasp_source_records (
190
      event_id UUID PRIMARY KEY,
191
      operator_id TEXT NOT NULL,
192
      kind TEXT NOT NULL,
193
      record_id TEXT NOT NULL,
194
      schema_name TEXT NOT NULL,
195
      schema_version TEXT NOT NULL,
196
      payload JSONB NOT NULL,
197
      payload_digest TEXT NOT NULL,
198
      entity_refs JSONB NOT NULL DEFAULT '{}'::jsonb,
199
      provenance TEXT NOT NULL,
200
      visibility TEXT NOT NULL,
201
      retention_class TEXT NOT NULL,
202
      learning_scope TEXT NOT NULL,
203
      reported_at TIMESTAMPTZ NOT NULL,
204
      ingested_at TIMESTAMPTZ NOT NULL,
205
      UNIQUE (operator_id, kind, record_id, payload_digest),
206
      CHECK (provenance IN (
207
        'self_declared',
208
        'operator_attested',
209
        'cryptographically_verified',
210
        'domain_verified',
211
        'third_party_verified',
212
        'observed',
213
        'disputed'
214
      )),
215
      CHECK (visibility IN (
216
        'local_only',
217
        'private_requester',
218
        'private_responder',
219
        'shared_participants',
220
        'network_aggregate'
221
      )),
222
      CHECK (retention_class IN ('account', 'audit', 'operational', 'temporary')),
223
      CHECK (learning_scope IN (
224
        'not_evaluated',
225
        'excluded',
226
        'local_only',
227
        'network_aggregate'
228
      ))
229
    )
230
  `;
231
  await sql`
232
    CREATE INDEX IF NOT EXISTS openclasp_source_records_operator_ingested
233
    ON openclasp_source_records(operator_id, ingested_at DESC)
234
  `;
235
  await sql`
236
    CREATE INDEX IF NOT EXISTS openclasp_source_records_history
237
    ON openclasp_source_records(operator_id, kind, record_id, ingested_at ASC)
238
  `;
239
  await sql`
240
    CREATE INDEX IF NOT EXISTS openclasp_source_records_entities
241
    ON openclasp_source_records USING GIN(entity_refs)
242
  `;
243
}
244
245
export async function runHostedMigrations(sql: Sql): Promise<void> {
246
  await sql`
247
    CREATE TABLE IF NOT EXISTS openclasp_schema_migrations (
248
      version INTEGER PRIMARY KEY,
249
      name TEXT NOT NULL UNIQUE,
250
      applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
251
    )
252
  `;
253
  const appliedRows = await sql`
254
    SELECT version, name FROM openclasp_schema_migrations ORDER BY version ASC
255
  `;
256
  const applied = new Map(appliedRows.map((row) => [Number(row.version), String(row.name)]));
257
  for (const migration of HOSTED_MIGRATIONS) {
258
    const appliedName = applied.get(migration.version);
259
    if (appliedName && appliedName !== migration.name)
260
      throw new Error(
261
        `Hosted migration ${migration.version} name mismatch: ${appliedName} != ${migration.name}`,
262
      );
263
    if (appliedName) continue;
264
    if (migration.version === 1) await applyBaseline(sql);
265
    if (migration.version === 2) await applyAppendOnlySourceRecords(sql);
266
    await sql`
267
      INSERT INTO openclasp_schema_migrations(version, name)
268
      VALUES (${migration.version}, ${migration.name})
269
      ON CONFLICT (version) DO NOTHING
270
    `;
271
  }
272
}
273
274
export async function verifyHostedMigrations(sql: Sql): Promise<void> {
275
  const tableRows = await sql`
276
    SELECT to_regclass('public.openclasp_schema_migrations') AS table_name
277
  `;
278
  if (!tableRows[0]?.table_name)
279
    throw new Error('Hosted database is not migrated; run `pnpm migrate` before starting the API');
280
  const appliedRows = await sql`
281
    SELECT version, name FROM openclasp_schema_migrations ORDER BY version ASC
282
  `;
283
  const applied = new Map(appliedRows.map((row) => [Number(row.version), String(row.name)]));
284
  for (const migration of HOSTED_MIGRATIONS) {
285
    const name = applied.get(migration.version);
286
    if (name !== migration.name)
287
      throw new Error(
288
        `Hosted database migration ${migration.version} is missing or invalid; run \`pnpm migrate\``,
289
      );
290
  }
291
}