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

Viewing files at commit 8974f6e2faf1Commit detailsView latest

packages/persistence/src/onboarding.ts 432 lines · 15.3 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
  agentMode?: 'persistent_runtime' | 'temporary_chat';
17
  a2aEndpoint?: string;
18
  transport?: 'direct_a2a' | 'openclasp_gateway';
19
  autoPublish: boolean;
20
  autoAcceptPolicy: 'off' | 'safe_matching';
21
  autoAcceptTaskCategories: string[];
22
  capabilities: string[];
23
  limitations: string[];
24
  identityMode: 'oauth_installation' | 'owner_managed';
25
  status: 'active' | 'revoked';
26
  createdAt: string;
27
  updatedAt: string;
28
};
29
30
export type AgentInstallation = {
31
  installationId: string;
32
  clientId: string;
33
  agentId: string;
34
  projectId: string;
35
  connectedAt: string;
36
  updatedAt: string;
37
};
38
39
export type SetupRequest = {
40
  requestId: string;
41
  clientId: string;
42
  action: 'connect' | 'switch';
43
  status: 'pending' | 'approved' | 'rejected';
44
  agentName?: string;
45
  description?: string;
46
  projectName?: string;
47
  projectId?: string;
48
  existingAgentId?: string;
49
  framework: string;
50
  agentVersion: string;
51
  agentMode: 'persistent_runtime' | 'temporary_chat';
52
  a2aEndpoint?: string;
53
  autoPublish: boolean;
54
  autoAcceptPolicy: 'off' | 'safe_matching';
55
  autoAcceptTaskCategories: string[];
56
  capabilities: string[];
57
  limitations: string[];
58
  requestedAt: string;
59
  decidedAt?: string;
60
  installationId?: string;
61
};
62
63
export type OnboardingKind = 'project' | 'agent_profile' | 'installation' | 'setup_request';
64
65
export type OnboardingRow = {
66
  kind: string;
67
  recordId: string;
68
  payload: any;
69
};
70
71
export interface OnboardingStore {
72
  list(operatorId: string): Promise<OnboardingRow[]>;
73
  upsert(
74
    operatorId: string,
75
    kind: OnboardingKind,
76
    recordId: string,
77
    payload: unknown,
78
  ): Promise<void>;
79
}
80
81
export type OnboardingState = {
82
  projects: Project[];
83
  agentProfiles: AgentProfile[];
84
  installations: AgentInstallation[];
85
  setupRequests: SetupRequest[];
86
};
87
88
export async function getOnboardingState(
89
  store: OnboardingStore,
90
  operatorId: string,
91
): Promise<OnboardingState> {
92
  const rows = await store.list(operatorId);
93
  const values = <T>(kind: OnboardingKind) =>
94
    rows.filter((row) => row.kind === kind).map((row) => row.payload as T);
95
  return {
96
    projects: values<Project>('project'),
97
    agentProfiles: values<AgentProfile>('agent_profile').map((agent) => ({
98
      ...agent,
99
      agentMode: agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat'),
100
      transport: agent.a2aEndpoint ? 'direct_a2a' : (agent.transport ?? 'direct_a2a'),
101
      description: agent.description ?? '',
102
      agentVersion: agent.agentVersion ?? '1.0.0',
103
      autoPublish: agent.autoPublish ?? false,
104
      autoAcceptPolicy: agent.autoAcceptPolicy ?? 'off',
105
      autoAcceptTaskCategories: agent.autoAcceptTaskCategories ?? [],
106
    })),
107
    installations: values<AgentInstallation>('installation'),
108
    setupRequests: values<SetupRequest>('setup_request'),
109
  };
110
}
111
112
export async function resolveInstallation(
113
  store: OnboardingStore,
114
  operatorId: string,
115
  clientId: string,
116
) {
117
  const state = await getOnboardingState(store, operatorId);
118
  const installation = state.installations.find((item) => item.clientId === clientId);
119
  if (!installation) return { status: 'unbound' as const };
120
  const agent = state.agentProfiles.find((item) => item.agentId === installation.agentId);
121
  const project = state.projects.find((item) => item.projectId === installation.projectId);
122
  if (!agent || !project) return { status: 'invalid_binding' as const, installation };
123
  return { status: 'connected' as const, installation, agent, project };
124
}
125
126
export async function createHostedProviderAgent(
127
  store: OnboardingStore,
128
  operatorId: string,
129
  input: {
130
    provider: 'botpress' | 'custom';
131
    agentName: string;
132
    projectName: string;
133
    description?: string | undefined;
134
    capabilities?: string[] | undefined;
135
    limitations?: string[] | undefined;
136
  },
137
) {
138
  const state = await getOnboardingState(store, operatorId);
139
  const projectName = input.projectName.trim();
140
  const agentName = input.agentName.trim();
141
  if (!projectName || !agentName) throw new Error('Agent and project names are required');
142
  const now = new Date().toISOString();
143
  let project = state.projects.find(
144
    (candidate) => candidate.name.trim().toLowerCase() === projectName.toLowerCase(),
145
  );
146
  if (!project) {
147
    project = { projectId: `project_${randomUUID()}`, name: projectName, createdAt: now };
148
    await store.upsert(operatorId, 'project', project.projectId, project);
149
  }
150
  const agent: AgentProfile = {
151
    agentId: `agent_${randomUUID()}`,
152
    projectId: project.projectId,
153
    name: agentName,
154
    description: input.description?.trim() ?? '',
155
    framework: input.provider === 'botpress' ? 'Botpress' : 'Custom runtime',
156
    agentVersion: '1.0.0',
157
    agentMode: 'persistent_runtime',
158
    transport: 'direct_a2a',
159
    autoPublish: false,
160
    autoAcceptPolicy: 'off',
161
    autoAcceptTaskCategories: [],
162
    capabilities: [...new Set(input.capabilities ?? [])],
163
    limitations: [...new Set(input.limitations ?? [])],
164
    identityMode: 'owner_managed',
165
    status: 'active',
166
    createdAt: now,
167
    updatedAt: now,
168
  };
169
  await store.upsert(operatorId, 'agent_profile', agent.agentId, agent);
170
  return { project, agent };
171
}
172
173
export async function createDashboardAgent(
174
  store: OnboardingStore,
175
  operatorId: string,
176
  input: {
177
    agentName: string;
178
    projectName: string;
179
    description: string;
180
    framework?: string | undefined;
181
    capabilities: string[];
182
    limitations?: string[] | undefined;
183
  },
184
) {
185
  const state = await getOnboardingState(store, operatorId);
186
  const projectName = input.projectName.trim();
187
  const agentName = input.agentName.trim();
188
  if (!projectName || !agentName) throw new Error('Agent and project names are required');
189
  const capabilities = [
190
    ...new Set(input.capabilities.map((value) => value.trim()).filter(Boolean)),
191
  ];
192
  if (!capabilities.length) throw new Error('At least one capability is required');
193
  const now = new Date().toISOString();
194
  let project = state.projects.find(
195
    (candidate) => candidate.name.trim().toLowerCase() === projectName.toLowerCase(),
196
  );
197
  if (!project) {
198
    project = { projectId: `project_${randomUUID()}`, name: projectName, createdAt: now };
199
    await store.upsert(operatorId, 'project', project.projectId, project);
200
  }
201
  const existing = state.agentProfiles.find(
202
    (candidate) =>
203
      candidate.status === 'active' &&
204
      candidate.projectId === project!.projectId &&
205
      candidate.name.trim().toLowerCase() === agentName.toLowerCase(),
206
  );
207
  if (existing) return { project, agent: existing };
208
  const agent: AgentProfile = {
209
    agentId: `agent_${randomUUID()}`,
210
    projectId: project.projectId,
211
    name: agentName,
212
    description: input.description.trim(),
213
    framework: input.framework?.trim() || 'Custom agent',
214
    agentVersion: '1.0.0',
215
    agentMode: 'persistent_runtime',
216
    transport: 'direct_a2a',
217
    autoPublish: false,
218
    autoAcceptPolicy: 'off',
219
    autoAcceptTaskCategories: [],
220
    capabilities,
221
    limitations: [
222
      ...new Set((input.limitations ?? []).map((value) => value.trim()).filter(Boolean)),
223
    ],
224
    identityMode: 'owner_managed',
225
    status: 'active',
226
    createdAt: now,
227
    updatedAt: now,
228
  };
229
  await store.upsert(operatorId, 'agent_profile', agent.agentId, agent);
230
  return { project, agent };
231
}
232
233
export async function requestAgentSetup(
234
  store: OnboardingStore,
235
  operatorId: string,
236
  input: {
237
    clientId: string;
238
    action?: 'connect' | 'switch' | undefined;
239
    agentName?: string | undefined;
240
    projectName?: string | undefined;
241
    projectId?: string | undefined;
242
    existingAgentId?: string | undefined;
243
    framework?: string | undefined;
244
    description?: string | undefined;
245
    agentVersion?: string | undefined;
246
    agentMode?: 'persistent_runtime' | 'temporary_chat' | undefined;
247
    a2aEndpoint?: string | undefined;
248
    autoPublish?: boolean | undefined;
249
    autoAcceptPolicy?: 'off' | 'safe_matching' | undefined;
250
    autoAcceptTaskCategories?: string[] | undefined;
251
    capabilities?: string[] | undefined;
252
    limitations?: string[] | undefined;
253
  },
254
): Promise<SetupRequest> {
255
  const state = await getOnboardingState(store, operatorId);
256
  const pending = state.setupRequests.find(
257
    (item) => item.clientId === input.clientId && item.status === 'pending',
258
  );
259
  if (pending) return pending;
260
  if (
261
    !input.existingAgentId &&
262
    (!input.agentName?.trim() || (!input.projectId && !input.projectName?.trim()))
263
  )
264
    throw new Error('agentName and either projectName or projectId are required for a new agent');
265
  if (
266
    input.existingAgentId &&
267
    !state.agentProfiles.some((agent) => agent.agentId === input.existingAgentId)
268
  )
269
    throw new Error('Existing agent not found');
270
  if (input.projectId && !state.projects.some((project) => project.projectId === input.projectId))
271
    throw new Error('Project not found');
272
273
  const request: SetupRequest = {
274
    requestId: randomUUID(),
275
    clientId: input.clientId,
276
    action: input.action ?? (input.existingAgentId ? 'switch' : 'connect'),
277
    status: 'pending',
278
    ...(input.agentName?.trim() ? { agentName: input.agentName.trim() } : {}),
279
    ...(input.projectName?.trim() ? { projectName: input.projectName.trim() } : {}),
280
    ...(input.projectId ? { projectId: input.projectId } : {}),
281
    ...(input.existingAgentId ? { existingAgentId: input.existingAgentId } : {}),
282
    framework: input.framework?.trim() || 'unknown',
283
    description: input.description?.trim() || '',
284
    agentVersion: input.agentVersion?.trim() || '1.0.0',
285
    agentMode: input.agentMode ?? 'temporary_chat',
286
    ...(input.a2aEndpoint?.trim() ? { a2aEndpoint: input.a2aEndpoint.trim() } : {}),
287
    autoPublish: input.autoPublish ?? true,
288
    autoAcceptPolicy: input.autoAcceptPolicy ?? 'safe_matching',
289
    autoAcceptTaskCategories: [
290
      ...new Set(input.autoAcceptTaskCategories ?? input.capabilities ?? []),
291
    ],
292
    capabilities: [...new Set(input.capabilities ?? [])],
293
    limitations: [...new Set(input.limitations ?? [])],
294
    requestedAt: new Date().toISOString(),
295
  };
296
  await store.upsert(operatorId, 'setup_request', request.requestId, request);
297
  return request;
298
}
299
300
export async function approveAgentSetup(
301
  store: OnboardingStore,
302
  operatorId: string,
303
  requestId: string,
304
) {
305
  const state = await getOnboardingState(store, operatorId);
306
  const request = state.setupRequests.find((item) => item.requestId === requestId);
307
  if (!request) throw new Error('Setup request not found');
308
  if (request.status === 'rejected') throw new Error('Setup request was rejected');
309
  if (request.status === 'approved')
310
    return resolveInstallation(store, operatorId, request.clientId);
311
312
  const now = new Date().toISOString();
313
  let agent = request.existingAgentId
314
    ? state.agentProfiles.find((item) => item.agentId === request.existingAgentId)
315
    : undefined;
316
  let project = agent
317
    ? state.projects.find((item) => item.projectId === agent!.projectId)
318
    : request.projectId
319
      ? state.projects.find((item) => item.projectId === request.projectId)
320
      : undefined;
321
322
  if (!project) {
323
    project = {
324
      projectId: `project_${randomUUID()}`,
325
      name: request.projectName!,
326
      createdAt: now,
327
    };
328
    await store.upsert(operatorId, 'project', project.projectId, project);
329
  }
330
  if (!agent) {
331
    agent = {
332
      agentId: `agent_${randomUUID()}`,
333
      projectId: project.projectId,
334
      name: request.agentName!,
335
      description: request.description ?? '',
336
      framework: request.framework,
337
      agentVersion: request.agentVersion,
338
      agentMode: request.agentMode ?? 'temporary_chat',
339
      transport: 'direct_a2a',
340
      autoPublish: request.autoPublish ?? false,
341
      autoAcceptPolicy: request.autoAcceptPolicy ?? 'off',
342
      autoAcceptTaskCategories: request.autoAcceptTaskCategories ?? [],
343
      capabilities: request.capabilities,
344
      limitations: request.limitations,
345
      identityMode: 'oauth_installation',
346
      status: 'active',
347
      createdAt: now,
348
      updatedAt: now,
349
    };
350
    await store.upsert(operatorId, 'agent_profile', agent.agentId, agent);
351
  }
352
353
  const previous = state.installations.find((item) => item.clientId === request.clientId);
354
  const installation: AgentInstallation = {
355
    installationId: previous?.installationId ?? `install_${randomUUID()}`,
356
    clientId: request.clientId,
357
    agentId: agent.agentId,
358
    projectId: project.projectId,
359
    connectedAt: previous?.connectedAt ?? now,
360
    updatedAt: now,
361
  };
362
  await store.upsert(operatorId, 'installation', request.clientId, installation);
363
  await store.upsert(operatorId, 'setup_request', request.requestId, {
364
    ...request,
365
    status: 'approved',
366
    decidedAt: now,
367
    installationId: installation.installationId,
368
  } satisfies SetupRequest);
369
  return { status: 'connected' as const, installation, agent, project };
370
}
371
372
export async function rejectAgentSetup(
373
  store: OnboardingStore,
374
  operatorId: string,
375
  requestId: string,
376
) {
377
  const state = await getOnboardingState(store, operatorId);
378
  const request = state.setupRequests.find((item) => item.requestId === requestId);
379
  if (!request) throw new Error('Setup request not found');
380
  if (request.status !== 'pending') throw new Error('Setup request is no longer pending');
381
  const rejected: SetupRequest = {
382
    ...request,
383
    status: 'rejected',
384
    decidedAt: new Date().toISOString(),
385
  };
386
  await store.upsert(operatorId, 'setup_request', requestId, rejected);
387
  return rejected;
388
}
389
390
export async function updateAgentProfile(
391
  store: OnboardingStore,
392
  operatorId: string,
393
  clientId: string,
394
  patch: {
395
    name?: string | undefined;
396
    description?: string | undefined;
397
    framework?: string | undefined;
398
    agentVersion?: string | undefined;
399
    agentMode?: 'persistent_runtime' | 'temporary_chat' | undefined;
400
    a2aEndpoint?: string | undefined;
401
    autoPublish?: boolean | undefined;
402
    autoAcceptPolicy?: 'off' | 'safe_matching' | undefined;
403
    autoAcceptTaskCategories?: string[] | undefined;
404
    capabilities?: string[] | undefined;
405
    limitations?: string[] | undefined;
406
  },
407
) {
408
  const binding = await resolveInstallation(store, operatorId, clientId);
409
  if (binding.status !== 'connected') throw new Error('This MCP installation is not connected');
410
  const agent: AgentProfile = {
411
    ...binding.agent,
412
    transport: 'direct_a2a',
413
    agentMode:
414
      patch.agentMode ??
415
      binding.agent.agentMode ??
416
      (binding.agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat'),
417
    ...(patch.name?.trim() ? { name: patch.name.trim() } : {}),
418
    ...(patch.description !== undefined ? { description: patch.description.trim() } : {}),
419
    ...(patch.framework?.trim() ? { framework: patch.framework.trim() } : {}),
420
    ...(patch.agentVersion?.trim() ? { agentVersion: patch.agentVersion.trim() } : {}),
421
    ...(patch.autoPublish !== undefined ? { autoPublish: patch.autoPublish } : {}),
422
    ...(patch.autoAcceptPolicy ? { autoAcceptPolicy: patch.autoAcceptPolicy } : {}),
423
    ...(patch.autoAcceptTaskCategories
424
      ? { autoAcceptTaskCategories: [...new Set(patch.autoAcceptTaskCategories)] }
425
      : {}),
426
    ...(patch.capabilities ? { capabilities: [...new Set(patch.capabilities)] } : {}),
427
    ...(patch.limitations ? { limitations: [...new Set(patch.limitations)] } : {}),
428
    updatedAt: new Date().toISOString(),
429
  };
430
  await store.upsert(operatorId, 'agent_profile', agent.agentId, agent);
431
  return agent;
432
}