agent-inbox 0.1.7 → 0.1.9
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/CLAUDE.md +44 -7
- package/README.md +67 -24
- package/dist/federation/connection-manager.d.ts +13 -2
- package/dist/federation/connection-manager.d.ts.map +1 -1
- package/dist/federation/connection-manager.js +109 -10
- package/dist/federation/connection-manager.js.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ipc/ipc-server.d.ts +2 -0
- package/dist/ipc/ipc-server.d.ts.map +1 -1
- package/dist/ipc/ipc-server.js +48 -0
- package/dist/ipc/ipc-server.js.map +1 -1
- package/dist/map/map-client.d.ts +100 -0
- package/dist/map/map-client.d.ts.map +1 -1
- package/dist/map/map-client.js +61 -0
- package/dist/map/map-client.js.map +1 -1
- package/dist/mcp/mcp-proxy.d.ts +28 -0
- package/dist/mcp/mcp-proxy.d.ts.map +1 -0
- package/dist/mcp/mcp-proxy.js +280 -0
- package/dist/mcp/mcp-proxy.js.map +1 -0
- package/dist/mesh/delivery-bridge.d.ts +47 -0
- package/dist/mesh/delivery-bridge.d.ts.map +1 -0
- package/dist/mesh/delivery-bridge.js +73 -0
- package/dist/mesh/delivery-bridge.js.map +1 -0
- package/dist/mesh/mesh-connector.d.ts +29 -0
- package/dist/mesh/mesh-connector.d.ts.map +1 -0
- package/dist/mesh/mesh-connector.js +36 -0
- package/dist/mesh/mesh-connector.js.map +1 -0
- package/dist/mesh/mesh-transport.d.ts +70 -0
- package/dist/mesh/mesh-transport.d.ts.map +1 -0
- package/dist/mesh/mesh-transport.js +92 -0
- package/dist/mesh/mesh-transport.js.map +1 -0
- package/dist/mesh/type-mapper.d.ts +67 -0
- package/dist/mesh/type-mapper.d.ts.map +1 -0
- package/dist/mesh/type-mapper.js +165 -0
- package/dist/mesh/type-mapper.js.map +1 -0
- package/dist/types.d.ts +29 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
- package/package.json +7 -2
- package/src/federation/connection-manager.ts +125 -10
- package/src/index.ts +96 -5
- package/src/ipc/ipc-server.ts +58 -0
- package/src/map/map-client.ts +152 -0
- package/src/mcp/mcp-proxy.ts +326 -0
- package/src/mesh/delivery-bridge.ts +110 -0
- package/src/mesh/mesh-connector.ts +41 -0
- package/src/mesh/mesh-transport.ts +157 -0
- package/src/mesh/type-mapper.ts +239 -0
- package/src/types.ts +33 -1
- package/test/federation/integration.test.ts +37 -3
- package/test/federation/sdk-integration.test.ts +4 -8
- package/test/ipc-new-commands.test.ts +200 -0
- package/test/mcp-proxy.test.ts +191 -0
- package/test/mesh/delivery-bridge.test.ts +178 -0
- package/test/mesh/e2e-mesh.test.ts +527 -0
- package/test/mesh/e2e-real-meshpeer.test.ts +629 -0
- package/test/mesh/federation-mesh.test.ts +269 -0
- package/test/mesh/mesh-connector.test.ts +66 -0
- package/test/mesh/mesh-transport.test.ts +191 -0
- package/test/mesh/meshpeer-integration.test.ts +442 -0
- package/test/mesh/mock-mesh.ts +125 -0
- package/test/mesh/mock-meshpeer.ts +266 -0
- package/test/mesh/type-mapper.test.ts +226 -0
- package/docs/PLAN.md +0 -545
|
@@ -15,7 +15,11 @@ import type {
|
|
|
15
15
|
MapConnection,
|
|
16
16
|
MapAgentConnectionClass,
|
|
17
17
|
IncomingMapMessage,
|
|
18
|
+
MeshPeerLike,
|
|
19
|
+
MeshFederationGateway,
|
|
18
20
|
} from "../map/map-client.js";
|
|
21
|
+
import type { MeshConnector } from "../mesh/mesh-connector.js";
|
|
22
|
+
import { inboxMessageToMap } from "../mesh/type-mapper.js";
|
|
19
23
|
import { RoutingEngine } from "./routing-engine.js";
|
|
20
24
|
import { DeliveryQueue } from "./delivery-queue.js";
|
|
21
25
|
import { TrustManager } from "./trust.js";
|
|
@@ -49,8 +53,11 @@ export type IncomingMessageHandler = (incoming: {
|
|
|
49
53
|
export class ConnectionManager {
|
|
50
54
|
private peers = new Map<string, FederationLink>();
|
|
51
55
|
private connections = new Map<string, MapConnection>();
|
|
56
|
+
private gateways = new Map<string, MeshFederationGateway>();
|
|
52
57
|
private systemId: SystemId;
|
|
53
58
|
private sdkClass: MapAgentConnectionClass | null;
|
|
59
|
+
private meshConnector: MeshConnector | null;
|
|
60
|
+
private meshPeer: MeshPeerLike | null;
|
|
54
61
|
private onIncoming: IncomingMessageHandler | null;
|
|
55
62
|
readonly routing: RoutingEngine;
|
|
56
63
|
readonly queue: DeliveryQueue;
|
|
@@ -61,6 +68,9 @@ export class ConnectionManager {
|
|
|
61
68
|
private config: FederationConfig = {},
|
|
62
69
|
opts?: {
|
|
63
70
|
sdkClass?: MapAgentConnectionClass;
|
|
71
|
+
meshConnector?: MeshConnector;
|
|
72
|
+
/** Full MeshPeer for Phase 2 FederationGateway delegation */
|
|
73
|
+
meshPeer?: MeshPeerLike;
|
|
64
74
|
onIncomingMessage?: IncomingMessageHandler;
|
|
65
75
|
}
|
|
66
76
|
) {
|
|
@@ -69,6 +79,8 @@ export class ConnectionManager {
|
|
|
69
79
|
this.queue = new DeliveryQueue(events, config.deliveryQueue);
|
|
70
80
|
this.trust = new TrustManager(config.trust);
|
|
71
81
|
this.sdkClass = opts?.sdkClass ?? null;
|
|
82
|
+
this.meshConnector = opts?.meshConnector ?? null;
|
|
83
|
+
this.meshPeer = opts?.meshPeer ?? null;
|
|
72
84
|
this.onIncoming = opts?.onIncomingMessage ?? null;
|
|
73
85
|
|
|
74
86
|
// Wire up flush-on-reconnect
|
|
@@ -104,6 +116,12 @@ export class ConnectionManager {
|
|
|
104
116
|
* Returns the federation link, or throws if trust check fails.
|
|
105
117
|
*/
|
|
106
118
|
async federate(peer: FederationPeerConfig): Promise<FederationLink> {
|
|
119
|
+
if (!peer.url && !peer.meshPeerId) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Federation peer "${peer.systemId}" must have either url or meshPeerId`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
107
125
|
// Trust check
|
|
108
126
|
if (!this.trust.canConnect(peer.systemId)) {
|
|
109
127
|
throw new Error(
|
|
@@ -111,9 +129,62 @@ export class ConnectionManager {
|
|
|
111
129
|
);
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
// Open real MAP connection if SDK is available
|
|
115
132
|
let conn: MapConnection | undefined;
|
|
116
|
-
|
|
133
|
+
let transport: "websocket" | "mesh" = "websocket";
|
|
134
|
+
|
|
135
|
+
if (peer.meshPeerId && this.meshPeer) {
|
|
136
|
+
// Phase 2: Full MeshPeer integration with FederationGateway (v0.2.0+)
|
|
137
|
+
transport = "mesh";
|
|
138
|
+
try {
|
|
139
|
+
// Try to get an existing gateway first (e.g. pre-connected in test setup),
|
|
140
|
+
// otherwise create one via federateWith()
|
|
141
|
+
let gateway: MeshFederationGateway | undefined =
|
|
142
|
+
this.meshPeer.getFederationGateway(peer.systemId);
|
|
143
|
+
if (!gateway) {
|
|
144
|
+
gateway = await this.meshPeer.federateWith(peer.systemId, {
|
|
145
|
+
localSystemId: this.meshPeer.peerId,
|
|
146
|
+
remoteSystemId: peer.systemId,
|
|
147
|
+
buffer: { enabled: true, maxMessages: 1000 },
|
|
148
|
+
routing: { maxHops: 5, trackPath: true },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.gateways.set(peer.systemId, gateway);
|
|
153
|
+
|
|
154
|
+
// Listen for incoming federated messages
|
|
155
|
+
gateway.on("message:received", (envelope: unknown) => {
|
|
156
|
+
const env = envelope as { payload?: { from?: string; payload?: unknown; meta?: Record<string, unknown> } };
|
|
157
|
+
if (env.payload) {
|
|
158
|
+
this.handlePeerMessage(peer.systemId, {
|
|
159
|
+
from: env.payload.from ?? peer.systemId,
|
|
160
|
+
payload: env.payload.payload,
|
|
161
|
+
meta: env.payload.meta,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Mesh federation connection failed for "${peer.systemId}": ${err instanceof Error ? err.message : err}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
} else if (peer.meshPeerId && this.meshConnector) {
|
|
171
|
+
// Phase 1 fallback: Mesh transport via raw channel (no MeshPeer)
|
|
172
|
+
transport = "mesh";
|
|
173
|
+
try {
|
|
174
|
+
conn = await this.meshConnector.connect(peer.meshPeerId);
|
|
175
|
+
|
|
176
|
+
conn.onMessage((msg: IncomingMapMessage) => {
|
|
177
|
+
this.handlePeerMessage(peer.systemId, msg);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
this.connections.set(peer.systemId, conn);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Mesh federation connection failed for "${peer.systemId}": ${err instanceof Error ? err.message : err}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
} else if (peer.url && this.sdkClass) {
|
|
187
|
+
// WebSocket transport — connect via MAP SDK
|
|
117
188
|
try {
|
|
118
189
|
conn = await this.sdkClass.connect(peer.url, {
|
|
119
190
|
name: this.systemId.id,
|
|
@@ -133,7 +204,6 @@ export class ConnectionManager {
|
|
|
133
204
|
},
|
|
134
205
|
});
|
|
135
206
|
|
|
136
|
-
// Register incoming message handler
|
|
137
207
|
conn.onMessage((msg: IncomingMapMessage) => {
|
|
138
208
|
this.handlePeerMessage(peer.systemId, msg);
|
|
139
209
|
});
|
|
@@ -151,7 +221,8 @@ export class ConnectionManager {
|
|
|
151
221
|
sessionId: crypto.randomUUID(),
|
|
152
222
|
status: "connected",
|
|
153
223
|
exposure: peer.exposure ?? { agents: "all" },
|
|
154
|
-
url: peer.url,
|
|
224
|
+
url: peer.url ?? "",
|
|
225
|
+
transport,
|
|
155
226
|
connectedAt: new Date().toISOString(),
|
|
156
227
|
};
|
|
157
228
|
|
|
@@ -213,10 +284,13 @@ export class ConnectionManager {
|
|
|
213
284
|
|
|
214
285
|
const link = this.peers.get(peerId);
|
|
215
286
|
if (!link || link.status !== "connected") {
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
287
|
+
// No direct peer link — try broadcast/hierarchical strategies before queuing.
|
|
288
|
+
// This handles cases like system-qualified addresses (bob@system-2) targeting
|
|
289
|
+
// a system we're not directly connected to but could reach via relay or hub.
|
|
290
|
+
const strategy = this.routing.getStrategy();
|
|
291
|
+
if (strategy !== "table") {
|
|
292
|
+
return this.handleUnknownRoute(addr, message);
|
|
293
|
+
}
|
|
220
294
|
this.queue.enqueue(peerId, message);
|
|
221
295
|
return { delivered: false, peerId, queued: true };
|
|
222
296
|
}
|
|
@@ -236,7 +310,7 @@ export class ConnectionManager {
|
|
|
236
310
|
}
|
|
237
311
|
|
|
238
312
|
/**
|
|
239
|
-
* Send a message to a peer via real connection or event
|
|
313
|
+
* Send a message to a peer via FederationGateway, real connection, or event.
|
|
240
314
|
* Returns true if send succeeded (or event was emitted).
|
|
241
315
|
*/
|
|
242
316
|
private async sendToPeer(
|
|
@@ -244,9 +318,42 @@ export class ConnectionManager {
|
|
|
244
318
|
addr: FederatedAddress,
|
|
245
319
|
message: Message
|
|
246
320
|
): Promise<boolean> {
|
|
321
|
+
// Phase 2: Try FederationGateway first (provides envelope, hop/loop detection)
|
|
322
|
+
const gateway = this.gateways.get(peerId);
|
|
323
|
+
if (gateway) {
|
|
324
|
+
try {
|
|
325
|
+
const mapMsg = inboxMessageToMap(message);
|
|
326
|
+
// Build a MAP-compatible message for the gateway.
|
|
327
|
+
// Use local agent address (not federated) since the gateway
|
|
328
|
+
// already handles cross-system routing. A federated `to` like
|
|
329
|
+
// { system: "system-b", agent: "bob" } would cause the remote
|
|
330
|
+
// MapServer to call routeToFederation() instead of deliverToAgent().
|
|
331
|
+
const targetAgentIds = addr.agent ? [addr.agent] : [];
|
|
332
|
+
const localTo =
|
|
333
|
+
targetAgentIds.length === 1
|
|
334
|
+
? { agent: targetAgentIds[0] }
|
|
335
|
+
: targetAgentIds.length > 1
|
|
336
|
+
? { agents: targetAgentIds }
|
|
337
|
+
: mapMsg.to;
|
|
338
|
+
const gatewayMessage = {
|
|
339
|
+
id: message.id,
|
|
340
|
+
from: message.sender_id,
|
|
341
|
+
to: localTo,
|
|
342
|
+
timestamp: Date.now(),
|
|
343
|
+
payload: mapMsg.payload,
|
|
344
|
+
meta: mapMsg.meta,
|
|
345
|
+
};
|
|
346
|
+
const routed = await gateway.route(gatewayMessage, targetAgentIds);
|
|
347
|
+
if (routed) return true;
|
|
348
|
+
// Fall through to direct connection if gateway route fails
|
|
349
|
+
} catch {
|
|
350
|
+
// Fall through to direct connection
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
247
354
|
const conn = this.connections.get(peerId);
|
|
248
355
|
if (conn) {
|
|
249
|
-
// Real transport — send via MAP SDK connection
|
|
356
|
+
// Real transport — send via MAP SDK or mesh channel connection
|
|
250
357
|
try {
|
|
251
358
|
await conn.send(
|
|
252
359
|
{ agentId: addr.agent, scope: addr.scope },
|
|
@@ -391,6 +498,13 @@ export class ConnectionManager {
|
|
|
391
498
|
/**
|
|
392
499
|
* Clean up all state. Call on shutdown.
|
|
393
500
|
*/
|
|
501
|
+
/**
|
|
502
|
+
* Check if a FederationGateway exists for a peer.
|
|
503
|
+
*/
|
|
504
|
+
hasGateway(peerId: string): boolean {
|
|
505
|
+
return this.gateways.has(peerId);
|
|
506
|
+
}
|
|
507
|
+
|
|
394
508
|
async destroy(): Promise<void> {
|
|
395
509
|
// Close all real connections
|
|
396
510
|
for (const [peerId, conn] of this.connections) {
|
|
@@ -402,6 +516,7 @@ export class ConnectionManager {
|
|
|
402
516
|
this.connections.delete(peerId);
|
|
403
517
|
}
|
|
404
518
|
|
|
519
|
+
this.gateways.clear();
|
|
405
520
|
this.routing.destroy();
|
|
406
521
|
this.queue.destroy();
|
|
407
522
|
this.peers.clear();
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,10 @@ import { InboxMcpServer } from "./mcp/mcp-server.js";
|
|
|
11
11
|
import { MailJsonRpcServer } from "./jsonrpc/mail-server.js";
|
|
12
12
|
import { PushNotifier } from "./push/notifier.js";
|
|
13
13
|
import { ConnectionManager } from "./federation/connection-manager.js";
|
|
14
|
+
import { MeshConnector } from "./mesh/mesh-connector.js";
|
|
15
|
+
import { DeliveryBridge } from "./mesh/delivery-bridge.js";
|
|
16
|
+
import type { MeshContextLike } from "./mesh/mesh-transport.js";
|
|
17
|
+
import type { MeshPeerLike, MeshMapServer } from "./map/map-client.js";
|
|
14
18
|
import { WarmRegistry } from "./registry/warm-registry.js";
|
|
15
19
|
import type { InboxConfig } from "./types.js";
|
|
16
20
|
import type { Storage } from "./storage/interface.js";
|
|
@@ -22,6 +26,7 @@ export { TraceabilityLayer } from "./traceability/traceability.js";
|
|
|
22
26
|
export { IpcServer } from "./ipc/ipc-server.js";
|
|
23
27
|
export { MapClient } from "./map/map-client.js";
|
|
24
28
|
export { InboxMcpServer } from "./mcp/mcp-server.js";
|
|
29
|
+
export { InboxMcpProxy } from "./mcp/mcp-proxy.js";
|
|
25
30
|
export { MailJsonRpcServer } from "./jsonrpc/mail-server.js";
|
|
26
31
|
export { PushNotifier, formatInboxMarkdown, parseCommand } from "./push/notifier.js";
|
|
27
32
|
export type { InboxMessageEvent, NotifierConfig, InboxFileEntry } from "./push/notifier.js";
|
|
@@ -38,6 +43,14 @@ export {
|
|
|
38
43
|
isRemoteAddress,
|
|
39
44
|
isBroadcastAddress,
|
|
40
45
|
} from "./federation/address.js";
|
|
46
|
+
export { MeshTransport, DEFAULT_CHANNEL_NAME } from "./mesh/mesh-transport.js";
|
|
47
|
+
export type { MeshContextLike, MeshChannel, MeshPeerInfo, InboxWireMessage } from "./mesh/mesh-transport.js";
|
|
48
|
+
export { MeshConnector } from "./mesh/mesh-connector.js";
|
|
49
|
+
export { DeliveryBridge } from "./mesh/delivery-bridge.js";
|
|
50
|
+
export type { MeshDeliveryHandler } from "./mesh/delivery-bridge.js";
|
|
51
|
+
export { mapMessageToInbox, inboxMessageToMap } from "./mesh/type-mapper.js";
|
|
52
|
+
export type { MapMessage as MeshMapMessage, MapAddress as MeshMapAddress } from "./mesh/type-mapper.js";
|
|
53
|
+
export type { MeshPeerLike, MeshAgentConnection, MeshMapServer } from "./map/map-client.js";
|
|
41
54
|
export type * from "./types.js";
|
|
42
55
|
export type { Storage, InboxQuery, ThreadQuery } from "./storage/interface.js";
|
|
43
56
|
|
|
@@ -101,6 +114,20 @@ export interface CreateOptions {
|
|
|
101
114
|
* When provided, Agent Inbox borrows this connection for messaging
|
|
102
115
|
* but does not own its lifecycle (won't disconnect it on stop()). */
|
|
103
116
|
connection?: import("./map/map-client.js").MapConnection;
|
|
117
|
+
/** Phase 1: agentic-mesh context for P2P federation transport.
|
|
118
|
+
* When provided, federation peers with meshPeerId use encrypted
|
|
119
|
+
* mesh channels instead of WebSocket MAP connections. */
|
|
120
|
+
mesh?: {
|
|
121
|
+
context: MeshContextLike;
|
|
122
|
+
localPeerId: string;
|
|
123
|
+
channelName?: string;
|
|
124
|
+
};
|
|
125
|
+
/** Phase 2: Full MeshPeer integration.
|
|
126
|
+
* When provided, agent-inbox registers as an agent on the MeshPeer's
|
|
127
|
+
* MapServer, uses FederationGateway for cross-mesh routing, and
|
|
128
|
+
* installs a DeliveryHandler bridge for incoming messages.
|
|
129
|
+
* Supersedes `mesh` option — if both are set, meshPeer takes priority. */
|
|
130
|
+
meshPeer?: MeshPeerLike;
|
|
104
131
|
}
|
|
105
132
|
|
|
106
133
|
export async function createAgentInbox(
|
|
@@ -146,7 +173,35 @@ export async function createAgentInbox(
|
|
|
146
173
|
router,
|
|
147
174
|
events,
|
|
148
175
|
);
|
|
149
|
-
|
|
176
|
+
|
|
177
|
+
// Phase 2: MeshPeer integration (if provided)
|
|
178
|
+
if (opts.meshPeer) {
|
|
179
|
+
const meshSystemId = await mapClient.connectViaMesh(opts.meshPeer);
|
|
180
|
+
|
|
181
|
+
// Install DeliveryHandler bridge on the MeshPeer's MapServer
|
|
182
|
+
const server = opts.meshPeer.server as MeshMapServer & {
|
|
183
|
+
setDeliveryHandler?(handler: unknown): unknown;
|
|
184
|
+
};
|
|
185
|
+
if (typeof server.setDeliveryHandler === "function") {
|
|
186
|
+
// Get the current handler, then replace with our bridge that delegates back
|
|
187
|
+
const prev = server.setDeliveryHandler(
|
|
188
|
+
new DeliveryBridge(storage, events, config.scope)
|
|
189
|
+
);
|
|
190
|
+
if (prev) {
|
|
191
|
+
// Re-install with the previous handler as fallback
|
|
192
|
+
server.setDeliveryHandler(
|
|
193
|
+
new DeliveryBridge(
|
|
194
|
+
storage,
|
|
195
|
+
events,
|
|
196
|
+
config.scope,
|
|
197
|
+
prev as import("./mesh/delivery-bridge.js").MeshDeliveryHandler
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.error(`Connected to MeshPeer (systemId: ${meshSystemId})`);
|
|
204
|
+
} else if (opts.connection) {
|
|
150
205
|
// External connection — borrow it, don't manage its lifecycle
|
|
151
206
|
mapClient.useConnection(opts.connection);
|
|
152
207
|
} else if (config.map?.enabled) {
|
|
@@ -173,8 +228,28 @@ export async function createAgentInbox(
|
|
|
173
228
|
if (opts.enableFederation || config.federation?.peers?.length) {
|
|
174
229
|
const sdkClass = mapClient.getAgentConnectionClass() ?? undefined;
|
|
175
230
|
|
|
231
|
+
let meshConnector: MeshConnector | undefined;
|
|
232
|
+
if (opts.mesh) {
|
|
233
|
+
meshConnector = new MeshConnector(
|
|
234
|
+
opts.mesh.context,
|
|
235
|
+
opts.mesh.localPeerId,
|
|
236
|
+
opts.mesh.channelName
|
|
237
|
+
);
|
|
238
|
+
} else if (opts.meshPeer) {
|
|
239
|
+
// Phase 2: Derive mesh context from MeshPeer if available
|
|
240
|
+
const peerObj = opts.meshPeer as MeshPeerLike & { mesh?: MeshContextLike };
|
|
241
|
+
if (peerObj.mesh) {
|
|
242
|
+
meshConnector = new MeshConnector(
|
|
243
|
+
peerObj.mesh,
|
|
244
|
+
opts.meshPeer.peerId
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
176
249
|
federation = new ConnectionManager(events, config.federation, {
|
|
177
250
|
sdkClass,
|
|
251
|
+
meshConnector,
|
|
252
|
+
meshPeer: opts.meshPeer,
|
|
178
253
|
onIncomingMessage: ({ from, peerId, payload, meta }) => {
|
|
179
254
|
// Delegate incoming federation messages to the router.
|
|
180
255
|
// The targetAgent in meta tells us which local agent this is for.
|
|
@@ -198,10 +273,12 @@ export async function createAgentInbox(
|
|
|
198
273
|
});
|
|
199
274
|
router.setFederation(federation);
|
|
200
275
|
|
|
201
|
-
// Tier 2 system ID resolution: update from MAP
|
|
276
|
+
// Tier 2 system ID resolution: update from MAP or MeshPeer systemInfo
|
|
202
277
|
const mapSystemName = mapClient.getSystemName();
|
|
203
278
|
if (mapSystemName) {
|
|
204
279
|
federation.updateSystemIdFromMap(mapSystemName);
|
|
280
|
+
} else if (opts.meshPeer) {
|
|
281
|
+
federation.updateSystemIdFromMap(opts.meshPeer.server.systemId);
|
|
205
282
|
}
|
|
206
283
|
|
|
207
284
|
// Connect to configured federation peers
|
|
@@ -209,7 +286,8 @@ export async function createAgentInbox(
|
|
|
209
286
|
for (const peer of config.federation.peers) {
|
|
210
287
|
try {
|
|
211
288
|
await federation.federate(peer);
|
|
212
|
-
|
|
289
|
+
const via = peer.meshPeerId ? `mesh:${peer.meshPeerId}` : peer.url;
|
|
290
|
+
console.error(`Federated with peer: ${peer.systemId} (${via})`);
|
|
213
291
|
} catch (err) {
|
|
214
292
|
console.error(
|
|
215
293
|
`Failed to federate with ${peer.systemId}: ${err instanceof Error ? err.message : err}`
|
|
@@ -269,8 +347,21 @@ const isMainModule =
|
|
|
269
347
|
if (isMainModule) {
|
|
270
348
|
const mode = process.argv[2] ?? "ipc";
|
|
271
349
|
|
|
272
|
-
if (mode === "mcp") {
|
|
273
|
-
// MCP
|
|
350
|
+
if (mode === "mcp-proxy" || (mode === "mcp" && process.env.INBOX_SOCKET_PATH)) {
|
|
351
|
+
// MCP proxy mode — forward all tool calls to an existing inbox IPC socket.
|
|
352
|
+
// Auto-detected when INBOX_SOCKET_PATH is set in mcp mode.
|
|
353
|
+
const { InboxMcpProxy } = await import("./mcp/mcp-proxy.js");
|
|
354
|
+
const socketPath = process.env.INBOX_SOCKET_PATH ?? defaultSocketPath();
|
|
355
|
+
const agentId = process.env.INBOX_AGENT_ID ?? "anonymous";
|
|
356
|
+
const scope = process.env.INBOX_SCOPE ?? "default";
|
|
357
|
+
console.error(`[agent-inbox] MCP proxy mode → ${socketPath} (agent: ${agentId})`);
|
|
358
|
+
const proxy = new InboxMcpProxy(socketPath, agentId, scope);
|
|
359
|
+
proxy.start().catch((err) => {
|
|
360
|
+
console.error("MCP proxy failed:", err);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
});
|
|
363
|
+
} else if (mode === "mcp") {
|
|
364
|
+
// MCP standalone mode (stdio transport, own storage)
|
|
274
365
|
const config = loadConfig();
|
|
275
366
|
const sqlitePath = process.env.INBOX_SQLITE_PATH;
|
|
276
367
|
const storage: Storage = sqlitePath
|
package/src/ipc/ipc-server.ts
CHANGED
|
@@ -117,6 +117,12 @@ export class IpcServer {
|
|
|
117
117
|
case "check_inbox":
|
|
118
118
|
return this.handleCheckInbox(command);
|
|
119
119
|
|
|
120
|
+
case "read_thread":
|
|
121
|
+
return this.handleReadThread(command);
|
|
122
|
+
|
|
123
|
+
case "list_agents":
|
|
124
|
+
return this.handleListAgents(command);
|
|
125
|
+
|
|
120
126
|
case "emit":
|
|
121
127
|
// For Phase 1, emit is acknowledged but not forwarded to MAP
|
|
122
128
|
return { ok: true };
|
|
@@ -170,6 +176,58 @@ export class IpcServer {
|
|
|
170
176
|
return { ok: true, messages };
|
|
171
177
|
}
|
|
172
178
|
|
|
179
|
+
private handleReadThread(
|
|
180
|
+
command: Extract<IpcCommand, { action: "read_thread" }>
|
|
181
|
+
): IpcResponse {
|
|
182
|
+
const messages = this.storage.getThread({
|
|
183
|
+
threadTag: command.threadTag,
|
|
184
|
+
scope: command.scope ?? "default",
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
threadTag: command.threadTag,
|
|
189
|
+
count: messages.length,
|
|
190
|
+
messages: messages.map((m) => ({
|
|
191
|
+
...m,
|
|
192
|
+
// Flatten for IPC consumers — keep full message but ensure key fields are accessible
|
|
193
|
+
})),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private handleListAgents(
|
|
198
|
+
command: Extract<IpcCommand, { action: "list_agents" }>
|
|
199
|
+
): IpcResponse {
|
|
200
|
+
const agents = this.storage.listAgents(command.scope);
|
|
201
|
+
const localList = agents.map((a) => ({
|
|
202
|
+
agentId: a.agent_id,
|
|
203
|
+
name: a.display_name,
|
|
204
|
+
scope: a.scope,
|
|
205
|
+
status: a.status,
|
|
206
|
+
program: a.program,
|
|
207
|
+
lastActive: a.last_active_at,
|
|
208
|
+
location: "local" as const,
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
let federatedList: IpcResponse["agents"] = [];
|
|
212
|
+
if (command.includeFederated && this.router.getFederation?.()) {
|
|
213
|
+
const federation = this.router.getFederation!()!;
|
|
214
|
+
const routingTable = federation.routing.getTable();
|
|
215
|
+
federatedList = routingTable.map((entry) => ({
|
|
216
|
+
agentId: entry.agentId,
|
|
217
|
+
peerId: entry.peerId,
|
|
218
|
+
scope: command.scope ?? "default",
|
|
219
|
+
status: entry.status,
|
|
220
|
+
location: "federated" as const,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
count: localList.length + (federatedList?.length ?? 0),
|
|
227
|
+
agents: [...localList, ...(federatedList ?? [])],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
173
231
|
private handleNotify(
|
|
174
232
|
command: Extract<IpcCommand, { action: "notify" }>
|
|
175
233
|
): IpcResponse {
|
package/src/map/map-client.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { MessageRouter } from "../router/message-router.js";
|
|
|
4
4
|
import { normalizeContent } from "../router/message-router.js";
|
|
5
5
|
import { ulid } from "ulid";
|
|
6
6
|
import type { Message, Recipient, InboxConfig } from "../types.js";
|
|
7
|
+
import { mapMessageToInbox } from "../mesh/type-mapper.js";
|
|
8
|
+
import type { MapMessage } from "../mesh/type-mapper.js";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Wrapper around @multi-agent-protocol/sdk for Agent Inbox's MAP connection.
|
|
@@ -15,6 +17,9 @@ export class MapClient {
|
|
|
15
17
|
private conn: MapConnection | null = null;
|
|
16
18
|
private agentConnectionClass: MapAgentConnectionClass | null = null;
|
|
17
19
|
private externalConn = false;
|
|
20
|
+
private meshPeer: MeshPeerLike | null = null;
|
|
21
|
+
private meshAgentConn: MeshAgentConnection | null = null;
|
|
22
|
+
private meshMessageHandler: ((agentId: string, message: MeshMapMessage) => void) | null = null;
|
|
18
23
|
|
|
19
24
|
constructor(
|
|
20
25
|
private storage: Storage,
|
|
@@ -78,7 +83,60 @@ export class MapClient {
|
|
|
78
83
|
});
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Connect to an agentic-mesh MeshPeer as a local agent.
|
|
88
|
+
*
|
|
89
|
+
* Registers agent-inbox as an agent on the MeshPeer's MapServer,
|
|
90
|
+
* receives messages via the MapServer's message handler mechanism.
|
|
91
|
+
* This is the Phase 2 integration path — full MAP protocol over mesh.
|
|
92
|
+
*
|
|
93
|
+
* Returns the MapServer's systemId for federation system ID resolution.
|
|
94
|
+
*/
|
|
95
|
+
async connectViaMesh(meshPeer: MeshPeerLike): Promise<string> {
|
|
96
|
+
this.meshPeer = meshPeer;
|
|
97
|
+
|
|
98
|
+
// Register agent-inbox as an agent on the MeshPeer's MapServer
|
|
99
|
+
this.meshAgentConn = await meshPeer.createAgent({
|
|
100
|
+
agentId: "agent-inbox",
|
|
101
|
+
name: "Agent Inbox",
|
|
102
|
+
role: "inbox",
|
|
103
|
+
scopes: ["default"],
|
|
104
|
+
capabilities: { trajectory: { canReport: false } },
|
|
105
|
+
metadata: { type: "agent-inbox" },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Set up message handler on the MapServer for this agent
|
|
109
|
+
this.meshMessageHandler = (_agentId: string, message: MeshMapMessage) => {
|
|
110
|
+
this.handleMeshMessage(message);
|
|
111
|
+
};
|
|
112
|
+
meshPeer.server.setMessageHandler("agent-inbox", this.meshMessageHandler);
|
|
113
|
+
|
|
114
|
+
return meshPeer.server.systemId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the MeshPeer this client is connected to, if any.
|
|
119
|
+
*/
|
|
120
|
+
getMeshPeer(): MeshPeerLike | null {
|
|
121
|
+
return this.meshPeer;
|
|
122
|
+
}
|
|
123
|
+
|
|
81
124
|
async disconnect(): Promise<void> {
|
|
125
|
+
// Disconnect mesh agent connection
|
|
126
|
+
if (this.meshAgentConn) {
|
|
127
|
+
if (this.meshPeer && this.meshMessageHandler) {
|
|
128
|
+
this.meshPeer.server.removeMessageHandler("agent-inbox");
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
await this.meshAgentConn.unregister();
|
|
132
|
+
} catch {
|
|
133
|
+
// Best-effort
|
|
134
|
+
}
|
|
135
|
+
this.meshAgentConn = null;
|
|
136
|
+
this.meshPeer = null;
|
|
137
|
+
this.meshMessageHandler = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
82
140
|
if (this.conn) {
|
|
83
141
|
if (!this.externalConn) {
|
|
84
142
|
await this.conn.disconnect();
|
|
@@ -174,6 +232,16 @@ export class MapClient {
|
|
|
174
232
|
);
|
|
175
233
|
}
|
|
176
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Handle an incoming MAP message from the MeshPeer's MapServer.
|
|
237
|
+
* Uses the type-mapper for full MAP Message → Inbox Message translation.
|
|
238
|
+
*/
|
|
239
|
+
private handleMeshMessage(msg: MeshMapMessage): void {
|
|
240
|
+
const inboxMsg = mapMessageToInbox(msg as MapMessage);
|
|
241
|
+
this.storage.putMessage(inboxMsg);
|
|
242
|
+
this.events.emit("message.created", inboxMsg);
|
|
243
|
+
}
|
|
244
|
+
|
|
177
245
|
private handleIncoming(msg: IncomingMapMessage): void {
|
|
178
246
|
const now = new Date().toISOString();
|
|
179
247
|
const content = normalizeContent(msg.payload);
|
|
@@ -220,6 +288,90 @@ export class MapClient {
|
|
|
220
288
|
}
|
|
221
289
|
}
|
|
222
290
|
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// MeshPeer integration (Phase 2)
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/** Subset of agentic-mesh AgentConnection used by MapClient. */
|
|
296
|
+
export interface MeshAgentConnection {
|
|
297
|
+
readonly agentId: string;
|
|
298
|
+
readonly isRegistered: boolean;
|
|
299
|
+
register(): Promise<unknown>;
|
|
300
|
+
unregister(reason?: string): Promise<void>;
|
|
301
|
+
send(
|
|
302
|
+
to: unknown,
|
|
303
|
+
payload: unknown,
|
|
304
|
+
meta?: Record<string, unknown>
|
|
305
|
+
): Promise<{ messageId: string; delivered: string[] }>;
|
|
306
|
+
on(event: "message", handler: (message: MeshMapMessage) => void): this;
|
|
307
|
+
off(event: string, handler: (...args: unknown[]) => void): this;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Subset of agentic-mesh MapServer used by MapClient (v0.2.0+). */
|
|
311
|
+
export interface MeshMapServer {
|
|
312
|
+
readonly systemId: string;
|
|
313
|
+
readonly systemName?: string;
|
|
314
|
+
/** Replace the default delivery handler. Returns the previous handler. */
|
|
315
|
+
setDeliveryHandler(handler: MeshDeliveryHandler): MeshDeliveryHandler;
|
|
316
|
+
setMessageHandler(agentId: string, handler: (agentId: string, message: MeshMapMessage) => void | Promise<void>): void;
|
|
317
|
+
removeMessageHandler(agentId: string): void;
|
|
318
|
+
registerAgent(params: {
|
|
319
|
+
agentId?: string;
|
|
320
|
+
name?: string;
|
|
321
|
+
role?: string;
|
|
322
|
+
scopes?: string[];
|
|
323
|
+
capabilities?: Record<string, unknown>;
|
|
324
|
+
metadata?: Record<string, unknown>;
|
|
325
|
+
}): unknown;
|
|
326
|
+
unregisterAgent(agentId: string, reason?: string): unknown;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Subset of agentic-mesh DeliveryHandler (v0.2.0+). */
|
|
330
|
+
export interface MeshDeliveryHandler {
|
|
331
|
+
deliverToAgent(agentId: string, message: MeshMapMessage): Promise<boolean>;
|
|
332
|
+
forwardToPeer(peerId: string, agentIds: string[], message: MeshMapMessage): Promise<boolean>;
|
|
333
|
+
routeToFederation?(systemId: string, agentIds: string[], message: MeshMapMessage): Promise<boolean>;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Subset of agentic-mesh MeshPeer used by MapClient and ConnectionManager (v0.2.0+). */
|
|
337
|
+
export interface MeshPeerLike {
|
|
338
|
+
readonly peerId: string;
|
|
339
|
+
readonly server: MeshMapServer;
|
|
340
|
+
createAgent(config: {
|
|
341
|
+
agentId?: string;
|
|
342
|
+
name?: string;
|
|
343
|
+
role?: string;
|
|
344
|
+
scopes?: string[];
|
|
345
|
+
capabilities?: Record<string, unknown>;
|
|
346
|
+
metadata?: Record<string, unknown>;
|
|
347
|
+
}): Promise<MeshAgentConnection>;
|
|
348
|
+
/** Establish a federation link with a remote MAP system. */
|
|
349
|
+
federateWith(remoteSystemId: string, config?: unknown): Promise<MeshFederationGateway>;
|
|
350
|
+
/** Get an existing federation gateway by remote system ID. */
|
|
351
|
+
getFederationGateway(remoteSystemId: string): MeshFederationGateway | undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Subset of agentic-mesh FederationGateway (v0.2.0+). */
|
|
355
|
+
export interface MeshFederationGateway {
|
|
356
|
+
readonly localSystemId: string;
|
|
357
|
+
readonly remoteSystemId: string;
|
|
358
|
+
readonly isConnected: boolean;
|
|
359
|
+
route(message: unknown, targetAgentIds: string[]): Promise<boolean>;
|
|
360
|
+
on(event: string, handler: (...args: unknown[]) => void): this;
|
|
361
|
+
off(event: string, handler: (...args: unknown[]) => void): this;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Subset of agentic-mesh MAP Message. */
|
|
365
|
+
export interface MeshMapMessage {
|
|
366
|
+
id: string;
|
|
367
|
+
from: string;
|
|
368
|
+
to: unknown;
|
|
369
|
+
timestamp: number;
|
|
370
|
+
payload?: unknown;
|
|
371
|
+
meta?: { priority?: string; correlationId?: string; _meta?: Record<string, unknown>; [key: string]: unknown };
|
|
372
|
+
_meta?: Record<string, unknown>;
|
|
373
|
+
}
|
|
374
|
+
|
|
223
375
|
// --- MAP SDK type stubs (for when the SDK isn't installed) ---
|
|
224
376
|
|
|
225
377
|
export interface IncomingMapMessage {
|