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

Viewing files at commit b506ef3834f7Commit detailsView latest

packages/persistence/src/onboarding.ts 314 lines · 10.9 KB · typescript
1
import { randomUUID } from 'node:crypto';
2
3
export type Project = {
4
  projectId: string;
5
  name: string;
6
  createdAt: string;
7
};
8
9
export type AgentProfile = {
10
  agentId: string;
11
  projectId: string;
12
  name: string;
13
  description: string;
14
  framework: string;
15
  agentVersion: string;
16
  a2aEndpoint?: string;
17
  transport?: 'openclasp_gateway';
18
  autoPublish: boolean;
19
  autoAcceptPolicy: 'off' | 'safe_matching';
20
  autoAcceptTaskCategories: string[];
21
  capabilities: string[];
22
  limitations: string[];
23
  identityMode: 'oauth_installation';
24
  status: 'active' | 'revoked';
25
  createdAt: string;
26
  updatedAt: string;
27
};
28
29
export type AgentInstallation = {
30
  installationId: string;
31
  clientId: string;
32
  agentId: string;
33
  projectId: string;
34
  connectedAt: string;
35
  updatedAt: string;
36
};
37
38
export type SetupRequest = {
39
  requestId: string;
40
  clientId: string;
41
  action: 'connect' | 'switch';
42
  status: 'pending' | 'approved' | 'rejected';
43
  agentName?: string;
44
  description?: string;
45
  projectName?: string;
46
  projectId?: string;
47
  existingAgentId?: string;
48
  framework: string;
49
  agentVersion: string;
50
  a2aEndpoint?: string;
51
  autoPublish: boolean;
52
  autoAcceptPolicy: 'off' | 'safe_matching';
53
  autoAcceptTaskCategories: string[];
54
  capabilities: string[];
55
  limitations: string[];
56
  requestedAt: string;
57
  decidedAt?: string;
58
  installationId?: string;
59
};
60
61
export type OnboardingKind = 'project' | 'agent_profile' | 'installation' | 'setup_request';
62
63
export type OnboardingRow = {
64
  kind: string;
65
  recordId: string;
66
  payload: any;
67
};
68
69
export interface OnboardingStore {
70
  list(operatorId: string): Promise<OnboardingRow[]>;
71
  upsert(
72
    operatorId: string,
73
    kind: OnboardingKind,
74
    recordId: string,
75
    payload: unknown,
76
  ): Promise<void>;
77
}
78
79
export type OnboardingState = {
80
  projects: Project[];
81
  agentProfiles: AgentProfile[];
82
  installations: AgentInstallation[];
83
  setupRequests: SetupRequest[];
84
};
85
86
export async function getOnboardingState(
87
  store: OnboardingStore,
88
  operatorId: string,
89
): Promise<OnboardingState> {
90
  const rows = await store.list(operatorId);
91
  const values = <T>(kind: OnboardingKind) =>
92
    rows.filter((row) => row.kind === kind).map((row) => row.payload as T);
93
  return {
94
    projects: values<Project>('project'),
95
    agentProfiles: values<AgentProfile>('agent_profile').map((agent) => ({
96
      ...agent,
97
      transport: 'openclasp_gateway',
98
      description: agent.description ?? '',
99
      agentVersion: agent.agentVersion ?? '1.0.0',
100
      autoPublish: agent.autoPublish ?? false,
101
      autoAcceptPolicy: agent.autoAcceptPolicy ?? 'off',
102
      autoAcceptTaskCategories: agent.autoAcceptTaskCategories ?? [],
103
    })),
104
    installations: values<AgentInstallation>('installation'),
105
    setupRequests: values<SetupRequest>('setup_request'),
106
  };
107
}
108
109
export async function resolveInstallation(
110
  store: OnboardingStore,
111
  operatorId: string,
112
  clientId: string,
113
) {
114
  const state = await getOnboardingState(store, operatorId);
115
  const installation = state.installations.find((item) => item.clientId === clientId);
116
  if (!installation) return { status: 'unbound' as const };
117
  const agent = state.agentProfiles.find((item) => item.agentId === installation.agentId);
118
  const project = state.projects.find((item) => item.projectId === installation.projectId);
119
  if (!agent || !project) return { status: 'invalid_binding' as const, installation };
120
  return { status: 'connected' as const, installation, agent, project };
121
}
122
123
export async function requestAgentSetup(
124
  store: OnboardingStore,
125
  operatorId: string,
126
  input: {
127
    clientId: string;
128
    action?: 'connect' | 'switch' | undefined;
129
    agentName?: string | undefined;
130
    projectName?: string | undefined;
131
    projectId?: string | undefined;
132
    existingAgentId?: string | undefined;
133
    framework?: string | undefined;
134
    description?: string | undefined;
135
    agentVersion?: string | undefined;
136
    a2aEndpoint?: string | undefined;
137
    autoPublish?: boolean | undefined;
138
    autoAcceptPolicy?: 'off' | 'safe_matching' | undefined;
139
    autoAcceptTaskCategories?: string[] | undefined;
140
    capabilities?: string[] | undefined;
141
    limitations?: string[] | undefined;
142
  },
143
): Promise<SetupRequest> {
144
  const state = await getOnboardingState(store, operatorId);
145
  const pending = state.setupRequests.find(
146
    (item) => item.clientId === input.clientId && item.status === 'pending',
147
  );
148
  if (pending) return pending;
149
  if (
150
    !input.existingAgentId &&
151
    (!input.agentName?.trim() || (!input.projectId && !input.projectName?.trim()))
152
  )
153
    throw new Error('agentName and either projectName or projectId are required for a new agent');
154
  if (
155
    input.existingAgentId &&
156
    !state.agentProfiles.some((agent) => agent.agentId === input.existingAgentId)
157
  )
158
    throw new Error('Existing agent not found');
159
  if (input.projectId && !state.projects.some((project) => project.projectId === input.projectId))
160
    throw new Error('Project not found');
161
162
  const request: SetupRequest = {
163
    requestId: randomUUID(),
164
    clientId: input.clientId,
165
    action: input.action ?? (input.existingAgentId ? 'switch' : 'connect'),
166
    status: 'pending',
167
    ...(input.agentName?.trim() ? { agentName: input.agentName.trim() } : {}),
168
    ...(input.projectName?.trim() ? { projectName: input.projectName.trim() } : {}),
169
    ...(input.projectId ? { projectId: input.projectId } : {}),
170
    ...(input.existingAgentId ? { existingAgentId: input.existingAgentId } : {}),
171
    framework: input.framework?.trim() || 'unknown',
172
    description: input.description?.trim() || '',
173
    agentVersion: input.agentVersion?.trim() || '1.0.0',
174
    ...(input.a2aEndpoint?.trim() ? { a2aEndpoint: input.a2aEndpoint.trim() } : {}),
175
    autoPublish: input.autoPublish ?? true,
176
    autoAcceptPolicy: input.autoAcceptPolicy ?? 'safe_matching',
177
    autoAcceptTaskCategories: [
178
      ...new Set(input.autoAcceptTaskCategories ?? input.capabilities ?? []),
179
    ],
180
    capabilities: [...new Set(input.capabilities ?? [])],
181
    limitations: [...new Set(input.limitations ?? [])],
182
    requestedAt: new Date().toISOString(),
183
  };
184
  await store.upsert(operatorId, 'setup_request', request.requestId, request);
185
  return request;
186
}
187
188
export async function approveAgentSetup(
189
  store: OnboardingStore,
190
  operatorId: string,
191
  requestId: string,
192
) {
193
  const state = await getOnboardingState(store, operatorId);
194
  const request = state.setupRequests.find((item) => item.requestId === requestId);
195
  if (!request) throw new Error('Setup request not found');
196
  if (request.status === 'rejected') throw new Error('Setup request was rejected');
197
  if (request.status === 'approved')
198
    return resolveInstallation(store, operatorId, request.clientId);
199
200
  const now = new Date().toISOString();
201
  let agent = request.existingAgentId
202
    ? state.agentProfiles.find((item) => item.agentId === request.existingAgentId)
203
    : undefined;
204
  let project = agent
205
    ? state.projects.find((item) => item.projectId === agent!.projectId)
206
    : request.projectId
207
      ? state.projects.find((item) => item.projectId === request.projectId)
208
      : undefined;
209
210
  if (!project) {
211
    project = {
212
      projectId: `project_${randomUUID()}`,
213
      name: request.projectName!,
214
      createdAt: now,
215
    };
216
    await store.upsert(operatorId, 'project', project.projectId, project);
217
  }
218
  if (!agent) {
219
    agent = {
220
      agentId: `agent_${randomUUID()}`,
221
      projectId: project.projectId,
222
      name: request.agentName!,
223
      description: request.description ?? '',
224
      framework: request.framework,
225
      agentVersion: request.agentVersion,
226
      transport: 'openclasp_gateway',
227
      autoPublish: request.autoPublish ?? false,
228
      autoAcceptPolicy: request.autoAcceptPolicy ?? 'off',
229
      autoAcceptTaskCategories: request.autoAcceptTaskCategories ?? [],
230
      capabilities: request.capabilities,
231
      limitations: request.limitations,
232
      identityMode: 'oauth_installation',
233
      status: 'active',
234
      createdAt: now,
235
      updatedAt: now,
236
    };
237
    await store.upsert(operatorId, 'agent_profile', agent.agentId, agent);
238
  }
239
240
  const previous = state.installations.find((item) => item.clientId === request.clientId);
241
  const installation: AgentInstallation = {
242
    installationId: previous?.installationId ?? `install_${randomUUID()}`,
243
    clientId: request.clientId,
244
    agentId: agent.agentId,
245
    projectId: project.projectId,
246
    connectedAt: previous?.connectedAt ?? now,
247
    updatedAt: now,
248
  };
249
  await store.upsert(operatorId, 'installation', request.clientId, installation);
250
  await store.upsert(operatorId, 'setup_request', request.requestId, {
251
    ...request,
252
    status: 'approved',
253
    decidedAt: now,
254
    installationId: installation.installationId,
255
  } satisfies SetupRequest);
256
  return { status: 'connected' as const, installation, agent, project };
257
}
258
259
export async function rejectAgentSetup(
260
  store: OnboardingStore,
261
  operatorId: string,
262
  requestId: string,
263
) {
264
  const state = await getOnboardingState(store, operatorId);
265
  const request = state.setupRequests.find((item) => item.requestId === requestId);
266
  if (!request) throw new Error('Setup request not found');
267
  if (request.status !== 'pending') throw new Error('Setup request is no longer pending');
268
  const rejected: SetupRequest = {
269
    ...request,
270
    status: 'rejected',
271
    decidedAt: new Date().toISOString(),
272
  };
273
  await store.upsert(operatorId, 'setup_request', requestId, rejected);
274
  return rejected;
275
}
276
277
export async function updateAgentProfile(
278
  store: OnboardingStore,
279
  operatorId: string,
280
  clientId: string,
281
  patch: {
282
    name?: string | undefined;
283
    description?: string | undefined;
284
    framework?: string | undefined;
285
    agentVersion?: string | undefined;
286
    a2aEndpoint?: string | undefined;
287
    autoPublish?: boolean | undefined;
288
    autoAcceptPolicy?: 'off' | 'safe_matching' | undefined;
289
    autoAcceptTaskCategories?: string[] | undefined;
290
    capabilities?: string[] | undefined;
291
    limitations?: string[] | undefined;
292
  },
293
) {
294
  const binding = await resolveInstallation(store, operatorId, clientId);
295
  if (binding.status !== 'connected') throw new Error('This MCP installation is not connected');
296
  const agent: AgentProfile = {
297
    ...binding.agent,
298
    transport: 'openclasp_gateway',
299
    ...(patch.name?.trim() ? { name: patch.name.trim() } : {}),
300
    ...(patch.description !== undefined ? { description: patch.description.trim() } : {}),
301
    ...(patch.framework?.trim() ? { framework: patch.framework.trim() } : {}),
302
    ...(patch.agentVersion?.trim() ? { agentVersion: patch.agentVersion.trim() } : {}),
303
    ...(patch.autoPublish !== undefined ? { autoPublish: patch.autoPublish } : {}),
304
    ...(patch.autoAcceptPolicy ? { autoAcceptPolicy: patch.autoAcceptPolicy } : {}),
305
    ...(patch.autoAcceptTaskCategories
306
      ? { autoAcceptTaskCategories: [...new Set(patch.autoAcceptTaskCategories)] }
307
      : {}),
308
    ...(patch.capabilities ? { capabilities: [...new Set(patch.capabilities)] } : {}),
309
    ...(patch.limitations ? { limitations: [...new Set(patch.limitations)] } : {}),
310
    updatedAt: new Date().toISOString(),
311
  };
312
  await store.upsert(operatorId, 'agent_profile', agent.agentId, agent);
313
  return agent;
314
}