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

Viewing files at commit 3dc3c2c6da63Commit detailsView latest

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