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

Viewing files at commit 18afdc33bab8Commit detailsView latest

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