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

Viewing files at commit 3dc3c2c6da63Commit detailsView latest

packages/persistence/src/hosted.ts 172 lines · 5.3 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
20
export type AccountSettings = {
21
  displayName: string;
22
  contributionEnabled: boolean;
23
  retentionDays: number;
24
  evidenceSharing: 'never' | 'ask' | 'contract_only';
25
  rawConversationsStored: false;
26
};
27
28
const defaults: AccountSettings = {
29
  displayName: '',
30
  contributionEnabled: false,
31
  retentionDays: 30,
32
  evidenceSharing: 'ask',
33
  rawConversationsStored: false,
34
};
35
36
export class HostedRepository {
37
  private readonly sql: NeonQueryFunction<false, false>;
38
  private initialized?: Promise<void>;
39
40
  constructor(databaseUrl: string) {
41
    this.sql = neon(databaseUrl);
42
  }
43
44
  ensureSchema(): Promise<void> {
45
    return (this.initialized ??= (async () => {
46
      await this.sql`
47
        CREATE TABLE IF NOT EXISTS openclasp_records (
48
          operator_id TEXT NOT NULL,
49
          kind TEXT NOT NULL,
50
          record_id TEXT NOT NULL,
51
          payload JSONB NOT NULL,
52
          created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
53
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
54
          PRIMARY KEY (operator_id, kind, record_id)
55
        )
56
      `;
57
      await this.sql`
58
        CREATE INDEX IF NOT EXISTS openclasp_records_operator_created
59
        ON openclasp_records(operator_id, created_at DESC)
60
      `;
61
      await this.sql`
62
        CREATE TABLE IF NOT EXISTS openclasp_account_settings (
63
          operator_id TEXT PRIMARY KEY,
64
          display_name TEXT NOT NULL DEFAULT '',
65
          contribution_enabled BOOLEAN NOT NULL DEFAULT FALSE,
66
          retention_days INTEGER NOT NULL DEFAULT 30 CHECK (retention_days BETWEEN 0 AND 3650),
67
          evidence_sharing TEXT NOT NULL DEFAULT 'ask'
68
            CHECK (evidence_sharing IN ('never', 'ask', 'contract_only')),
69
          updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
70
        )
71
      `;
72
    })());
73
  }
74
75
  async upsert(
76
    operatorId: string,
77
    kind: HostedRecordKind,
78
    recordId: string,
79
    payload: unknown,
80
  ): Promise<void> {
81
    await this.ensureSchema();
82
    const encoded = JSON.stringify(payload);
83
    await this.sql`
84
      INSERT INTO openclasp_records(operator_id, kind, record_id, payload)
85
      VALUES (${operatorId}, ${kind}, ${recordId}, ${encoded}::jsonb)
86
      ON CONFLICT (operator_id, kind, record_id)
87
      DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()
88
    `;
89
  }
90
91
  async list(
92
    operatorId: string,
93
  ): Promise<{ kind: HostedRecordKind; recordId: string; payload: any }[]> {
94
    await this.ensureSchema();
95
    const rows = await this.sql`
96
      SELECT kind, record_id, payload
97
      FROM openclasp_records
98
      WHERE operator_id = ${operatorId}
99
      ORDER BY created_at ASC
100
    `;
101
    return rows.map((row) => ({
102
      kind: row.kind as HostedRecordKind,
103
      recordId: String(row.record_id),
104
      payload: row.payload,
105
    }));
106
  }
107
108
  async dashboard(operatorId: string) {
109
    const rows = await this.list(operatorId);
110
    const ofKind = (kind: HostedRecordKind) =>
111
      rows.filter((row) => row.kind === kind).map((row) => row.payload);
112
    return {
113
      agents: [...ofKind('agent_profile'), ...ofKind('agent')],
114
      projects: ofKind('project'),
115
      installations: ofKind('installation'),
116
      setupRequests: ofKind('setup_request'),
117
      interactions: ofKind('interaction'),
118
      events: ofKind('event'),
119
      conflicts: ofKind('conflict'),
120
      receipts: ofKind('receipt'),
121
      profiles: ofKind('profile'),
122
    };
123
  }
124
125
  async getSettings(operatorId: string): Promise<AccountSettings> {
126
    await this.ensureSchema();
127
    const rows = await this.sql`
128
      SELECT display_name, contribution_enabled, retention_days, evidence_sharing
129
      FROM openclasp_account_settings
130
      WHERE operator_id = ${operatorId}
131
    `;
132
    const row = rows[0] as
133
      | {
134
          display_name: string;
135
          contribution_enabled: boolean;
136
          retention_days: number;
137
          evidence_sharing: AccountSettings['evidenceSharing'];
138
        }
139
      | undefined;
140
    return row
141
      ? {
142
          displayName: row.display_name,
143
          contributionEnabled: row.contribution_enabled,
144
          retentionDays: row.retention_days,
145
          evidenceSharing: row.evidence_sharing,
146
          rawConversationsStored: false,
147
        }
148
      : defaults;
149
  }
150
151
  async saveSettings(
152
    operatorId: string,
153
    settings: Omit<AccountSettings, 'rawConversationsStored'>,
154
  ): Promise<AccountSettings> {
155
    await this.ensureSchema();
156
    await this.sql`
157
      INSERT INTO openclasp_account_settings(
158
        operator_id, display_name, contribution_enabled, retention_days, evidence_sharing
159
      ) VALUES (
160
        ${operatorId}, ${settings.displayName}, ${settings.contributionEnabled},
161
        ${settings.retentionDays}, ${settings.evidenceSharing}
162
      )
163
      ON CONFLICT (operator_id) DO UPDATE SET
164
        display_name = EXCLUDED.display_name,
165
        contribution_enabled = EXCLUDED.contribution_enabled,
166
        retention_days = EXCLUDED.retention_days,
167
        evidence_sharing = EXCLUDED.evidence_sharing,
168
        updated_at = NOW()
169
    `;
170
    return { ...settings, rawConversationsStored: false };
171
  }
172
}