multi-agent-protocol 0.0.3 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/docs/00-design-specification.md +36 -0
- package/docs/01-open-questions.md +0 -5
- package/docs/02-wire-protocol.md +1 -1
- package/docs/07-federation.md +81 -5
- package/docs/09-authentication.md +748 -0
- package/docs/10-environment-awareness.md +242 -0
- package/docs/10-mail-protocol.md +553 -0
- package/docs/11-anp-inspired-improvements.md +1079 -0
- package/docs/12-anp-implementation-plan.md +641 -0
- package/docs/agent-iam-integration.md +877 -0
- package/docs/agentic-mesh-integration-draft.md +459 -0
- package/docs/git-transport-draft.md +251 -0
- package/package.json +5 -4
- package/schema/meta.json +200 -2
- package/schema/schema.json +1252 -13
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
# MAP Protocol Integration Design Specification
|
|
2
|
+
|
|
3
|
+
This document specifies how to integrate agent-iam as an IAM provider for the Multi-Agent Protocol (MAP).
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
agent-iam provides capability-based tokens with optional identity binding, federation metadata, and agent capabilities. MAP consumes these tokens through a custom authenticator that maps agent-iam concepts to MAP's native types.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
11
|
+
│ MAP System │
|
|
12
|
+
│ │
|
|
13
|
+
│ ┌──────────────────┐ ┌─────────────────────┐ ┌────────────────┐ │
|
|
14
|
+
│ │ MAP Connection │───▶│ AgentIAMAuthenticator│───▶│ MAP Router │ │
|
|
15
|
+
│ │ (with token) │ │ │ │ │ │
|
|
16
|
+
│ └──────────────────┘ │ - Verify token │ │ - Route msgs │ │
|
|
17
|
+
│ │ - Extract identity │ │ - Enforce │ │
|
|
18
|
+
│ │ - Map capabilities │ │ permissions │ │
|
|
19
|
+
│ └─────────────────────┘ └────────────────┘ │
|
|
20
|
+
│ │ │
|
|
21
|
+
│ ▼ │
|
|
22
|
+
│ ┌─────────────────────┐ │
|
|
23
|
+
│ │ agent-iam │ │
|
|
24
|
+
│ │ TokenService │ │
|
|
25
|
+
│ └─────────────────────┘ │
|
|
26
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 1. Authentication Method
|
|
30
|
+
|
|
31
|
+
### 1.1 Custom Auth Method: `x-agent-iam`
|
|
32
|
+
|
|
33
|
+
Register a custom authentication method for agent-iam tokens:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// map-protocol/ts-sdk/src/server/auth/authenticators/agent-iam.ts
|
|
37
|
+
|
|
38
|
+
import { TokenService, type AgentToken } from 'agent-iam';
|
|
39
|
+
import type { Authenticator, AuthContext, AuthResult } from '../types';
|
|
40
|
+
|
|
41
|
+
export interface AgentIAMAuthenticatorOptions {
|
|
42
|
+
/** The agent-iam TokenService instance */
|
|
43
|
+
tokenService: TokenService;
|
|
44
|
+
|
|
45
|
+
/** System ID for this MAP system (used in identity binding) */
|
|
46
|
+
systemId: string;
|
|
47
|
+
|
|
48
|
+
/** Whether to require identity binding */
|
|
49
|
+
requireIdentity?: boolean;
|
|
50
|
+
|
|
51
|
+
/** Allowed tenant IDs (undefined = all) */
|
|
52
|
+
allowedTenants?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class AgentIAMAuthenticator implements Authenticator {
|
|
56
|
+
readonly methods = ['x-agent-iam'] as const;
|
|
57
|
+
|
|
58
|
+
private tokenService: TokenService;
|
|
59
|
+
private systemId: string;
|
|
60
|
+
private requireIdentity: boolean;
|
|
61
|
+
private allowedTenants?: string[];
|
|
62
|
+
|
|
63
|
+
constructor(options: AgentIAMAuthenticatorOptions) {
|
|
64
|
+
this.tokenService = options.tokenService;
|
|
65
|
+
this.systemId = options.systemId;
|
|
66
|
+
this.requireIdentity = options.requireIdentity ?? false;
|
|
67
|
+
this.allowedTenants = options.allowedTenants;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async authenticate(
|
|
71
|
+
credentials: AuthCredentials,
|
|
72
|
+
context: AuthContext
|
|
73
|
+
): Promise<AuthResult> {
|
|
74
|
+
// Extract token from credentials
|
|
75
|
+
const tokenStr = credentials.credentials?.token;
|
|
76
|
+
if (!tokenStr || typeof tokenStr !== 'string') {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: {
|
|
80
|
+
code: 'invalid_credentials',
|
|
81
|
+
message: 'Missing or invalid agent-iam token',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Deserialize and verify token
|
|
88
|
+
const token = this.tokenService.deserialize(tokenStr);
|
|
89
|
+
const verification = this.tokenService.verify(token);
|
|
90
|
+
|
|
91
|
+
if (!verification.valid) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: {
|
|
95
|
+
code: 'invalid_token',
|
|
96
|
+
message: verification.error ?? 'Token verification failed',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check identity requirement
|
|
102
|
+
if (this.requireIdentity && !token.identity) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: {
|
|
106
|
+
code: 'identity_required',
|
|
107
|
+
message: 'Token must include identity binding',
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check tenant restriction
|
|
113
|
+
if (this.allowedTenants && token.identity?.tenantId) {
|
|
114
|
+
if (!this.allowedTenants.includes(token.identity.tenantId)) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: {
|
|
118
|
+
code: 'tenant_not_allowed',
|
|
119
|
+
message: `Tenant ${token.identity.tenantId} not allowed`,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check federation (if token came from another system)
|
|
126
|
+
if (token.federation && token.identity?.systemId !== this.systemId) {
|
|
127
|
+
if (!token.federation.crossSystemAllowed) {
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
error: {
|
|
131
|
+
code: 'federation_not_allowed',
|
|
132
|
+
message: 'Token does not allow cross-system use',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (token.federation.allowedSystems &&
|
|
138
|
+
!token.federation.allowedSystems.includes(this.systemId)) {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: {
|
|
142
|
+
code: 'system_not_allowed',
|
|
143
|
+
message: `Token not allowed for system ${this.systemId}`,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Build principal from token
|
|
150
|
+
const principal = this.buildPrincipal(token);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
principal,
|
|
155
|
+
// Pass the full token for later use
|
|
156
|
+
metadata: { agentIamToken: token },
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error: {
|
|
162
|
+
code: 'token_parse_error',
|
|
163
|
+
message: error instanceof Error ? error.message : 'Failed to parse token',
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private buildPrincipal(token: AgentToken): AuthPrincipal {
|
|
170
|
+
return {
|
|
171
|
+
id: token.agentId,
|
|
172
|
+
issuer: token.identity?.systemId ?? this.systemId,
|
|
173
|
+
claims: {
|
|
174
|
+
// Core token info
|
|
175
|
+
agentId: token.agentId,
|
|
176
|
+
parentId: token.parentId,
|
|
177
|
+
scopes: token.scopes,
|
|
178
|
+
delegationDepth: token.currentDepth,
|
|
179
|
+
|
|
180
|
+
// Identity binding (if present)
|
|
181
|
+
...(token.identity && {
|
|
182
|
+
principalId: token.identity.principalId,
|
|
183
|
+
principalType: token.identity.principalType,
|
|
184
|
+
tenantId: token.identity.tenantId,
|
|
185
|
+
organizationId: token.identity.organizationId,
|
|
186
|
+
}),
|
|
187
|
+
|
|
188
|
+
// Federation info (if present)
|
|
189
|
+
...(token.federation && {
|
|
190
|
+
federationOrigin: token.federation.originSystem,
|
|
191
|
+
federationHops: token.federation.hopCount,
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
expiresAt: token.expiresAt ? new Date(token.expiresAt).getTime() : undefined,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 1.2 Authentication Flow
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
Client MAP Server agent-iam
|
|
204
|
+
│ │ │
|
|
205
|
+
│── map/connect ────────────────▶│ │
|
|
206
|
+
│ { auth: { │ │
|
|
207
|
+
│ method: "x-agent-iam", │ │
|
|
208
|
+
│ credentials: { │ │
|
|
209
|
+
│ token: "<serialized>" │ │
|
|
210
|
+
│ } │ │
|
|
211
|
+
│ }} │ │
|
|
212
|
+
│ │ │
|
|
213
|
+
│ │── deserialize & verify ──────▶│
|
|
214
|
+
│ │ │
|
|
215
|
+
│ │◀── verification result ───────│
|
|
216
|
+
│ │ │
|
|
217
|
+
│ │── build principal │
|
|
218
|
+
│ │── extract capabilities │
|
|
219
|
+
│ │ │
|
|
220
|
+
│◀── connected ──────────────────│ │
|
|
221
|
+
│ { principal, capabilities } │ │
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## 2. Capability Mapping
|
|
225
|
+
|
|
226
|
+
### 2.1 Scopes to ParticipantCapabilities
|
|
227
|
+
|
|
228
|
+
Map agent-iam scopes to MAP participant capabilities:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// map-protocol/ts-sdk/src/server/auth/agent-iam-mapper.ts
|
|
232
|
+
|
|
233
|
+
import type { AgentToken, AgentCapabilities } from 'agent-iam';
|
|
234
|
+
import type { ParticipantCapabilities, AgentPermissions } from '../../types';
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Configuration for mapping agent-iam tokens to MAP capabilities
|
|
238
|
+
*/
|
|
239
|
+
export interface CapabilityMapperConfig {
|
|
240
|
+
/** Map agent-iam scopes to MAP capabilities */
|
|
241
|
+
scopeMappings?: {
|
|
242
|
+
/** Scopes that grant observation capability */
|
|
243
|
+
observation?: string[];
|
|
244
|
+
/** Scopes that grant messaging capability */
|
|
245
|
+
messaging?: string[];
|
|
246
|
+
/** Scopes that grant lifecycle (spawn/register) capability */
|
|
247
|
+
lifecycle?: string[];
|
|
248
|
+
/** Scopes that grant scope management capability */
|
|
249
|
+
scopes?: string[];
|
|
250
|
+
/** Scopes that grant federation capability */
|
|
251
|
+
federation?: string[];
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/** Default capabilities when no mappings match */
|
|
255
|
+
defaults?: Partial<ParticipantCapabilities>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const DEFAULT_SCOPE_MAPPINGS = {
|
|
259
|
+
observation: ['map:observe:*', 'map:*'],
|
|
260
|
+
messaging: ['map:message:*', 'map:*'],
|
|
261
|
+
lifecycle: ['map:lifecycle:*', 'map:agent:*', 'map:*'],
|
|
262
|
+
scopes: ['map:scope:*', 'map:*'],
|
|
263
|
+
federation: ['map:federation:*', 'map:*'],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export class AgentIAMCapabilityMapper {
|
|
267
|
+
private scopeMappings: Required<CapabilityMapperConfig['scopeMappings']>;
|
|
268
|
+
private defaults: Partial<ParticipantCapabilities>;
|
|
269
|
+
|
|
270
|
+
constructor(config?: CapabilityMapperConfig) {
|
|
271
|
+
this.scopeMappings = {
|
|
272
|
+
...DEFAULT_SCOPE_MAPPINGS,
|
|
273
|
+
...config?.scopeMappings,
|
|
274
|
+
};
|
|
275
|
+
this.defaults = config?.defaults ?? {};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Map an agent-iam token to MAP ParticipantCapabilities
|
|
280
|
+
*/
|
|
281
|
+
mapToParticipantCapabilities(token: AgentToken): ParticipantCapabilities {
|
|
282
|
+
const caps = token.agentCapabilities;
|
|
283
|
+
const scopes = token.scopes;
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
observation: this.mapObservation(scopes, caps),
|
|
287
|
+
messaging: this.mapMessaging(scopes, caps),
|
|
288
|
+
lifecycle: this.mapLifecycle(scopes, caps),
|
|
289
|
+
scopes: this.mapScopes(scopes, caps),
|
|
290
|
+
federation: this.mapFederation(scopes, caps, token.federation),
|
|
291
|
+
...this.defaults,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Map an agent-iam token to MAP AgentPermissions
|
|
297
|
+
*/
|
|
298
|
+
mapToAgentPermissions(token: AgentToken): AgentPermissions {
|
|
299
|
+
const caps = token.agentCapabilities;
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
canSee: this.mapVisibility(caps),
|
|
303
|
+
canMessage: this.mapMessagePermissions(caps),
|
|
304
|
+
acceptsFrom: this.mapAcceptsFrom(caps),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private hasScope(scopes: string[], patterns: string[]): boolean {
|
|
309
|
+
return patterns.some(pattern =>
|
|
310
|
+
scopes.some(scope => this.scopeMatches(pattern, scope))
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private scopeMatches(pattern: string, scope: string): boolean {
|
|
315
|
+
if (pattern === scope) return true;
|
|
316
|
+
if (pattern === '*') return true;
|
|
317
|
+
if (pattern.endsWith(':*')) {
|
|
318
|
+
return scope.startsWith(pattern.slice(0, -1));
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private mapObservation(
|
|
324
|
+
scopes: string[],
|
|
325
|
+
caps?: AgentCapabilities
|
|
326
|
+
): ParticipantCapabilities['observation'] {
|
|
327
|
+
const canObserve = caps?.canObserve ??
|
|
328
|
+
this.hasScope(scopes, this.scopeMappings.observation);
|
|
329
|
+
return {
|
|
330
|
+
canObserve,
|
|
331
|
+
canQuery: canObserve,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private mapMessaging(
|
|
336
|
+
scopes: string[],
|
|
337
|
+
caps?: AgentCapabilities
|
|
338
|
+
): ParticipantCapabilities['messaging'] {
|
|
339
|
+
const hasMessagingScope = this.hasScope(scopes, this.scopeMappings.messaging);
|
|
340
|
+
return {
|
|
341
|
+
canSend: caps?.canMessage ?? hasMessagingScope,
|
|
342
|
+
canReceive: caps?.canReceive ?? hasMessagingScope,
|
|
343
|
+
canBroadcast: caps?.canMessage ?? hasMessagingScope,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private mapLifecycle(
|
|
348
|
+
scopes: string[],
|
|
349
|
+
caps?: AgentCapabilities
|
|
350
|
+
): ParticipantCapabilities['lifecycle'] {
|
|
351
|
+
const hasLifecycleScope = this.hasScope(scopes, this.scopeMappings.lifecycle);
|
|
352
|
+
return {
|
|
353
|
+
canSpawn: caps?.canSpawn ?? hasLifecycleScope,
|
|
354
|
+
canRegister: hasLifecycleScope,
|
|
355
|
+
canUnregister: hasLifecycleScope,
|
|
356
|
+
canSteer: hasLifecycleScope,
|
|
357
|
+
canStop: hasLifecycleScope,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private mapScopes(
|
|
362
|
+
scopes: string[],
|
|
363
|
+
caps?: AgentCapabilities
|
|
364
|
+
): ParticipantCapabilities['scopes'] {
|
|
365
|
+
const hasScopesScope = this.hasScope(scopes, this.scopeMappings.scopes);
|
|
366
|
+
return {
|
|
367
|
+
canCreateScopes: caps?.canCreateScopes ?? hasScopesScope,
|
|
368
|
+
canManageScopes: hasScopesScope,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private mapFederation(
|
|
373
|
+
scopes: string[],
|
|
374
|
+
caps?: AgentCapabilities,
|
|
375
|
+
federation?: AgentToken['federation']
|
|
376
|
+
): ParticipantCapabilities['federation'] {
|
|
377
|
+
const hasFederationScope = this.hasScope(scopes, this.scopeMappings.federation);
|
|
378
|
+
const canFederate = (caps?.canFederate ?? hasFederationScope) &&
|
|
379
|
+
(federation?.crossSystemAllowed ?? true);
|
|
380
|
+
return {
|
|
381
|
+
canFederate,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private mapVisibility(
|
|
386
|
+
caps?: AgentCapabilities
|
|
387
|
+
): AgentPermissions['canSee'] {
|
|
388
|
+
// Map agent-iam visibility to MAP canSee
|
|
389
|
+
const visibility = caps?.visibility ?? 'public';
|
|
390
|
+
|
|
391
|
+
switch (visibility) {
|
|
392
|
+
case 'public':
|
|
393
|
+
return { agents: 'all', scopes: 'all', structure: 'full' };
|
|
394
|
+
case 'scope':
|
|
395
|
+
return { agents: 'scoped', scopes: 'member', structure: 'local' };
|
|
396
|
+
case 'parent-only':
|
|
397
|
+
return { agents: 'hierarchy', scopes: 'member', structure: 'local' };
|
|
398
|
+
case 'system':
|
|
399
|
+
return { agents: 'direct', scopes: 'member', structure: 'none' };
|
|
400
|
+
default:
|
|
401
|
+
return { agents: 'all', scopes: 'all', structure: 'full' };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private mapMessagePermissions(
|
|
406
|
+
caps?: AgentCapabilities
|
|
407
|
+
): AgentPermissions['canMessage'] {
|
|
408
|
+
if (caps?.canMessage === false) {
|
|
409
|
+
return { agents: 'direct', scopes: 'member' };
|
|
410
|
+
}
|
|
411
|
+
return { agents: 'all', scopes: 'all' };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private mapAcceptsFrom(
|
|
415
|
+
caps?: AgentCapabilities
|
|
416
|
+
): AgentPermissions['acceptsFrom'] {
|
|
417
|
+
if (caps?.canReceive === false) {
|
|
418
|
+
return { agents: 'hierarchy', clients: 'none', systems: 'none' };
|
|
419
|
+
}
|
|
420
|
+
return { agents: 'all', clients: 'all', systems: 'all' };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### 2.2 Integration with Connection Handler
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// map-protocol/ts-sdk/src/server/handlers/connection.ts
|
|
429
|
+
|
|
430
|
+
import { AgentIAMAuthenticator } from '../auth/authenticators/agent-iam';
|
|
431
|
+
import { AgentIAMCapabilityMapper } from '../auth/agent-iam-mapper';
|
|
432
|
+
import type { AgentToken } from 'agent-iam';
|
|
433
|
+
|
|
434
|
+
export function createConnectionHandler(
|
|
435
|
+
options: ConnectionHandlerOptions & {
|
|
436
|
+
agentIamMapper?: AgentIAMCapabilityMapper;
|
|
437
|
+
}
|
|
438
|
+
) {
|
|
439
|
+
const mapper = options.agentIamMapper ?? new AgentIAMCapabilityMapper();
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
'map/connect': async (params, context) => {
|
|
443
|
+
// ... authentication happens via AuthManager ...
|
|
444
|
+
|
|
445
|
+
// After successful auth, extract agent-iam token from metadata
|
|
446
|
+
const agentIamToken = context.authResult?.metadata?.agentIamToken as AgentToken | undefined;
|
|
447
|
+
|
|
448
|
+
if (agentIamToken) {
|
|
449
|
+
// Map to MAP capabilities
|
|
450
|
+
const participantCapabilities = mapper.mapToParticipantCapabilities(agentIamToken);
|
|
451
|
+
const agentPermissions = mapper.mapToAgentPermissions(agentIamToken);
|
|
452
|
+
|
|
453
|
+
// Use these capabilities for the connection
|
|
454
|
+
context.capabilities = participantCapabilities;
|
|
455
|
+
context.defaultAgentPermissions = agentPermissions;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ... rest of connection handling ...
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## 3. Agent Spawn Integration
|
|
465
|
+
|
|
466
|
+
### 3.1 Token Delegation on Spawn
|
|
467
|
+
|
|
468
|
+
When an agent spawns a child, delegate the agent-iam token:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// map-protocol/ts-sdk/src/server/handlers/lifecycle.ts
|
|
472
|
+
|
|
473
|
+
import { TokenService, type DelegationRequest } from 'agent-iam';
|
|
474
|
+
|
|
475
|
+
export function createLifecycleHandler(
|
|
476
|
+
options: LifecycleHandlerOptions & {
|
|
477
|
+
tokenService?: TokenService;
|
|
478
|
+
}
|
|
479
|
+
) {
|
|
480
|
+
return {
|
|
481
|
+
'map/agents/spawn': async (params, context) => {
|
|
482
|
+
const parentToken = context.authResult?.metadata?.agentIamToken;
|
|
483
|
+
|
|
484
|
+
if (parentToken && options.tokenService) {
|
|
485
|
+
// Build delegation request from spawn params
|
|
486
|
+
const delegationRequest: DelegationRequest = {
|
|
487
|
+
agentId: params.agentId,
|
|
488
|
+
requestedScopes: params.requestedScopes ?? parentToken.scopes,
|
|
489
|
+
ttlMinutes: params.ttlMinutes,
|
|
490
|
+
|
|
491
|
+
// Map spawn params to agent capabilities
|
|
492
|
+
agentCapabilities: params.capabilities ? {
|
|
493
|
+
canSpawn: params.capabilities.canSpawn,
|
|
494
|
+
canMessage: params.capabilities.canSend,
|
|
495
|
+
canReceive: params.capabilities.canReceive,
|
|
496
|
+
visibility: params.visibility,
|
|
497
|
+
} : undefined,
|
|
498
|
+
|
|
499
|
+
// Inherit identity by default
|
|
500
|
+
inheritIdentity: params.inheritIdentity ?? true,
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Delegate token for child
|
|
504
|
+
const childToken = options.tokenService.delegate(parentToken, delegationRequest);
|
|
505
|
+
|
|
506
|
+
// Pass to spawned agent
|
|
507
|
+
context.childToken = childToken;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ... spawn the agent ...
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### 3.2 Token Passing to Subprocess Agents
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// map-protocol/ts-sdk/src/server/subprocess-spawner.ts
|
|
520
|
+
|
|
521
|
+
import { AGENT_TOKEN_ENV } from 'agent-iam';
|
|
522
|
+
|
|
523
|
+
export class SubprocessSpawner {
|
|
524
|
+
spawn(command: string[], options: SpawnOptions & { childToken?: AgentToken }) {
|
|
525
|
+
const env = {
|
|
526
|
+
...process.env,
|
|
527
|
+
...options.env,
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// Pass delegated token via environment
|
|
531
|
+
if (options.childToken) {
|
|
532
|
+
env[AGENT_TOKEN_ENV] = this.tokenService.serialize(options.childToken);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return spawn(command[0], command.slice(1), { env });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
## 4. Federation Gateway
|
|
541
|
+
|
|
542
|
+
### 4.1 Cross-System Token Handling
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
// map-protocol/ts-sdk/src/federation/agent-iam-gateway.ts
|
|
546
|
+
|
|
547
|
+
import { TokenService, type AgentToken } from 'agent-iam';
|
|
548
|
+
|
|
549
|
+
export interface FederationGatewayConfig {
|
|
550
|
+
/** This system's ID */
|
|
551
|
+
systemId: string;
|
|
552
|
+
|
|
553
|
+
/** Token service for this system */
|
|
554
|
+
tokenService: TokenService;
|
|
555
|
+
|
|
556
|
+
/** Trusted peer systems and their public keys */
|
|
557
|
+
trustedPeers: {
|
|
558
|
+
[systemId: string]: {
|
|
559
|
+
/** Token service for verifying their tokens */
|
|
560
|
+
tokenService: TokenService;
|
|
561
|
+
/** Scope mapping (their scopes → our scopes) */
|
|
562
|
+
scopeMapping?: Record<string, string | null>;
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export class AgentIAMFederationGateway {
|
|
568
|
+
constructor(private config: FederationGatewayConfig) {}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Handle incoming federated token from another system
|
|
572
|
+
*/
|
|
573
|
+
async handleIncomingToken(
|
|
574
|
+
sourceSystemId: string,
|
|
575
|
+
incomingToken: AgentToken
|
|
576
|
+
): Promise<{ localToken: AgentToken; allowed: boolean; reason?: string }> {
|
|
577
|
+
const peer = this.config.trustedPeers[sourceSystemId];
|
|
578
|
+
|
|
579
|
+
if (!peer) {
|
|
580
|
+
return { localToken: incomingToken, allowed: false, reason: 'Unknown peer system' };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Verify with peer's token service
|
|
584
|
+
const verification = peer.tokenService.verify(incomingToken);
|
|
585
|
+
if (!verification.valid) {
|
|
586
|
+
return { localToken: incomingToken, allowed: false, reason: verification.error };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Check federation metadata
|
|
590
|
+
if (!incomingToken.federation?.crossSystemAllowed) {
|
|
591
|
+
return { localToken: incomingToken, allowed: false, reason: 'Token does not allow federation' };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (incomingToken.federation.allowedSystems &&
|
|
595
|
+
!incomingToken.federation.allowedSystems.includes(this.config.systemId)) {
|
|
596
|
+
return { localToken: incomingToken, allowed: false, reason: 'System not in allowed list' };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Check hop count
|
|
600
|
+
const hopCount = (incomingToken.federation.hopCount ?? 0) + 1;
|
|
601
|
+
if (hopCount > (incomingToken.federation.maxHops ?? 3)) {
|
|
602
|
+
return { localToken: incomingToken, allowed: false, reason: 'Max hops exceeded' };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Translate scopes if mapping configured
|
|
606
|
+
const translatedScopes = this.translateScopes(
|
|
607
|
+
incomingToken.scopes,
|
|
608
|
+
peer.scopeMapping
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
// Create local token with federated identity
|
|
612
|
+
const localToken = this.config.tokenService.createRootToken({
|
|
613
|
+
agentId: `federated:${sourceSystemId}:${incomingToken.agentId}`,
|
|
614
|
+
scopes: translatedScopes,
|
|
615
|
+
constraints: incomingToken.constraints,
|
|
616
|
+
delegatable: incomingToken.delegatable &&
|
|
617
|
+
(incomingToken.federation.allowFurtherFederation ?? true),
|
|
618
|
+
maxDelegationDepth: Math.min(incomingToken.maxDelegationDepth, 2),
|
|
619
|
+
ttlDays: 1, // Short TTL for federated tokens
|
|
620
|
+
|
|
621
|
+
// Preserve identity with federation info
|
|
622
|
+
identity: {
|
|
623
|
+
systemId: this.config.systemId,
|
|
624
|
+
principalId: incomingToken.identity?.principalId
|
|
625
|
+
? `federated:${sourceSystemId}:${incomingToken.identity.principalId}`
|
|
626
|
+
: undefined,
|
|
627
|
+
principalType: incomingToken.identity?.principalType,
|
|
628
|
+
tenantId: incomingToken.identity?.tenantId,
|
|
629
|
+
federatedFrom: {
|
|
630
|
+
sourceOrganization: sourceSystemId,
|
|
631
|
+
originalPrincipalId: incomingToken.identity?.principalId ?? incomingToken.agentId,
|
|
632
|
+
originalSystemId: incomingToken.federation.originSystem ?? sourceSystemId,
|
|
633
|
+
federatedAt: new Date().toISOString(),
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
// Update federation metadata
|
|
638
|
+
federation: {
|
|
639
|
+
crossSystemAllowed: incomingToken.federation.allowFurtherFederation ?? false,
|
|
640
|
+
originSystem: incomingToken.federation.originSystem ?? sourceSystemId,
|
|
641
|
+
hopCount,
|
|
642
|
+
maxHops: incomingToken.federation.maxHops,
|
|
643
|
+
allowFurtherFederation: false, // Don't allow further federation by default
|
|
644
|
+
},
|
|
645
|
+
|
|
646
|
+
// Preserve capabilities with attenuation
|
|
647
|
+
agentCapabilities: incomingToken.agentCapabilities ? {
|
|
648
|
+
...incomingToken.agentCapabilities,
|
|
649
|
+
canFederate: false, // Federated tokens can't federate further
|
|
650
|
+
} : undefined,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
return { localToken, allowed: true };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Prepare a token for sending to another system
|
|
658
|
+
*/
|
|
659
|
+
prepareOutgoingToken(
|
|
660
|
+
token: AgentToken,
|
|
661
|
+
targetSystemId: string
|
|
662
|
+
): { serialized: string; allowed: boolean; reason?: string } {
|
|
663
|
+
if (!token.federation?.crossSystemAllowed) {
|
|
664
|
+
return { serialized: '', allowed: false, reason: 'Token does not allow federation' };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (token.federation.allowedSystems &&
|
|
668
|
+
!token.federation.allowedSystems.includes(targetSystemId)) {
|
|
669
|
+
return { serialized: '', allowed: false, reason: 'Target system not allowed' };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
serialized: this.config.tokenService.serialize(token),
|
|
674
|
+
allowed: true,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private translateScopes(
|
|
679
|
+
scopes: string[],
|
|
680
|
+
mapping?: Record<string, string | null>
|
|
681
|
+
): string[] {
|
|
682
|
+
if (!mapping) return scopes;
|
|
683
|
+
|
|
684
|
+
return scopes
|
|
685
|
+
.map(scope => {
|
|
686
|
+
const mapped = mapping[scope];
|
|
687
|
+
if (mapped === null) return null; // Blocked scope
|
|
688
|
+
return mapped ?? scope; // Use mapping or original
|
|
689
|
+
})
|
|
690
|
+
.filter((s): s is string => s !== null);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
## 5. Server Setup
|
|
696
|
+
|
|
697
|
+
### 5.1 Complete Server Configuration
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
// Example: Setting up a MAP server with agent-iam authentication
|
|
701
|
+
|
|
702
|
+
import { TokenService, generateSecret, Broker } from 'agent-iam';
|
|
703
|
+
import { createMAPServer } from 'map-protocol/server';
|
|
704
|
+
import { AgentIAMAuthenticator } from 'map-protocol/server/auth/authenticators/agent-iam';
|
|
705
|
+
import { AgentIAMCapabilityMapper } from 'map-protocol/server/auth/agent-iam-mapper';
|
|
706
|
+
|
|
707
|
+
// Initialize agent-iam
|
|
708
|
+
const secret = generateSecret(); // Or load from config
|
|
709
|
+
const tokenService = new TokenService(secret);
|
|
710
|
+
const broker = new Broker();
|
|
711
|
+
|
|
712
|
+
// Create the authenticator
|
|
713
|
+
const agentIamAuth = new AgentIAMAuthenticator({
|
|
714
|
+
tokenService,
|
|
715
|
+
systemId: 'my-map-system',
|
|
716
|
+
requireIdentity: true, // Require identity for audit
|
|
717
|
+
allowedTenants: ['acme-corp', 'partner-inc'], // Multi-tenant
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Create the capability mapper
|
|
721
|
+
const capabilityMapper = new AgentIAMCapabilityMapper({
|
|
722
|
+
scopeMappings: {
|
|
723
|
+
observation: ['map:observe:*', 'system:*'],
|
|
724
|
+
messaging: ['map:message:*', 'agent:*'],
|
|
725
|
+
lifecycle: ['map:lifecycle:*', 'agent:spawn:*'],
|
|
726
|
+
},
|
|
727
|
+
defaults: {
|
|
728
|
+
streaming: {
|
|
729
|
+
supportsAck: true,
|
|
730
|
+
supportsFlowControl: false,
|
|
731
|
+
supportsPause: false,
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Create the server
|
|
737
|
+
const server = createMAPServer({
|
|
738
|
+
port: 8080,
|
|
739
|
+
|
|
740
|
+
auth: {
|
|
741
|
+
required: true,
|
|
742
|
+
authenticators: [agentIamAuth],
|
|
743
|
+
bypassForTransports: { stdio: true }, // Trust local subprocesses
|
|
744
|
+
},
|
|
745
|
+
|
|
746
|
+
agentIamMapper: capabilityMapper,
|
|
747
|
+
tokenService,
|
|
748
|
+
|
|
749
|
+
// Federation config
|
|
750
|
+
federation: {
|
|
751
|
+
enabled: true,
|
|
752
|
+
systemId: 'my-map-system',
|
|
753
|
+
trustedPeers: {
|
|
754
|
+
'partner-system': {
|
|
755
|
+
publicKey: '...', // For verifying their tokens
|
|
756
|
+
scopeMapping: {
|
|
757
|
+
'partner:resource:read': 'shared:resource:read',
|
|
758
|
+
'partner:admin:*': null, // Block admin scopes
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
await server.start();
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### 5.2 Client Connection with agent-iam Token
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
// Example: Connecting to MAP server with agent-iam token
|
|
772
|
+
|
|
773
|
+
import { Broker } from 'agent-iam';
|
|
774
|
+
import { createMAPClient } from 'map-protocol/client';
|
|
775
|
+
|
|
776
|
+
// Get or create a token
|
|
777
|
+
const broker = new Broker();
|
|
778
|
+
const token = broker.createRootToken({
|
|
779
|
+
agentId: 'my-agent',
|
|
780
|
+
scopes: ['map:*', 'github:repo:read'],
|
|
781
|
+
identity: {
|
|
782
|
+
systemId: 'my-map-system',
|
|
783
|
+
principalId: 'user@acme-corp.com',
|
|
784
|
+
principalType: 'human',
|
|
785
|
+
tenantId: 'acme-corp',
|
|
786
|
+
},
|
|
787
|
+
federation: {
|
|
788
|
+
crossSystemAllowed: true,
|
|
789
|
+
maxHops: 2,
|
|
790
|
+
},
|
|
791
|
+
agentCapabilities: {
|
|
792
|
+
canSpawn: true,
|
|
793
|
+
canMessage: true,
|
|
794
|
+
canReceive: true,
|
|
795
|
+
visibility: 'public',
|
|
796
|
+
},
|
|
797
|
+
ttlDays: 1,
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Connect to MAP server
|
|
801
|
+
const client = await createMAPClient({
|
|
802
|
+
url: 'ws://localhost:8080',
|
|
803
|
+
auth: {
|
|
804
|
+
method: 'x-agent-iam',
|
|
805
|
+
credentials: {
|
|
806
|
+
token: broker.serializeToken(token),
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// Now use the client...
|
|
812
|
+
await client.send({
|
|
813
|
+
to: { agent: 'other-agent' },
|
|
814
|
+
payload: { type: 'hello' },
|
|
815
|
+
});
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
## 6. Type Mappings Summary
|
|
819
|
+
|
|
820
|
+
| agent-iam | MAP | Notes |
|
|
821
|
+
|-----------|-----|-------|
|
|
822
|
+
| `AgentToken.agentId` | `AuthPrincipal.id` | Direct mapping |
|
|
823
|
+
| `AgentToken.scopes` | `ParticipantCapabilities.*` | Via mapper config |
|
|
824
|
+
| `AgentToken.identity.principalId` | `AuthPrincipal.claims.principalId` | In claims |
|
|
825
|
+
| `AgentToken.identity.systemId` | `AuthPrincipal.issuer` | Token issuer |
|
|
826
|
+
| `AgentToken.identity.tenantId` | `AuthPrincipal.claims.tenantId` | For multi-tenant |
|
|
827
|
+
| `AgentToken.agentCapabilities.canSpawn` | `ParticipantCapabilities.lifecycle.canSpawn` | Direct |
|
|
828
|
+
| `AgentToken.agentCapabilities.canMessage` | `ParticipantCapabilities.messaging.canSend` | Direct |
|
|
829
|
+
| `AgentToken.agentCapabilities.visibility` | `AgentPermissions.canSee` | Mapped |
|
|
830
|
+
| `AgentToken.federation.crossSystemAllowed` | Gateway routing decision | Federation control |
|
|
831
|
+
| `AgentToken.expiresAt` | `AuthPrincipal.expiresAt` | Token expiry |
|
|
832
|
+
|
|
833
|
+
## 7. Security Considerations
|
|
834
|
+
|
|
835
|
+
### 7.1 Token Validation
|
|
836
|
+
|
|
837
|
+
- Always verify token signature before trusting any claims
|
|
838
|
+
- Check token expiration before each operation
|
|
839
|
+
- Validate scopes against requested operations
|
|
840
|
+
- Verify federation metadata for cross-system requests
|
|
841
|
+
|
|
842
|
+
### 7.2 Federation Security
|
|
843
|
+
|
|
844
|
+
- Maintain separate signing keys per system (never share)
|
|
845
|
+
- Use scope mapping to restrict federated tokens
|
|
846
|
+
- Limit hop count to prevent routing loops
|
|
847
|
+
- Log all federation events for audit
|
|
848
|
+
|
|
849
|
+
### 7.3 Identity Binding
|
|
850
|
+
|
|
851
|
+
- Require identity for audit-sensitive operations
|
|
852
|
+
- Preserve identity through delegation chain
|
|
853
|
+
- Include external auth info when available
|
|
854
|
+
- Log principal ID with all security-relevant events
|
|
855
|
+
|
|
856
|
+
## 8. Implementation Checklist
|
|
857
|
+
|
|
858
|
+
### MAP Server Side
|
|
859
|
+
|
|
860
|
+
- [ ] Implement `AgentIAMAuthenticator`
|
|
861
|
+
- [ ] Implement `AgentIAMCapabilityMapper`
|
|
862
|
+
- [ ] Update connection handler to use mapper
|
|
863
|
+
- [ ] Update spawn handler for token delegation
|
|
864
|
+
- [ ] Implement federation gateway
|
|
865
|
+
- [ ] Add configuration options
|
|
866
|
+
- [ ] Write tests
|
|
867
|
+
|
|
868
|
+
### Integration Tests
|
|
869
|
+
|
|
870
|
+
- [ ] Token verification flow
|
|
871
|
+
- [ ] Capability mapping accuracy
|
|
872
|
+
- [ ] Delegation on spawn
|
|
873
|
+
- [ ] Federation gateway (same trust domain)
|
|
874
|
+
- [ ] Federation gateway (cross-org)
|
|
875
|
+
- [ ] Token expiration handling
|
|
876
|
+
- [ ] Identity inheritance
|
|
877
|
+
|