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

Viewing files at commit 9c0e6fd29feaCommit detailsView latest

packages/persistence/src/hosted.ts 257 lines · 8.2 KB · typescript
1
import { neon, type NeonQueryFunction } from '@neondatabase/serverless';
2
3
export type HostedRecordKind =
4
  | 'agent'
5
  | 'delegation'
6
  | 'contract'
7
  | 'interaction'
8
  | 'event'
9
  | 'receipt'
10
  | 'feedback'
11
  | 'conflict'
12
  | 'profile'
13
  | 'consent'
14
  | 'revocation'
15
  | 'project'
16
  | 'agent_profile'
17
  | 'installation'
18
  | 'setup_request'
19
  | 'publication';
20
21
export type PublicAgentCard = {
22
  agentId: string;
23
  name: string;
24
  framework: string;
25
  capabilities: string[];
26
  limitations: string[];
27
  assurance: 'oauth_authenticated' | 'cryptographically_verified';
28
  publishedAt: string;
29
  updatedAt: string;
30
};
31
32
export type AccountSettings = {
33
  displayName: string;
34
  contributionEnabled: boolean;
35
  retentionDays: number;
36
  evidenceSharing: 'never' | 'ask' | 'contract_only';
37
  rawConversationsStored: false;
38
};
39
40
const defaults: AccountSettings = {
41
  displayName: '',
42
  contributionEnabled: false,
43
  retentionDays: 30,
44
  evidenceSharing: 'ask',
45
  rawConversationsStored: false,
46
};
47
48
export class HostedRepository {
49
  private readonly sql: NeonQueryFunction<false, false>;
50
  private initialized?: Promise<void>;
51
52
  constructor(databaseUrl: string) {
53
    this.sql = neon(databaseUrl);
54
  }
55
56
  ensureSchema(): Promise<void> {
57
    return (this.initialized ??= (async () => {
58
      await this.sql`
59
        CREATE TABLE IF NOT EXISTS openclasp_records (
60
          operator_id TEXT NOT NULL,
61
          kind TEXT NOT NULL,
62
          record_id TEXT NOT NULL,
63
          payload JSONB NOT NULL,
64
          created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
65
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
66
          PRIMARY KEY (operator_id, kind, record_id)
67
        )
68
      `;
69
      await this.sql`
70
        CREATE INDEX IF NOT EXISTS openclasp_records_operator_created
71
        ON openclasp_records(operator_id, created_at DESC)
72
      `;
73
      await this.sql`
74
        CREATE TABLE IF NOT EXISTS openclasp_account_settings (
75
          operator_id TEXT PRIMARY KEY,
76
          display_name TEXT NOT NULL DEFAULT '',
77
          contribution_enabled BOOLEAN NOT NULL DEFAULT FALSE,
78
          retention_days INTEGER NOT NULL DEFAULT 30 CHECK (retention_days BETWEEN 0 AND 3650),
79
          evidence_sharing TEXT NOT NULL DEFAULT 'ask'
80
            CHECK (evidence_sharing IN ('never', 'ask', 'contract_only')),
81
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
82
        )
83
      `;
84
      await this.sql`
85
        CREATE TABLE IF NOT EXISTS openclasp_public_agents (
86
          agent_id TEXT PRIMARY KEY,
87
          operator_id TEXT NOT NULL,
88
          card JSONB NOT NULL,
89
          published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
90
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
91
        )
92
      `;
93
      await this.sql`
94
        CREATE INDEX IF NOT EXISTS openclasp_public_agents_updated
95
        ON openclasp_public_agents(updated_at DESC)
96
      `;
97
    })());
98
  }
99
100
  async upsert(
101
    operatorId: string,
102
    kind: HostedRecordKind,
103
    recordId: string,
104
    payload: unknown,
105
  ): Promise<void> {
106
    await this.ensureSchema();
107
    const encoded = JSON.stringify(payload);
108
    await this.sql`
109
      INSERT INTO openclasp_records(operator_id, kind, record_id, payload)
110
      VALUES (${operatorId}, ${kind}, ${recordId}, ${encoded}::jsonb)
111
      ON CONFLICT (operator_id, kind, record_id)
112
      DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()
113
    `;
114
  }
115
116
  async list(
117
    operatorId: string,
118
  ): Promise<{ kind: HostedRecordKind; recordId: string; payload: any }[]> {
119
    await this.ensureSchema();
120
    const rows = await this.sql`
121
      SELECT kind, record_id, payload
122
      FROM openclasp_records
123
      WHERE operator_id = ${operatorId}
124
      ORDER BY created_at ASC
125
    `;
126
    return rows.map((row) => ({
127
      kind: row.kind as HostedRecordKind,
128
      recordId: String(row.record_id),
129
      payload: row.payload,
130
    }));
131
  }
132
133
  async dashboard(operatorId: string) {
134
    const rows = await this.list(operatorId);
135
    const ofKind = (kind: HostedRecordKind) =>
136
      rows.filter((row) => row.kind === kind).map((row) => row.payload);
137
    return {
138
      agents: [...ofKind('agent_profile'), ...ofKind('agent')],
139
      projects: ofKind('project'),
140
      installations: ofKind('installation'),
141
      setupRequests: ofKind('setup_request'),
142
      publications: ofKind('publication'),
143
      interactions: ofKind('interaction'),
144
      events: ofKind('event'),
145
      conflicts: ofKind('conflict'),
146
      receipts: ofKind('receipt'),
147
      profiles: ofKind('profile'),
148
    };
149
  }
150
151
  async publishAgent(operatorId: string, card: PublicAgentCard): Promise<PublicAgentCard> {
152
    await this.ensureSchema();
153
    const encoded = JSON.stringify(card);
154
    const rows = await this.sql`
155
      INSERT INTO openclasp_public_agents(agent_id, operator_id, card, published_at, updated_at)
156
      VALUES (${card.agentId}, ${operatorId}, ${encoded}::jsonb, NOW(), NOW())
157
      ON CONFLICT (agent_id) DO UPDATE SET
158
        card = EXCLUDED.card,
159
        updated_at = NOW()
160
      WHERE openclasp_public_agents.operator_id = ${operatorId}
161
      RETURNING card
162
    `;
163
    const published = rows[0]?.card as PublicAgentCard | undefined;
164
    if (!published) throw new Error('Agent ID is already owned by another operator');
165
    return published;
166
  }
167
168
  async unpublishAgent(operatorId: string, agentId: string): Promise<boolean> {
169
    await this.ensureSchema();
170
    const rows = await this.sql`
171
      DELETE FROM openclasp_public_agents
172
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
173
      RETURNING agent_id
174
    `;
175
    return rows.length > 0;
176
  }
177
178
  async getPublishedAgent(agentId: string): Promise<PublicAgentCard | undefined> {
179
    await this.ensureSchema();
180
    const rows = await this.sql`
181
      SELECT card FROM openclasp_public_agents WHERE agent_id = ${agentId}
182
    `;
183
    return rows[0]?.card as PublicAgentCard | undefined;
184
  }
185
186
  async searchPublishedAgents(input: {
187
    query?: string | undefined;
188
    capability?: string | undefined;
189
    limit?: number | undefined;
190
  }): Promise<PublicAgentCard[]> {
191
    await this.ensureSchema();
192
    const rows = await this.sql`
193
      SELECT card FROM openclasp_public_agents ORDER BY updated_at DESC LIMIT 100
194
    `;
195
    const query = input.query?.trim().toLowerCase();
196
    const capability = input.capability?.trim().toLowerCase();
197
    return rows
198
      .map((row) => row.card as PublicAgentCard)
199
      .filter(
200
        (card) =>
201
          (!query ||
202
            card.name.toLowerCase().includes(query) ||
203
            card.framework.toLowerCase().includes(query)) &&
204
          (!capability ||
205
            card.capabilities.some((value) => value.toLowerCase().includes(capability))),
206
      )
207
      .slice(0, Math.min(Math.max(input.limit ?? 20, 1), 50));
208
  }
209
210
  async getSettings(operatorId: string): Promise<AccountSettings> {
211
    await this.ensureSchema();
212
    const rows = await this.sql`
213
      SELECT display_name, contribution_enabled, retention_days, evidence_sharing
214
      FROM openclasp_account_settings
215
      WHERE operator_id = ${operatorId}
216
    `;
217
    const row = rows[0] as
218
      | {
219
          display_name: string;
220
          contribution_enabled: boolean;
221
          retention_days: number;
222
          evidence_sharing: AccountSettings['evidenceSharing'];
223
        }
224
      | undefined;
225
    return row
226
      ? {
227
          displayName: row.display_name,
228
          contributionEnabled: row.contribution_enabled,
229
          retentionDays: row.retention_days,
230
          evidenceSharing: row.evidence_sharing,
231
          rawConversationsStored: false,
232
        }
233
      : defaults;
234
  }
235
236
  async saveSettings(
237
    operatorId: string,
238
    settings: Omit<AccountSettings, 'rawConversationsStored'>,
239
  ): Promise<AccountSettings> {
240
    await this.ensureSchema();
241
    await this.sql`
242
      INSERT INTO openclasp_account_settings(
243
        operator_id, display_name, contribution_enabled, retention_days, evidence_sharing
244
      ) VALUES (
245
        ${operatorId}, ${settings.displayName}, ${settings.contributionEnabled},
246
        ${settings.retentionDays}, ${settings.evidenceSharing}
247
      )
248
      ON CONFLICT (operator_id) DO UPDATE SET
249
        display_name = EXCLUDED.display_name,
250
        contribution_enabled = EXCLUDED.contribution_enabled,
251
        retention_days = EXCLUDED.retention_days,
252
        evidence_sharing = EXCLUDED.evidence_sharing,
253
        updated_at = NOW()
254
    `;
255
    return { ...settings, rawConversationsStored: false };
256
  }
257
}