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

Viewing files at commit a93dad91899fCommit detailsView latest

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