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.
Files changed (75) hide show
  1. package/CLAUDE.md +44 -7
  2. package/README.md +67 -24
  3. package/dist/cli.d.ts +20 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +89 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/federation/connection-manager.d.ts +13 -2
  8. package/dist/federation/connection-manager.d.ts.map +1 -1
  9. package/dist/federation/connection-manager.js +109 -10
  10. package/dist/federation/connection-manager.js.map +1 -1
  11. package/dist/index.d.mts +2 -0
  12. package/dist/index.d.ts +25 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +58 -5
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +1 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/ipc/ipc-server.d.ts +2 -0
  19. package/dist/ipc/ipc-server.d.ts.map +1 -1
  20. package/dist/ipc/ipc-server.js +48 -0
  21. package/dist/ipc/ipc-server.js.map +1 -1
  22. package/dist/map/map-client.d.ts +100 -0
  23. package/dist/map/map-client.d.ts.map +1 -1
  24. package/dist/map/map-client.js +61 -0
  25. package/dist/map/map-client.js.map +1 -1
  26. package/dist/mcp/mcp-proxy.d.ts +28 -0
  27. package/dist/mcp/mcp-proxy.d.ts.map +1 -0
  28. package/dist/mcp/mcp-proxy.js +280 -0
  29. package/dist/mcp/mcp-proxy.js.map +1 -0
  30. package/dist/mesh/delivery-bridge.d.ts +47 -0
  31. package/dist/mesh/delivery-bridge.d.ts.map +1 -0
  32. package/dist/mesh/delivery-bridge.js +73 -0
  33. package/dist/mesh/delivery-bridge.js.map +1 -0
  34. package/dist/mesh/mesh-connector.d.ts +29 -0
  35. package/dist/mesh/mesh-connector.d.ts.map +1 -0
  36. package/dist/mesh/mesh-connector.js +36 -0
  37. package/dist/mesh/mesh-connector.js.map +1 -0
  38. package/dist/mesh/mesh-transport.d.ts +70 -0
  39. package/dist/mesh/mesh-transport.d.ts.map +1 -0
  40. package/dist/mesh/mesh-transport.js +92 -0
  41. package/dist/mesh/mesh-transport.js.map +1 -0
  42. package/dist/mesh/type-mapper.d.ts +67 -0
  43. package/dist/mesh/type-mapper.d.ts.map +1 -0
  44. package/dist/mesh/type-mapper.js +165 -0
  45. package/dist/mesh/type-mapper.js.map +1 -0
  46. package/dist/types.d.ts +29 -2
  47. package/dist/types.d.ts.map +1 -1
  48. package/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
  49. package/package.json +10 -2
  50. package/src/cli.ts +94 -0
  51. package/src/federation/connection-manager.ts +125 -10
  52. package/src/index.ts +96 -5
  53. package/src/ipc/ipc-server.ts +58 -0
  54. package/src/map/map-client.ts +152 -0
  55. package/src/mcp/mcp-proxy.ts +326 -0
  56. package/src/mesh/delivery-bridge.ts +110 -0
  57. package/src/mesh/mesh-connector.ts +41 -0
  58. package/src/mesh/mesh-transport.ts +157 -0
  59. package/src/mesh/type-mapper.ts +239 -0
  60. package/src/types.ts +33 -1
  61. package/test/federation/integration.test.ts +37 -3
  62. package/test/federation/sdk-integration.test.ts +4 -8
  63. package/test/ipc-new-commands.test.ts +200 -0
  64. package/test/mcp-proxy.test.ts +191 -0
  65. package/test/mesh/delivery-bridge.test.ts +178 -0
  66. package/test/mesh/e2e-mesh.test.ts +527 -0
  67. package/test/mesh/e2e-real-meshpeer.test.ts +629 -0
  68. package/test/mesh/federation-mesh.test.ts +269 -0
  69. package/test/mesh/mesh-connector.test.ts +66 -0
  70. package/test/mesh/mesh-transport.test.ts +191 -0
  71. package/test/mesh/meshpeer-integration.test.ts +442 -0
  72. package/test/mesh/mock-mesh.ts +125 -0
  73. package/test/mesh/mock-meshpeer.ts +266 -0
  74. package/test/mesh/type-mapper.test.ts +226 -0
  75. package/docs/PLAN.md +0 -545
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "agent-inbox",
3
- "version": "0.1.8",
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
- if (this.sdkClass) {
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
- // TODO: When resolved peerId has no peer link (e.g., system-qualified address
217
- // targeting a system we're not directly connected to), fall through to
218
- // handleUnknownRoute so broadcast/hierarchical strategies can attempt delivery
219
- // via connected peers or upstream hubs instead of immediately queuing.
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 emission.
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
- if (opts.connection) {
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 systemInfo if available
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
- console.error(`Federated with peer: ${peer.systemId} (${peer.url})`);
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-only mode (stdio transport for Claude Code integration)
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
@@ -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 {