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

Viewing files at commit 9a8fdf2387ffCommit detailsView latest

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