agent-inbox 0.0.1 → 0.1.1
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 +113 -0
- package/README.md +195 -1
- package/dist/federation/address.d.ts +24 -0
- package/dist/federation/address.d.ts.map +1 -0
- package/dist/federation/address.js +54 -0
- package/dist/federation/address.js.map +1 -0
- package/dist/federation/connection-manager.d.ts +118 -0
- package/dist/federation/connection-manager.d.ts.map +1 -0
- package/dist/federation/connection-manager.js +369 -0
- package/dist/federation/connection-manager.js.map +1 -0
- package/dist/federation/delivery-queue.d.ts +66 -0
- package/dist/federation/delivery-queue.d.ts.map +1 -0
- package/dist/federation/delivery-queue.js +199 -0
- package/dist/federation/delivery-queue.js.map +1 -0
- package/dist/federation/index.d.ts +7 -0
- package/dist/federation/index.d.ts.map +1 -0
- package/dist/federation/index.js +6 -0
- package/dist/federation/index.js.map +1 -0
- package/dist/federation/routing-engine.d.ts +74 -0
- package/dist/federation/routing-engine.d.ts.map +1 -0
- package/dist/federation/routing-engine.js +158 -0
- package/dist/federation/routing-engine.js.map +1 -0
- package/dist/federation/trust.d.ts +39 -0
- package/dist/federation/trust.d.ts.map +1 -0
- package/dist/federation/trust.js +64 -0
- package/dist/federation/trust.js.map +1 -0
- package/dist/index.d.ts +60 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +217 -18
- package/dist/index.js.map +1 -1
- package/dist/ipc/ipc-server.d.ts +20 -0
- package/dist/ipc/ipc-server.d.ts.map +1 -0
- package/dist/ipc/ipc-server.js +152 -0
- package/dist/ipc/ipc-server.js.map +1 -0
- package/dist/jsonrpc/mail-server.d.ts +45 -0
- package/dist/jsonrpc/mail-server.d.ts.map +1 -0
- package/dist/jsonrpc/mail-server.js +284 -0
- package/dist/jsonrpc/mail-server.js.map +1 -0
- package/dist/map/map-client.d.ts +91 -0
- package/dist/map/map-client.d.ts.map +1 -0
- package/dist/map/map-client.js +202 -0
- package/dist/map/map-client.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +23 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -0
- package/dist/mcp/mcp-server.js +226 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/push/notifier.d.ts +49 -0
- package/dist/push/notifier.d.ts.map +1 -0
- package/dist/push/notifier.js +150 -0
- package/dist/push/notifier.js.map +1 -0
- package/dist/registry/warm-registry.d.ts +63 -0
- package/dist/registry/warm-registry.d.ts.map +1 -0
- package/dist/registry/warm-registry.js +173 -0
- package/dist/registry/warm-registry.js.map +1 -0
- package/dist/router/message-router.d.ts +44 -0
- package/dist/router/message-router.d.ts.map +1 -0
- package/dist/router/message-router.js +137 -0
- package/dist/router/message-router.js.map +1 -0
- package/dist/storage/interface.d.ts +31 -0
- package/dist/storage/interface.d.ts.map +1 -0
- package/dist/storage/interface.js +2 -0
- package/dist/storage/interface.js.map +1 -0
- package/dist/storage/memory.d.ts +28 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +118 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/sqlite.d.ts +35 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +445 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/traceability/traceability.d.ts +29 -0
- package/dist/traceability/traceability.d.ts.map +1 -0
- package/dist/traceability/traceability.js +150 -0
- package/dist/traceability/traceability.js.map +1 -0
- package/dist/types.d.ts +253 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/docs/DESIGN.md +1156 -0
- package/docs/PLAN.md +545 -0
- package/hooks/inbox-hook.mjs +119 -0
- package/hooks/register-hook.mjs +69 -0
- package/package.json +33 -25
- package/rules/agent-inbox.md +78 -0
- package/src/federation/address.ts +61 -0
- package/src/federation/connection-manager.ts +458 -0
- package/src/federation/delivery-queue.ts +222 -0
- package/src/federation/index.ts +6 -0
- package/src/federation/routing-engine.ts +188 -0
- package/src/federation/trust.ts +71 -0
- package/src/index.ts +299 -0
- package/src/ipc/ipc-server.ts +180 -0
- package/src/jsonrpc/mail-server.ts +356 -0
- package/src/map/map-client.ts +260 -0
- package/src/mcp/mcp-server.ts +272 -0
- package/src/push/notifier.ts +192 -0
- package/src/registry/warm-registry.ts +210 -0
- package/src/router/message-router.ts +175 -0
- package/src/storage/interface.ts +48 -0
- package/src/storage/memory.ts +145 -0
- package/src/storage/sqlite.ts +645 -0
- package/src/traceability/traceability.ts +183 -0
- package/src/types.ts +287 -0
- package/test/federation/address.test.ts +101 -0
- package/test/federation/connection-manager.test.ts +546 -0
- package/test/federation/delivery-queue.test.ts +159 -0
- package/test/federation/integration.test.ts +823 -0
- package/test/federation/routing-engine.test.ts +117 -0
- package/test/federation/sdk-integration.test.ts +748 -0
- package/test/federation/trust.test.ts +89 -0
- package/test/ipc-jsonrpc.test.ts +113 -0
- package/test/ipc-server.test.ts +138 -0
- package/test/mail-server.test.ts +208 -0
- package/test/map-client.test.ts +408 -0
- package/test/message-router.test.ts +184 -0
- package/test/push-notifier.test.ts +139 -0
- package/test/registry/warm-registry.test.ts +171 -0
- package/test/sqlite-storage.test.ts +243 -0
- package/test/storage.test.ts +196 -0
- package/test/traceability.test.ts +123 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +8 -0
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { ulid } from "ulid";
|
|
3
|
+
import type { Message, Conversation, Turn, Thread } from "../types.js";
|
|
4
|
+
import type { Storage } from "../storage/interface.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Auto-creates Conversations, Turns, and Threads from messaging events.
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* 1. Message with conversationId → add Turn to that Conversation
|
|
11
|
+
* 2. Message with thread_tag, no conversationId → find/create Conversation for thread_tag+scope
|
|
12
|
+
* 3. Message with neither → add Turn to catch-all Conversation for scope
|
|
13
|
+
* 4. Reply (in_reply_to) → create Thread if not exists, link Turn
|
|
14
|
+
*/
|
|
15
|
+
export class TraceabilityLayer {
|
|
16
|
+
/** Maps "thread_tag:scope" → conversation ID */
|
|
17
|
+
private tagToConversation = new Map<string, string>();
|
|
18
|
+
/** Maps scope → catch-all conversation ID */
|
|
19
|
+
private scopeCatchAll = new Map<string, string>();
|
|
20
|
+
/** Maps original message ID → thread ID (for reply chains) */
|
|
21
|
+
private replyChainToThread = new Map<string, string>();
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private storage: Storage,
|
|
25
|
+
events: EventEmitter
|
|
26
|
+
) {
|
|
27
|
+
events.on("message.created", (msg: Message) => this.onMessage(msg));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private onMessage(message: Message): void {
|
|
31
|
+
const conversationId = this.resolveConversation(message);
|
|
32
|
+
|
|
33
|
+
// Update message's conversation_id if not already set
|
|
34
|
+
if (!message.conversation_id) {
|
|
35
|
+
message.conversation_id = conversationId;
|
|
36
|
+
this.storage.putMessage(message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Ensure sender is a participant
|
|
40
|
+
this.ensureParticipant(conversationId, message.sender_id);
|
|
41
|
+
|
|
42
|
+
// Ensure recipients are participants
|
|
43
|
+
for (const r of message.recipients) {
|
|
44
|
+
this.ensureParticipant(conversationId, r.agent_id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create Turn
|
|
48
|
+
const turn: Turn = {
|
|
49
|
+
id: `turn-${ulid()}`,
|
|
50
|
+
conversation_id: conversationId,
|
|
51
|
+
participant_id: message.sender_id,
|
|
52
|
+
source_message_id: message.id,
|
|
53
|
+
content_type: message.content.type,
|
|
54
|
+
content: message.content,
|
|
55
|
+
created_at: message.created_at,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Handle reply threading
|
|
59
|
+
if (message.in_reply_to) {
|
|
60
|
+
const threadId = this.resolveThread(message, conversationId, turn.id);
|
|
61
|
+
turn.thread_id = threadId;
|
|
62
|
+
turn.in_reply_to = this.findTurnForMessage(message.in_reply_to);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.storage.addTurn(turn);
|
|
66
|
+
|
|
67
|
+
// Update conversation updated_at
|
|
68
|
+
const conv = this.storage.getConversation(conversationId);
|
|
69
|
+
if (conv) {
|
|
70
|
+
conv.updated_at = message.created_at;
|
|
71
|
+
this.storage.putConversation(conv);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private resolveConversation(message: Message): string {
|
|
76
|
+
// 1. Explicit conversation ID
|
|
77
|
+
if (message.conversation_id) {
|
|
78
|
+
const existing = this.storage.getConversation(message.conversation_id);
|
|
79
|
+
if (existing) return existing.id;
|
|
80
|
+
// Create it if it doesn't exist
|
|
81
|
+
return this.createConversation(
|
|
82
|
+
message.scope,
|
|
83
|
+
message.subject,
|
|
84
|
+
message.conversation_id
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. thread_tag → conversation
|
|
89
|
+
if (message.thread_tag) {
|
|
90
|
+
const key = `${message.thread_tag}:${message.scope}`;
|
|
91
|
+
const existing = this.tagToConversation.get(key);
|
|
92
|
+
if (existing) return existing;
|
|
93
|
+
|
|
94
|
+
const convId = this.createConversation(
|
|
95
|
+
message.scope,
|
|
96
|
+
message.thread_tag
|
|
97
|
+
);
|
|
98
|
+
this.tagToConversation.set(key, convId);
|
|
99
|
+
return convId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. Catch-all for scope
|
|
103
|
+
const existing = this.scopeCatchAll.get(message.scope);
|
|
104
|
+
if (existing) return existing;
|
|
105
|
+
|
|
106
|
+
const convId = this.createConversation(message.scope, "General");
|
|
107
|
+
this.scopeCatchAll.set(message.scope, convId);
|
|
108
|
+
return convId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private createConversation(
|
|
112
|
+
scope: string,
|
|
113
|
+
subject?: string,
|
|
114
|
+
explicitId?: string
|
|
115
|
+
): string {
|
|
116
|
+
const now = new Date().toISOString();
|
|
117
|
+
const conv: Conversation = {
|
|
118
|
+
id: explicitId ?? `conv-${ulid()}`,
|
|
119
|
+
scope,
|
|
120
|
+
subject,
|
|
121
|
+
status: "active",
|
|
122
|
+
participants: [],
|
|
123
|
+
metadata: {},
|
|
124
|
+
created_at: now,
|
|
125
|
+
updated_at: now,
|
|
126
|
+
};
|
|
127
|
+
this.storage.putConversation(conv);
|
|
128
|
+
return conv.id;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private ensureParticipant(conversationId: string, agentId: string): void {
|
|
132
|
+
const conv = this.storage.getConversation(conversationId);
|
|
133
|
+
if (!conv) return;
|
|
134
|
+
if (conv.participants.some((p) => p.agent_id === agentId)) return;
|
|
135
|
+
conv.participants.push({
|
|
136
|
+
agent_id: agentId,
|
|
137
|
+
joined_at: new Date().toISOString(),
|
|
138
|
+
});
|
|
139
|
+
this.storage.putConversation(conv);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private resolveThread(
|
|
143
|
+
message: Message,
|
|
144
|
+
conversationId: string,
|
|
145
|
+
turnId: string
|
|
146
|
+
): string {
|
|
147
|
+
// Find the root of the reply chain
|
|
148
|
+
const rootMessageId = this.findRootMessage(message.in_reply_to!);
|
|
149
|
+
|
|
150
|
+
const existingThreadId = this.replyChainToThread.get(rootMessageId);
|
|
151
|
+
if (existingThreadId) return existingThreadId;
|
|
152
|
+
|
|
153
|
+
// Create new thread
|
|
154
|
+
const rootTurnId =
|
|
155
|
+
this.findTurnForMessage(rootMessageId) ?? turnId;
|
|
156
|
+
const thread: Thread = {
|
|
157
|
+
id: `thread-${ulid()}`,
|
|
158
|
+
conversation_id: conversationId,
|
|
159
|
+
root_turn_id: rootTurnId,
|
|
160
|
+
subject: message.subject,
|
|
161
|
+
created_at: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
this.storage.putThread(thread);
|
|
164
|
+
this.replyChainToThread.set(rootMessageId, thread.id);
|
|
165
|
+
return thread.id;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private findRootMessage(messageId: string): string {
|
|
169
|
+
const msg = this.storage.getMessage(messageId);
|
|
170
|
+
if (!msg || !msg.in_reply_to) return messageId;
|
|
171
|
+
return this.findRootMessage(msg.in_reply_to);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private findTurnForMessage(messageId: string): string | undefined {
|
|
175
|
+
// Look through all conversations for a turn with this source_message_id
|
|
176
|
+
for (const conv of this.storage.listConversations()) {
|
|
177
|
+
const turns = this.storage.getTurns(conv.id);
|
|
178
|
+
const turn = turns.find((t) => t.source_message_id === messageId);
|
|
179
|
+
if (turn) return turn.id;
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Core types for Agent Inbox
|
|
2
|
+
|
|
3
|
+
// --- Agent ---
|
|
4
|
+
|
|
5
|
+
export type AgentStatus = "active" | "idle" | "offline";
|
|
6
|
+
|
|
7
|
+
export interface Agent {
|
|
8
|
+
agent_id: string;
|
|
9
|
+
display_name?: string;
|
|
10
|
+
program?: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
scope: string;
|
|
13
|
+
status: AgentStatus;
|
|
14
|
+
metadata: Record<string, unknown>;
|
|
15
|
+
registered_at: string;
|
|
16
|
+
last_active_at: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --- Message Content ---
|
|
20
|
+
|
|
21
|
+
export type MessageContent =
|
|
22
|
+
| { type: "text"; text: string }
|
|
23
|
+
| { type: "data"; schema?: string; data: unknown }
|
|
24
|
+
| { type: "event"; event: string; data?: unknown }
|
|
25
|
+
| { type: "reference"; uri: string; label?: string }
|
|
26
|
+
| { type: string; [key: string]: unknown };
|
|
27
|
+
|
|
28
|
+
export type RecipientKind = "to" | "cc" | "bcc";
|
|
29
|
+
export type Importance = "low" | "normal" | "high" | "urgent";
|
|
30
|
+
|
|
31
|
+
export interface Recipient {
|
|
32
|
+
agent_id: string;
|
|
33
|
+
kind: RecipientKind;
|
|
34
|
+
delivered_at?: string;
|
|
35
|
+
read_at?: string;
|
|
36
|
+
ack_at?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Message {
|
|
40
|
+
id: string;
|
|
41
|
+
scope: string;
|
|
42
|
+
sender_id: string;
|
|
43
|
+
recipients: Recipient[];
|
|
44
|
+
subject?: string;
|
|
45
|
+
content: MessageContent;
|
|
46
|
+
thread_tag?: string;
|
|
47
|
+
in_reply_to?: string;
|
|
48
|
+
conversation_id?: string;
|
|
49
|
+
importance: Importance;
|
|
50
|
+
metadata: Record<string, unknown>;
|
|
51
|
+
created_at: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Traceability ---
|
|
55
|
+
|
|
56
|
+
export type ConversationStatus = "active" | "completed" | "archived";
|
|
57
|
+
|
|
58
|
+
export interface Participant {
|
|
59
|
+
agent_id: string;
|
|
60
|
+
role?: string;
|
|
61
|
+
joined_at: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Conversation {
|
|
65
|
+
id: string;
|
|
66
|
+
scope: string;
|
|
67
|
+
subject?: string;
|
|
68
|
+
status: ConversationStatus;
|
|
69
|
+
participants: Participant[];
|
|
70
|
+
metadata: Record<string, unknown>;
|
|
71
|
+
created_at: string;
|
|
72
|
+
updated_at: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type ContentType = "text" | "data" | "event" | "reference" | string;
|
|
76
|
+
|
|
77
|
+
export interface Turn {
|
|
78
|
+
id: string;
|
|
79
|
+
conversation_id: string;
|
|
80
|
+
participant_id: string;
|
|
81
|
+
source_message_id?: string;
|
|
82
|
+
content_type: ContentType;
|
|
83
|
+
content: MessageContent;
|
|
84
|
+
thread_id?: string;
|
|
85
|
+
in_reply_to?: string;
|
|
86
|
+
created_at: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Thread {
|
|
90
|
+
id: string;
|
|
91
|
+
conversation_id: string;
|
|
92
|
+
root_turn_id: string;
|
|
93
|
+
parent_thread_id?: string;
|
|
94
|
+
subject?: string;
|
|
95
|
+
created_at: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- IPC Protocol ---
|
|
99
|
+
|
|
100
|
+
export interface IpcSendCommand {
|
|
101
|
+
action: "send";
|
|
102
|
+
from: string;
|
|
103
|
+
to: string | string[] | { agent_id: string; kind?: RecipientKind }[];
|
|
104
|
+
payload: unknown;
|
|
105
|
+
scope?: string;
|
|
106
|
+
threadTag?: string;
|
|
107
|
+
inReplyTo?: string;
|
|
108
|
+
importance?: Importance;
|
|
109
|
+
meta?: Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface IpcEmitCommand {
|
|
113
|
+
action: "emit";
|
|
114
|
+
event: unknown;
|
|
115
|
+
meta?: Record<string, unknown>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface IpcNotifyCommand {
|
|
119
|
+
action: "notify";
|
|
120
|
+
event: {
|
|
121
|
+
type: string;
|
|
122
|
+
agent?: {
|
|
123
|
+
agentId: string;
|
|
124
|
+
name?: string;
|
|
125
|
+
role?: string;
|
|
126
|
+
scopes?: string[];
|
|
127
|
+
metadata?: Record<string, unknown>;
|
|
128
|
+
};
|
|
129
|
+
agentId?: string;
|
|
130
|
+
reason?: string;
|
|
131
|
+
[key: string]: unknown;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface IpcPingCommand {
|
|
136
|
+
action: "ping";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type IpcCommand =
|
|
140
|
+
| IpcSendCommand
|
|
141
|
+
| IpcEmitCommand
|
|
142
|
+
| IpcNotifyCommand
|
|
143
|
+
| IpcPingCommand;
|
|
144
|
+
|
|
145
|
+
export interface IpcResponse {
|
|
146
|
+
ok: boolean;
|
|
147
|
+
messageId?: string;
|
|
148
|
+
error?: string;
|
|
149
|
+
pid?: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Federation ---
|
|
153
|
+
|
|
154
|
+
export type WarmAgentStatus = "active" | "away" | "expired";
|
|
155
|
+
|
|
156
|
+
export interface AgentRegistryConfig {
|
|
157
|
+
/** Time after disconnect before away → expired (default: 60000ms) */
|
|
158
|
+
gracePeriodMs: number;
|
|
159
|
+
/** How long to keep expired entries for audit (default: 3600000ms) */
|
|
160
|
+
retainExpiredMs: number;
|
|
161
|
+
/** Flush queued messages when agent reconnects (default: true) */
|
|
162
|
+
requeueOnReconnect: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface FederatedAddress {
|
|
166
|
+
agent?: string;
|
|
167
|
+
system?: string;
|
|
168
|
+
scope?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface FederationPeerConfig {
|
|
172
|
+
systemId: string;
|
|
173
|
+
url: string;
|
|
174
|
+
auth?: FederationAuth;
|
|
175
|
+
exposure?: ExposurePolicy;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface FederationAuth {
|
|
179
|
+
method: "bearer" | "api-key" | "mtls" | "did:wba" | "none";
|
|
180
|
+
token?: string;
|
|
181
|
+
key?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type ExposureLevel = "none" | "gateway" | "tagged" | "all";
|
|
185
|
+
|
|
186
|
+
export interface ExposurePolicy {
|
|
187
|
+
agents: ExposureLevel;
|
|
188
|
+
scopes?: string[];
|
|
189
|
+
events?: ExposureLevel;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface FederationLink {
|
|
193
|
+
peerId: string;
|
|
194
|
+
sessionId: string;
|
|
195
|
+
status: "connected" | "disconnected" | "authenticating";
|
|
196
|
+
exposure: ExposurePolicy;
|
|
197
|
+
url: string;
|
|
198
|
+
connectedAt?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface SystemId {
|
|
202
|
+
id: string;
|
|
203
|
+
source: "config" | "map" | "auto";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface FederationRoutingConfig {
|
|
207
|
+
strategy: "table" | "broadcast" | "hierarchical";
|
|
208
|
+
/** Routing entry TTL in ms (default: 300000) */
|
|
209
|
+
tableTTL?: number;
|
|
210
|
+
/** Re-query peers on cache miss before failing (default: true) */
|
|
211
|
+
refreshOnMiss?: boolean;
|
|
212
|
+
/** Ms to wait for first responder in broadcast mode (default: 5000) */
|
|
213
|
+
broadcastTimeout?: number;
|
|
214
|
+
/** System IDs of upstream hubs for hierarchical routing */
|
|
215
|
+
upstream?: string[];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface DeliveryQueueConfig {
|
|
219
|
+
persistence: "memory" | "sqlite";
|
|
220
|
+
/** Max age before dropping in ms (default: 86400000 / 24h) */
|
|
221
|
+
maxTTL: number;
|
|
222
|
+
/** Max messages per destination (default: 10000) */
|
|
223
|
+
maxQueueSize: number;
|
|
224
|
+
retryStrategy: "exponential" | "fixed";
|
|
225
|
+
/** Base retry interval in ms (default: 1000) */
|
|
226
|
+
retryBaseInterval: number;
|
|
227
|
+
/** Max retries, 0 = unlimited until TTL (default: 0) */
|
|
228
|
+
retryMaxAttempts: number;
|
|
229
|
+
/** Drain queue when connection restored (default: true) */
|
|
230
|
+
flushOnReconnect: boolean;
|
|
231
|
+
overflow: "drop-oldest" | "drop-newest" | "reject-new";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface FederationTrustPolicy {
|
|
235
|
+
/** System IDs or URLs allowed to connect (Layer 1 — implemented) */
|
|
236
|
+
allowedServers: string[];
|
|
237
|
+
/** Local scope → allowed remote scopes (Layer 2 — stub) */
|
|
238
|
+
scopePermissions: Record<string, string[]>;
|
|
239
|
+
/** Require tokens for cross-server delivery (Layer 3 — stub) */
|
|
240
|
+
requireAuth: boolean;
|
|
241
|
+
authMethod?: "bearer" | "api-key" | "mtls" | "did:wba";
|
|
242
|
+
authTokens?: Record<string, string>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface FederationConfig {
|
|
246
|
+
systemId?: string;
|
|
247
|
+
peers?: FederationPeerConfig[];
|
|
248
|
+
routing?: Partial<FederationRoutingConfig>;
|
|
249
|
+
deliveryQueue?: Partial<DeliveryQueueConfig>;
|
|
250
|
+
trust?: Partial<FederationTrustPolicy>;
|
|
251
|
+
registry?: Partial<AgentRegistryConfig>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface RoutingEntry {
|
|
255
|
+
agentId: string;
|
|
256
|
+
peerId: string;
|
|
257
|
+
lastSeen: string;
|
|
258
|
+
status: WarmAgentStatus;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface QueuedMessage {
|
|
262
|
+
id: string;
|
|
263
|
+
peerId: string;
|
|
264
|
+
message: Message;
|
|
265
|
+
enqueuedAt: string;
|
|
266
|
+
attempts: number;
|
|
267
|
+
lastAttempt?: string;
|
|
268
|
+
nextRetry?: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- Config ---
|
|
272
|
+
|
|
273
|
+
export interface InboxConfig {
|
|
274
|
+
socketPath?: string;
|
|
275
|
+
scope?: string;
|
|
276
|
+
map?: {
|
|
277
|
+
enabled: boolean;
|
|
278
|
+
server?: string;
|
|
279
|
+
scope?: string;
|
|
280
|
+
systemId?: string;
|
|
281
|
+
auth?: {
|
|
282
|
+
token?: string;
|
|
283
|
+
param?: string;
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
federation?: FederationConfig;
|
|
287
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
parseAddress,
|
|
4
|
+
formatAddress,
|
|
5
|
+
isRemoteAddress,
|
|
6
|
+
isBroadcastAddress,
|
|
7
|
+
} from "../../src/federation/address.js";
|
|
8
|
+
|
|
9
|
+
describe("parseAddress", () => {
|
|
10
|
+
it("should parse local agent ID (no @)", () => {
|
|
11
|
+
expect(parseAddress("agent-alpha")).toEqual({ agent: "agent-alpha" });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should parse federated address (agent@system)", () => {
|
|
15
|
+
expect(parseAddress("agent-beta@backend-team")).toEqual({
|
|
16
|
+
agent: "agent-beta",
|
|
17
|
+
system: "backend-team",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should parse broadcast address (@system)", () => {
|
|
22
|
+
expect(parseAddress("@backend-team")).toEqual({
|
|
23
|
+
agent: undefined,
|
|
24
|
+
system: "backend-team",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should parse address with scope (agent@system/scope)", () => {
|
|
29
|
+
expect(parseAddress("agent-gamma@ml-team/training")).toEqual({
|
|
30
|
+
agent: "agent-gamma",
|
|
31
|
+
system: "ml-team",
|
|
32
|
+
scope: "training",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should parse broadcast with scope (@system/scope)", () => {
|
|
37
|
+
expect(parseAddress("@ml-team/training")).toEqual({
|
|
38
|
+
agent: undefined,
|
|
39
|
+
system: "ml-team",
|
|
40
|
+
scope: "training",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("formatAddress", () => {
|
|
46
|
+
it("should format local address", () => {
|
|
47
|
+
expect(formatAddress({ agent: "agent-alpha" })).toBe("agent-alpha");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should format federated address", () => {
|
|
51
|
+
expect(
|
|
52
|
+
formatAddress({ agent: "agent-beta", system: "backend-team" })
|
|
53
|
+
).toBe("agent-beta@backend-team");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should format broadcast address", () => {
|
|
57
|
+
expect(formatAddress({ system: "backend-team" })).toBe("@backend-team");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should format address with scope", () => {
|
|
61
|
+
expect(
|
|
62
|
+
formatAddress({
|
|
63
|
+
agent: "agent-gamma",
|
|
64
|
+
system: "ml-team",
|
|
65
|
+
scope: "training",
|
|
66
|
+
})
|
|
67
|
+
).toBe("agent-gamma@ml-team/training");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("isRemoteAddress", () => {
|
|
72
|
+
it("should return false for local address", () => {
|
|
73
|
+
expect(isRemoteAddress({ agent: "local-agent" })).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should return true for federated address", () => {
|
|
77
|
+
expect(
|
|
78
|
+
isRemoteAddress({ agent: "remote-agent", system: "other-system" })
|
|
79
|
+
).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should return true for broadcast address", () => {
|
|
83
|
+
expect(isRemoteAddress({ system: "other-system" })).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("isBroadcastAddress", () => {
|
|
88
|
+
it("should return false for local address", () => {
|
|
89
|
+
expect(isBroadcastAddress({ agent: "local-agent" })).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should return false for targeted federated address", () => {
|
|
93
|
+
expect(
|
|
94
|
+
isBroadcastAddress({ agent: "remote-agent", system: "other-system" })
|
|
95
|
+
).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should return true for system-only address", () => {
|
|
99
|
+
expect(isBroadcastAddress({ system: "other-system" })).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|