agent-inbox 0.1.8 → 0.2.0
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/cli.d.ts +20 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +89 -0
- package/dist/cli.js.map +1 -0
- 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 +10 -2
- package/src/cli.ts +94 -0
- 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
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-inbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Agent Inbox — message routing, traceability, and MCP tools for multi-agent systems",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"agent-inbox": "dist/cli.js"
|
|
10
|
+
},
|
|
8
11
|
"scripts": {
|
|
9
12
|
"build": "tsc",
|
|
10
13
|
"dev": "tsc --watch",
|
|
@@ -37,16 +40,21 @@
|
|
|
37
40
|
"ulid": "^2.3.0"
|
|
38
41
|
},
|
|
39
42
|
"peerDependencies": {
|
|
40
|
-
"@multi-agent-protocol/sdk": "^0.1.4"
|
|
43
|
+
"@multi-agent-protocol/sdk": "^0.1.4",
|
|
44
|
+
"agentic-mesh": ">=0.2.0"
|
|
41
45
|
},
|
|
42
46
|
"peerDependenciesMeta": {
|
|
43
47
|
"@multi-agent-protocol/sdk": {
|
|
44
48
|
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"agentic-mesh": {
|
|
51
|
+
"optional": true
|
|
45
52
|
}
|
|
46
53
|
},
|
|
47
54
|
"devDependencies": {
|
|
48
55
|
"@types/better-sqlite3": "^7.6.13",
|
|
49
56
|
"@types/node": "^22.0.0",
|
|
57
|
+
"agentic-mesh": "^0.2.0",
|
|
50
58
|
"typescript": "^5.7.0",
|
|
51
59
|
"vitest": "^3.0.0"
|
|
52
60
|
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for agent-inbox.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* agent-inbox # IPC server mode (default)
|
|
8
|
+
* agent-inbox mcp # MCP stdio server (standalone or proxy if INBOX_SOCKET_PATH set)
|
|
9
|
+
* agent-inbox mcp-proxy # MCP proxy to existing IPC socket
|
|
10
|
+
*
|
|
11
|
+
* Environment variables:
|
|
12
|
+
* INBOX_SOCKET_PATH — IPC socket path (also triggers proxy mode for `mcp`)
|
|
13
|
+
* INBOX_SQLITE_PATH — SQLite DB path (default: in-memory)
|
|
14
|
+
* INBOX_SCOPE — Message scope (default: "default")
|
|
15
|
+
* INBOX_AGENT_ID — Agent ID for proxy mode (default: "anonymous")
|
|
16
|
+
* INBOX_HTTP_PORT — HTTP port for JSON-RPC (0 = disabled)
|
|
17
|
+
* INBOX_MAP_ENABLED — Enable MAP transport ("true")
|
|
18
|
+
* INBOX_MAP_SERVER — MAP server URL
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { EventEmitter } from "node:events";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import * as os from "node:os";
|
|
24
|
+
import { InMemoryStorage } from "./storage/memory.js";
|
|
25
|
+
import { SqliteStorage } from "./storage/sqlite.js";
|
|
26
|
+
import { MessageRouter } from "./router/message-router.js";
|
|
27
|
+
import { TraceabilityLayer } from "./traceability/traceability.js";
|
|
28
|
+
import { InboxMcpServer } from "./mcp/mcp-server.js";
|
|
29
|
+
import type { Storage } from "./storage/interface.js";
|
|
30
|
+
|
|
31
|
+
function defaultSocketPath(): string {
|
|
32
|
+
return path.join(os.homedir(), ".claude", "agent-inbox", "inbox.sock");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const mode = process.argv[2] ?? "mcp";
|
|
36
|
+
|
|
37
|
+
if (mode === "mcp-proxy" || (mode === "mcp" && process.env.INBOX_SOCKET_PATH)) {
|
|
38
|
+
// MCP proxy mode — forward tool calls to an existing inbox IPC socket
|
|
39
|
+
const { InboxMcpProxy } = await import("./mcp/mcp-proxy.js");
|
|
40
|
+
const socketPath = process.env.INBOX_SOCKET_PATH ?? defaultSocketPath();
|
|
41
|
+
const agentId = process.env.INBOX_AGENT_ID ?? "anonymous";
|
|
42
|
+
const scope = process.env.INBOX_SCOPE ?? "default";
|
|
43
|
+
console.error(`[agent-inbox] MCP proxy mode → ${socketPath} (agent: ${agentId})`);
|
|
44
|
+
const proxy = new InboxMcpProxy(socketPath, agentId, scope);
|
|
45
|
+
proxy.start().catch((err) => {
|
|
46
|
+
console.error("MCP proxy failed:", err);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
49
|
+
} else if (mode === "mcp") {
|
|
50
|
+
// MCP standalone mode — stdio transport, own storage
|
|
51
|
+
const scope = process.env.INBOX_SCOPE ?? "default";
|
|
52
|
+
const sqlitePath = process.env.INBOX_SQLITE_PATH;
|
|
53
|
+
const storage: Storage = sqlitePath
|
|
54
|
+
? new SqliteStorage({ path: sqlitePath })
|
|
55
|
+
: new InMemoryStorage();
|
|
56
|
+
const events = new EventEmitter();
|
|
57
|
+
const router = new MessageRouter(storage, events, scope);
|
|
58
|
+
const _traceability = new TraceabilityLayer(storage, events);
|
|
59
|
+
const mcpServer = new InboxMcpServer(router, storage, scope);
|
|
60
|
+
console.error("[agent-inbox] MCP standalone mode (stdio)");
|
|
61
|
+
mcpServer.start().catch((err) => {
|
|
62
|
+
console.error("MCP server failed:", err);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
} else if (mode === "ipc") {
|
|
66
|
+
const { createAgentInbox } = await import("./index.js");
|
|
67
|
+
const httpPort = process.env.INBOX_HTTP_PORT
|
|
68
|
+
? parseInt(process.env.INBOX_HTTP_PORT, 10)
|
|
69
|
+
: 0;
|
|
70
|
+
const sqlitePath = process.env.INBOX_SQLITE_PATH;
|
|
71
|
+
|
|
72
|
+
createAgentInbox({ httpPort, sqlitePath })
|
|
73
|
+
.then((inbox) => {
|
|
74
|
+
console.error(
|
|
75
|
+
`[agent-inbox] IPC server listening on ${process.env.INBOX_SOCKET_PATH ?? defaultSocketPath()}`
|
|
76
|
+
);
|
|
77
|
+
process.on("SIGINT", async () => {
|
|
78
|
+
await inbox.stop();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
});
|
|
81
|
+
process.on("SIGTERM", async () => {
|
|
82
|
+
await inbox.stop();
|
|
83
|
+
process.exit(0);
|
|
84
|
+
});
|
|
85
|
+
})
|
|
86
|
+
.catch((err) => {
|
|
87
|
+
console.error("Failed to start Agent Inbox:", err);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
console.error(`Unknown mode: ${mode}`);
|
|
92
|
+
console.error("Usage: agent-inbox [mcp|mcp-proxy|ipc]");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
@@ -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 {
|