a2a-xmtp 1.4.6 → 2.0.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/dist/index.d.ts +11 -0
- package/dist/index.js +13 -0
- package/package.json +13 -10
- package/src/coordination/group-scheduler.ts +0 -193
- package/src/coordination/message-orchestrator.ts +0 -383
- package/src/coordination/policy-engine.ts +0 -134
- package/src/index.ts +0 -230
- package/src/tools/xmtp-agents.ts +0 -38
- package/src/tools/xmtp-group.ts +0 -153
- package/src/tools/xmtp-inbox.ts +0 -47
- package/src/tools/xmtp-send.ts +0 -77
- package/src/transport/identity-registry.ts +0 -133
- package/src/transport/xmtp-bridge.ts +0 -370
- package/src/types.ts +0 -192
package/src/index.ts
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Module 1: Plugin 入口
|
|
3
|
-
// 使用 OpenClaw Plugin SDK 真实 API
|
|
4
|
-
// ============================================================
|
|
5
|
-
|
|
6
|
-
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
7
|
-
import { Type } from "@sinclair/typebox";
|
|
8
|
-
import { mkdirSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { IdentityRegistry } from "./transport/identity-registry.js";
|
|
11
|
-
import { PolicyEngine } from "./coordination/policy-engine.js";
|
|
12
|
-
import { GroupScheduler } from "./coordination/group-scheduler.js";
|
|
13
|
-
import { MessageOrchestrator } from "./coordination/message-orchestrator.js";
|
|
14
|
-
import { XmtpBridge } from "./transport/xmtp-bridge.js";
|
|
15
|
-
import { handleXmtpSend } from "./tools/xmtp-send.js";
|
|
16
|
-
import { handleXmtpInbox } from "./tools/xmtp-inbox.js";
|
|
17
|
-
import { handleDiscoverAgents } from "./tools/xmtp-agents.js";
|
|
18
|
-
import { handleXmtpGroup } from "./tools/xmtp-group.js";
|
|
19
|
-
import { DEFAULT_PLUGIN_CONFIG, type PluginConfig } from "./types.js";
|
|
20
|
-
|
|
21
|
-
const AGENT_ID = "main"; // OpenClaw 默认 agent
|
|
22
|
-
|
|
23
|
-
// Module-level singletons: OpenClaw may call register() multiple times from different
|
|
24
|
-
// subsystems (gateway, plugins) within the same process. Shared state must live at
|
|
25
|
-
// module scope so all closures (tools, HTTP routes, service) reference the same instances.
|
|
26
|
-
const bridges = new Map<string, XmtpBridge>();
|
|
27
|
-
let registry: IdentityRegistry;
|
|
28
|
-
let policyEngine: PolicyEngine;
|
|
29
|
-
|
|
30
|
-
export default definePluginEntry({
|
|
31
|
-
id: "a2a-xmtp",
|
|
32
|
-
name: "Agent-to-Agent IM (XMTP)",
|
|
33
|
-
description: "Decentralized Agent-to-Agent E2EE messaging powered by XMTP protocol",
|
|
34
|
-
|
|
35
|
-
register(api) {
|
|
36
|
-
|
|
37
|
-
// ── 1. 注册 Tools ──
|
|
38
|
-
|
|
39
|
-
api.registerTool({
|
|
40
|
-
name: "xmtp_send",
|
|
41
|
-
label: "Send XMTP Message",
|
|
42
|
-
description:
|
|
43
|
-
"Send an E2EE message to another agent via XMTP. " +
|
|
44
|
-
"Supports cross-gateway and cross-organization communication.",
|
|
45
|
-
parameters: Type.Object({
|
|
46
|
-
to: Type.Optional(Type.String({ description: "Target agent ID or XMTP address (0x...). Optional when conversationId is provided." })),
|
|
47
|
-
message: Type.String({ description: "Message content to send" }),
|
|
48
|
-
conversationId: Type.Optional(Type.String({ description: "Reuse existing conversation. When set, 'to' is optional." })),
|
|
49
|
-
contentType: Type.Optional(
|
|
50
|
-
Type.String({ description: "Message type: text or markdown", default: "text" }),
|
|
51
|
-
),
|
|
52
|
-
}),
|
|
53
|
-
async execute(_toolCallId, params) {
|
|
54
|
-
const p = params as { to?: string; message: string; conversationId?: string; contentType?: "text" | "markdown" };
|
|
55
|
-
return await handleXmtpSend(bridges, registry, policyEngine, p, AGENT_ID);
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
api.registerTool({
|
|
60
|
-
name: "xmtp_inbox",
|
|
61
|
-
label: "XMTP Inbox",
|
|
62
|
-
description:
|
|
63
|
-
"Check your XMTP inbox for messages from other agents. " +
|
|
64
|
-
"Messages are E2E encrypted and only you can read them.",
|
|
65
|
-
parameters: Type.Object({
|
|
66
|
-
limit: Type.Optional(Type.Number({ description: "Max messages to return (default: 10)" })),
|
|
67
|
-
from: Type.Optional(Type.String({ description: "Filter by sender agent ID or address" })),
|
|
68
|
-
}),
|
|
69
|
-
async execute(_toolCallId, params) {
|
|
70
|
-
const p = params as { limit?: number; from?: string };
|
|
71
|
-
return await handleXmtpInbox(bridges, registry, p, AGENT_ID);
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
api.registerTool({
|
|
76
|
-
name: "xmtp_agents",
|
|
77
|
-
label: "Discover XMTP Agents",
|
|
78
|
-
description:
|
|
79
|
-
"Discover agents available for XMTP communication. " +
|
|
80
|
-
"Lists registered agents and their connection status.",
|
|
81
|
-
parameters: Type.Object({
|
|
82
|
-
includeExternal: Type.Optional(
|
|
83
|
-
Type.Boolean({ description: "Include ERC-8004 external registry (Phase 3)" }),
|
|
84
|
-
),
|
|
85
|
-
}),
|
|
86
|
-
async execute(_toolCallId, params) {
|
|
87
|
-
const p = params as { includeExternal?: boolean };
|
|
88
|
-
return await handleDiscoverAgents(bridges, registry, p);
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
api.registerTool({
|
|
93
|
-
name: "xmtp_group",
|
|
94
|
-
label: "XMTP Group Management",
|
|
95
|
-
description:
|
|
96
|
-
"Manage XMTP group conversations. Actions: create (new group), list (all groups), " +
|
|
97
|
-
"members (view members), add_member, remove_member.",
|
|
98
|
-
parameters: Type.Object({
|
|
99
|
-
action: Type.String({ description: "Action: create | list | members | add_member | remove_member" }),
|
|
100
|
-
members: Type.Optional(Type.Array(Type.String(), { description: "Agent IDs or 0x addresses (for create/add_member/remove_member)" })),
|
|
101
|
-
conversationId: Type.Optional(Type.String({ description: "Group conversation ID (for members/add_member/remove_member)" })),
|
|
102
|
-
name: Type.Optional(Type.String({ description: "Group name (for create)" })),
|
|
103
|
-
}),
|
|
104
|
-
async execute(_toolCallId, params) {
|
|
105
|
-
const p = params as { action: string; members?: string[]; conversationId?: string; name?: string };
|
|
106
|
-
return await handleXmtpGroup(bridges, registry, p, AGENT_ID);
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// ── 2. 注册 HTTP 状态路由 ──
|
|
111
|
-
|
|
112
|
-
api.registerHttpRoute({
|
|
113
|
-
path: "/a2a-xmtp/status",
|
|
114
|
-
auth: "gateway",
|
|
115
|
-
handler: async (_req, res) => {
|
|
116
|
-
const status = {
|
|
117
|
-
plugin: "a2a-xmtp",
|
|
118
|
-
bridgeCount: bridges.size,
|
|
119
|
-
agents: Array.from(bridges.entries()).map(([id, b]) => ({
|
|
120
|
-
agentId: id,
|
|
121
|
-
xmtpAddress: b.address,
|
|
122
|
-
connected: b.isConnected,
|
|
123
|
-
env: b.env,
|
|
124
|
-
})),
|
|
125
|
-
policy: policyEngine?.getPolicy(),
|
|
126
|
-
};
|
|
127
|
-
res.setHeader("Content-Type", "application/json");
|
|
128
|
-
res.end(JSON.stringify(status, null, 2));
|
|
129
|
-
return true;
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// ── 3. 注册 Service(生命周期管理) ──
|
|
134
|
-
|
|
135
|
-
api.registerService({
|
|
136
|
-
id: "a2a-xmtp-bridge",
|
|
137
|
-
async start(ctx) {
|
|
138
|
-
ctx.logger.info("[a2a-xmtp] Starting XMTP Bridge Service...");
|
|
139
|
-
|
|
140
|
-
// 加载配置(所有选项均有默认值,用户无需手动编辑配置文件)
|
|
141
|
-
const pluginCfg = (ctx.config as any)?.plugins?.entries?.["a2a-xmtp"]?.config;
|
|
142
|
-
const defaultDbPath = join(ctx.stateDir, "xmtp-data");
|
|
143
|
-
const config: PluginConfig = {
|
|
144
|
-
xmtp: {
|
|
145
|
-
env: pluginCfg?.xmtp?.env ?? DEFAULT_PLUGIN_CONFIG.xmtp.env,
|
|
146
|
-
dbPath: pluginCfg?.xmtp?.dbPath ?? defaultDbPath,
|
|
147
|
-
},
|
|
148
|
-
policy: {
|
|
149
|
-
maxTurns: pluginCfg?.policy?.maxTurns ?? DEFAULT_PLUGIN_CONFIG.policy.maxTurns,
|
|
150
|
-
minIntervalMs: pluginCfg?.policy?.minIntervalMs ?? DEFAULT_PLUGIN_CONFIG.policy.minIntervalMs,
|
|
151
|
-
ttlMinutes: pluginCfg?.policy?.ttlMinutes ?? DEFAULT_PLUGIN_CONFIG.policy.ttlMinutes,
|
|
152
|
-
consentMode: pluginCfg?.policy?.consentMode ?? DEFAULT_PLUGIN_CONFIG.policy.consentMode,
|
|
153
|
-
},
|
|
154
|
-
groupScheduling: {
|
|
155
|
-
baseDelayMs: pluginCfg?.groupScheduling?.baseDelayMs ?? DEFAULT_PLUGIN_CONFIG.groupScheduling.baseDelayMs,
|
|
156
|
-
slotTimeoutMs: pluginCfg?.groupScheduling?.slotTimeoutMs ?? DEFAULT_PLUGIN_CONFIG.groupScheduling.slotTimeoutMs,
|
|
157
|
-
claimExpireMs: pluginCfg?.groupScheduling?.claimExpireMs ?? DEFAULT_PLUGIN_CONFIG.groupScheduling.claimExpireMs,
|
|
158
|
-
},
|
|
159
|
-
walletKey: pluginCfg?.walletKey,
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// 自动创建数据目录
|
|
163
|
-
mkdirSync(config.xmtp.dbPath, { recursive: true });
|
|
164
|
-
|
|
165
|
-
// 初始化核心组件
|
|
166
|
-
registry = new IdentityRegistry(ctx.stateDir, config.xmtp.env);
|
|
167
|
-
policyEngine = new PolicyEngine(config.policy);
|
|
168
|
-
|
|
169
|
-
// 初始化群聊调度器和消息编排器
|
|
170
|
-
const groupScheduler = new GroupScheduler(config.groupScheduling);
|
|
171
|
-
const orchestrator = new MessageOrchestrator(
|
|
172
|
-
api.runtime.subagent,
|
|
173
|
-
ctx.logger,
|
|
174
|
-
policyEngine,
|
|
175
|
-
groupScheduler,
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
// 初始化主 Agent 的 XMTP Bridge
|
|
179
|
-
try {
|
|
180
|
-
const walletConfig = await registry.initAgent(AGENT_ID, config.walletKey);
|
|
181
|
-
policyEngine.registerLocalAgent(AGENT_ID);
|
|
182
|
-
|
|
183
|
-
const bridge = new XmtpBridge(
|
|
184
|
-
AGENT_ID,
|
|
185
|
-
walletConfig,
|
|
186
|
-
policyEngine,
|
|
187
|
-
registry,
|
|
188
|
-
config.xmtp.dbPath,
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
// 消息回调:委托给 orchestrator 处理
|
|
192
|
-
bridge.onMessage = (agentId, payload) =>
|
|
193
|
-
orchestrator.handleMessage(bridge, agentId, payload);
|
|
194
|
-
|
|
195
|
-
// Claim 回调:通知 orchestrator 取消 failover 等待
|
|
196
|
-
bridge.onClaim = (conversationId, senderAddress, messageId) =>
|
|
197
|
-
orchestrator.handleClaim(conversationId, senderAddress, messageId);
|
|
198
|
-
|
|
199
|
-
// 群聊 self 消息计数回调:保持 messageCount 同步
|
|
200
|
-
bridge.onSelfGroupMessage = (conversationId) =>
|
|
201
|
-
orchestrator.handleSelfGroupMessage(conversationId);
|
|
202
|
-
|
|
203
|
-
await bridge.start();
|
|
204
|
-
bridges.set(AGENT_ID, bridge);
|
|
205
|
-
|
|
206
|
-
ctx.logger.info(
|
|
207
|
-
`[a2a-xmtp] Bridge started: ${AGENT_ID} → ${bridge.address} (env: ${config.xmtp.env})`,
|
|
208
|
-
);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
ctx.logger.error(
|
|
211
|
-
`[a2a-xmtp] Failed to start bridge: ${err instanceof Error ? err.message : String(err)}`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
async stop(ctx) {
|
|
217
|
-
ctx.logger.info("[a2a-xmtp] Stopping XMTP bridges...");
|
|
218
|
-
for (const [id, bridge] of bridges) {
|
|
219
|
-
try {
|
|
220
|
-
await bridge.stop();
|
|
221
|
-
ctx.logger.info(`[a2a-xmtp] Bridge stopped: ${id}`);
|
|
222
|
-
} catch (err) {
|
|
223
|
-
ctx.logger.error(`[a2a-xmtp] Error stopping bridge ${id}: ${err}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
bridges.clear();
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
},
|
|
230
|
-
});
|
package/src/tools/xmtp-agents.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Tool: xmtp_agents — 发现可通信的 Agent
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import type { XmtpBridge } from "../transport/xmtp-bridge.js";
|
|
6
|
-
import type { IdentityRegistry } from "../transport/identity-registry.js";
|
|
7
|
-
|
|
8
|
-
export async function handleDiscoverAgents(
|
|
9
|
-
bridges: Map<string, XmtpBridge>,
|
|
10
|
-
registry: IdentityRegistry,
|
|
11
|
-
params: { includeExternal?: boolean },
|
|
12
|
-
) {
|
|
13
|
-
const localAgents = registry.listAgents();
|
|
14
|
-
|
|
15
|
-
const lines = localAgents.map((agent) => {
|
|
16
|
-
const bridge = bridges.get(agent.agentId);
|
|
17
|
-
const status = bridge?.isConnected ? "online" : "offline";
|
|
18
|
-
return `- ${agent.agentId} (${agent.address}) [${status}]`;
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
let text = `Available agents (${localAgents.length}):\n${lines.join("\n")}`;
|
|
22
|
-
|
|
23
|
-
if (params.includeExternal) {
|
|
24
|
-
text += "\n\nNote: ERC-8004 external registry lookup is planned for Phase 3.";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
content: [{ type: "text" as const, text }],
|
|
29
|
-
details: {
|
|
30
|
-
count: localAgents.length,
|
|
31
|
-
agents: localAgents.map((a) => ({
|
|
32
|
-
agentId: a.agentId,
|
|
33
|
-
address: a.address,
|
|
34
|
-
connected: bridges.get(a.agentId)?.isConnected ?? false,
|
|
35
|
-
})),
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|
package/src/tools/xmtp-group.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Tool: xmtp_group — 群聊管理(创建、列表、成员查看/添加/移除)
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import type { XmtpBridge } from "../transport/xmtp-bridge.js";
|
|
6
|
-
import type { IdentityRegistry } from "../transport/identity-registry.js";
|
|
7
|
-
|
|
8
|
-
type GroupAction = "create" | "list" | "members" | "add_member" | "remove_member";
|
|
9
|
-
|
|
10
|
-
export async function handleXmtpGroup(
|
|
11
|
-
bridges: Map<string, XmtpBridge>,
|
|
12
|
-
registry: IdentityRegistry,
|
|
13
|
-
params: {
|
|
14
|
-
action: string;
|
|
15
|
-
members?: string[];
|
|
16
|
-
conversationId?: string;
|
|
17
|
-
name?: string;
|
|
18
|
-
},
|
|
19
|
-
agentId: string,
|
|
20
|
-
) {
|
|
21
|
-
const bridge = bridges.get(agentId) ?? bridges.values().next().value;
|
|
22
|
-
if (!bridge) {
|
|
23
|
-
return { content: [{ type: "text" as const, text: "No XMTP bridge available. Plugin not initialized." }], details: null };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const action = params.action as GroupAction;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
switch (action) {
|
|
30
|
-
case "create": {
|
|
31
|
-
if (!params.members?.length) {
|
|
32
|
-
return { content: [{ type: "text" as const, text: "Parameter 'members' is required for create action. Provide agent IDs or 0x addresses." }], details: null };
|
|
33
|
-
}
|
|
34
|
-
// 解析成员地址
|
|
35
|
-
const addresses: string[] = [];
|
|
36
|
-
for (const member of params.members) {
|
|
37
|
-
if (member.startsWith("0x")) {
|
|
38
|
-
addresses.push(member);
|
|
39
|
-
} else {
|
|
40
|
-
const addr = registry.getAddress(member);
|
|
41
|
-
if (!addr) {
|
|
42
|
-
return { content: [{ type: "text" as const, text: `Agent "${member}" not found. Use xmtp_agents to discover available agents.` }], details: null };
|
|
43
|
-
}
|
|
44
|
-
addresses.push(addr);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const result = await bridge.createGroup(addresses, params.name);
|
|
49
|
-
return {
|
|
50
|
-
content: [{ type: "text" as const, text: `Group created${result.name ? ` "${result.name}"` : ""} with ${addresses.length} members.` }],
|
|
51
|
-
details: {
|
|
52
|
-
action: "create",
|
|
53
|
-
conversationId: result.conversationId,
|
|
54
|
-
name: result.name,
|
|
55
|
-
memberCount: addresses.length,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
case "list": {
|
|
61
|
-
const groups = await bridge.listGroups();
|
|
62
|
-
if (groups.length === 0) {
|
|
63
|
-
return { content: [{ type: "text" as const, text: "No group conversations found." }], details: null };
|
|
64
|
-
}
|
|
65
|
-
const lines = groups.map((g) => {
|
|
66
|
-
const nameLabel = g.name ? ` "${g.name}"` : "";
|
|
67
|
-
return `- ${g.conversationId}${nameLabel} (${g.memberAddresses.length} members, created ${g.createdAt})`;
|
|
68
|
-
});
|
|
69
|
-
return {
|
|
70
|
-
content: [{ type: "text" as const, text: `Groups:\n${lines.join("\n")}` }],
|
|
71
|
-
details: { action: "list", count: groups.length, groups },
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
case "members": {
|
|
76
|
-
if (!params.conversationId) {
|
|
77
|
-
return { content: [{ type: "text" as const, text: "Parameter 'conversationId' is required for members action." }], details: null };
|
|
78
|
-
}
|
|
79
|
-
const members = await bridge.getGroupMembers(params.conversationId);
|
|
80
|
-
// 尝试解析 agentId
|
|
81
|
-
const resolved = members.map((addr) => {
|
|
82
|
-
const aid = registry.getAgentId(addr);
|
|
83
|
-
return aid ? `${aid} (${addr})` : addr;
|
|
84
|
-
});
|
|
85
|
-
return {
|
|
86
|
-
content: [{ type: "text" as const, text: `Members (${members.length}):\n${resolved.map((m) => `- ${m}`).join("\n")}` }],
|
|
87
|
-
details: { action: "members", conversationId: params.conversationId, count: members.length, members },
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
case "add_member": {
|
|
92
|
-
if (!params.conversationId) {
|
|
93
|
-
return { content: [{ type: "text" as const, text: "Parameter 'conversationId' is required for add_member action." }], details: null };
|
|
94
|
-
}
|
|
95
|
-
if (!params.members?.length) {
|
|
96
|
-
return { content: [{ type: "text" as const, text: "Parameter 'members' is required for add_member action." }], details: null };
|
|
97
|
-
}
|
|
98
|
-
const addresses: string[] = [];
|
|
99
|
-
for (const member of params.members) {
|
|
100
|
-
if (member.startsWith("0x")) {
|
|
101
|
-
addresses.push(member);
|
|
102
|
-
} else {
|
|
103
|
-
const addr = registry.getAddress(member);
|
|
104
|
-
if (!addr) {
|
|
105
|
-
return { content: [{ type: "text" as const, text: `Agent "${member}" not found.` }], details: null };
|
|
106
|
-
}
|
|
107
|
-
addresses.push(addr);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
await bridge.addGroupMembers(params.conversationId, addresses);
|
|
111
|
-
return {
|
|
112
|
-
content: [{ type: "text" as const, text: `Added ${addresses.length} member(s) to group.` }],
|
|
113
|
-
details: { action: "add_member", conversationId: params.conversationId, added: addresses },
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
case "remove_member": {
|
|
118
|
-
if (!params.conversationId) {
|
|
119
|
-
return { content: [{ type: "text" as const, text: "Parameter 'conversationId' is required for remove_member action." }], details: null };
|
|
120
|
-
}
|
|
121
|
-
if (!params.members?.length) {
|
|
122
|
-
return { content: [{ type: "text" as const, text: "Parameter 'members' is required for remove_member action." }], details: null };
|
|
123
|
-
}
|
|
124
|
-
const addresses: string[] = [];
|
|
125
|
-
for (const member of params.members) {
|
|
126
|
-
if (member.startsWith("0x")) {
|
|
127
|
-
addresses.push(member);
|
|
128
|
-
} else {
|
|
129
|
-
const addr = registry.getAddress(member);
|
|
130
|
-
if (!addr) {
|
|
131
|
-
return { content: [{ type: "text" as const, text: `Agent "${member}" not found.` }], details: null };
|
|
132
|
-
}
|
|
133
|
-
addresses.push(addr);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
await bridge.removeGroupMembers(params.conversationId, addresses);
|
|
137
|
-
return {
|
|
138
|
-
content: [{ type: "text" as const, text: `Removed ${addresses.length} member(s) from group.` }],
|
|
139
|
-
details: { action: "remove_member", conversationId: params.conversationId, removed: addresses },
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
default:
|
|
144
|
-
return {
|
|
145
|
-
content: [{ type: "text" as const, text: `Unknown action "${action}". Supported: create, list, members, add_member, remove_member.` }],
|
|
146
|
-
details: null,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
} catch (err: unknown) {
|
|
150
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
-
return { content: [{ type: "text" as const, text: `Group operation failed: ${msg}` }], details: null };
|
|
152
|
-
}
|
|
153
|
-
}
|
package/src/tools/xmtp-inbox.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Tool: xmtp_inbox — 查看 XMTP 收件箱消息
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import type { XmtpBridge } from "../transport/xmtp-bridge.js";
|
|
6
|
-
import type { IdentityRegistry } from "../transport/identity-registry.js";
|
|
7
|
-
|
|
8
|
-
export async function handleXmtpInbox(
|
|
9
|
-
bridges: Map<string, XmtpBridge>,
|
|
10
|
-
registry: IdentityRegistry,
|
|
11
|
-
params: { limit?: number; from?: string },
|
|
12
|
-
agentId: string,
|
|
13
|
-
) {
|
|
14
|
-
const bridge = bridges.get(agentId) ?? bridges.values().next().value;
|
|
15
|
-
if (!bridge) {
|
|
16
|
-
return { content: [{ type: "text" as const, text: "No XMTP bridge available." }], details: null };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// 解析 from
|
|
20
|
-
let fromFilter = params.from;
|
|
21
|
-
if (fromFilter && !fromFilter.startsWith("0x")) {
|
|
22
|
-
const addr = registry.getAddress(fromFilter);
|
|
23
|
-
if (addr) fromFilter = addr;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const messages = await bridge.getInbox({ limit: params.limit ?? 10, from: fromFilter });
|
|
28
|
-
|
|
29
|
-
if (messages.length === 0) {
|
|
30
|
-
return { content: [{ type: "text" as const, text: "No messages in inbox." }], details: null };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const lines = messages.map((m, i) => {
|
|
34
|
-
const sender = m.from.agentId ?? m.from.xmtpAddress;
|
|
35
|
-
const groupTag = m.isGroup ? " [group]" : "";
|
|
36
|
-
return `${i + 1}. [${m.timestamp}]${groupTag} from ${sender}: ${m.content}`;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
41
|
-
details: { count: messages.length, myAddress: bridge.address },
|
|
42
|
-
};
|
|
43
|
-
} catch (err: unknown) {
|
|
44
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
-
return { content: [{ type: "text" as const, text: `Failed to fetch inbox: ${msg}` }], details: null };
|
|
46
|
-
}
|
|
47
|
-
}
|
package/src/tools/xmtp-send.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Tool: xmtp_send — 发送 E2EE 消息到另一个 Agent
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import type { XmtpBridge } from "../transport/xmtp-bridge.js";
|
|
6
|
-
import type { IdentityRegistry } from "../transport/identity-registry.js";
|
|
7
|
-
import type { PolicyEngine } from "../coordination/policy-engine.js";
|
|
8
|
-
|
|
9
|
-
export async function handleXmtpSend(
|
|
10
|
-
bridges: Map<string, XmtpBridge>,
|
|
11
|
-
registry: IdentityRegistry,
|
|
12
|
-
policyEngine: PolicyEngine,
|
|
13
|
-
params: { to?: string; message: string; conversationId?: string; contentType?: "text" | "markdown" },
|
|
14
|
-
senderAgentId: string,
|
|
15
|
-
) {
|
|
16
|
-
// 找到发送者 bridge(使用第一个可用的 bridge)
|
|
17
|
-
const bridge = bridges.get(senderAgentId) ?? bridges.values().next().value;
|
|
18
|
-
if (!bridge) {
|
|
19
|
-
return { content: [{ type: "text" as const, text: "No XMTP bridge available. Plugin not initialized." }], details: null };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 当提供 conversationId 时,to 可省略(直接向已有会话发送)
|
|
23
|
-
if (!params.to && !params.conversationId) {
|
|
24
|
-
return { content: [{ type: "text" as const, text: "Either 'to' or 'conversationId' must be provided." }], details: null };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 解析目标地址(仅在提供 to 时需要)
|
|
28
|
-
let targetAddress: string | undefined;
|
|
29
|
-
if (params.to) {
|
|
30
|
-
if (params.to.startsWith("0x")) {
|
|
31
|
-
targetAddress = params.to;
|
|
32
|
-
} else {
|
|
33
|
-
const addr = registry.getAddress(params.to);
|
|
34
|
-
if (!addr) {
|
|
35
|
-
return {
|
|
36
|
-
content: [{ type: "text" as const, text: `Agent "${params.to}" not found. Use xmtp_agents to discover available agents.` }],
|
|
37
|
-
details: null,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
targetAddress = addr;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (targetAddress.toLowerCase() === bridge.address.toLowerCase()) {
|
|
44
|
-
return { content: [{ type: "text" as const, text: "Cannot send message to yourself." }], details: null };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Policy 检查
|
|
49
|
-
const toLabel = params.to ?? params.conversationId!;
|
|
50
|
-
const convId = params.conversationId ?? `dm:${senderAgentId}:${params.to}`;
|
|
51
|
-
const check = policyEngine.checkOutgoing({ from: senderAgentId, to: toLabel, conversationId: convId });
|
|
52
|
-
if (!check.allowed) {
|
|
53
|
-
return { content: [{ type: "text" as const, text: `Blocked: ${check.reason}` }], details: null };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const result = await bridge.sendMessage(targetAddress ?? "", params.message, {
|
|
58
|
-
contentType: params.contentType,
|
|
59
|
-
conversationId: params.conversationId,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const recipientLabel = params.to ?? `conversation ${result.conversationId}`;
|
|
63
|
-
return {
|
|
64
|
-
content: [{ type: "text" as const, text: `Message sent to ${recipientLabel}.` }],
|
|
65
|
-
details: {
|
|
66
|
-
status: "sent",
|
|
67
|
-
conversationId: result.conversationId,
|
|
68
|
-
messageId: result.messageId,
|
|
69
|
-
to: params.to,
|
|
70
|
-
toAddress: targetAddress,
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
} catch (err: unknown) {
|
|
74
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
-
return { content: [{ type: "text" as const, text: `Failed to send: ${msg}` }], details: null };
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Module 3: Identity Registry
|
|
3
|
-
// agentId ↔ XMTP wallet address 双向映射,wallet key 生成/加载
|
|
4
|
-
// 使用文件系统持久化(stateDir)
|
|
5
|
-
// ============================================================
|
|
6
|
-
|
|
7
|
-
import { createUser, createSigner } from "@xmtp/agent-sdk";
|
|
8
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
9
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import type { XmtpWalletConfig, XmtpEnv } from "../types.js";
|
|
12
|
-
|
|
13
|
-
export class IdentityRegistry {
|
|
14
|
-
private agentToConfig = new Map<string, XmtpWalletConfig>();
|
|
15
|
-
private addressToAgent = new Map<string, string>();
|
|
16
|
-
private storePath: string;
|
|
17
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
stateDir: string,
|
|
20
|
-
private env: XmtpEnv = "dev",
|
|
21
|
-
) {
|
|
22
|
-
this.storePath = join(stateDir, "identities");
|
|
23
|
-
mkdirSync(this.storePath, { recursive: true });
|
|
24
|
-
this.loadFromDisk();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 为 Agent 初始化 XMTP wallet。
|
|
29
|
-
* 如果已有配置则加载,否则生成新的 wallet。
|
|
30
|
-
*/
|
|
31
|
-
async initAgent(agentId: string, existingKey?: string): Promise<XmtpWalletConfig> {
|
|
32
|
-
const cached = this.agentToConfig.get(agentId);
|
|
33
|
-
if (cached) return cached;
|
|
34
|
-
|
|
35
|
-
// 尝试从磁盘加载
|
|
36
|
-
const filePath = join(this.storePath, `${agentId}.json`);
|
|
37
|
-
if (existsSync(filePath)) {
|
|
38
|
-
const config: XmtpWalletConfig = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
39
|
-
this.cacheMapping(agentId, config);
|
|
40
|
-
return config;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 生成新 wallet 或使用提供的 key
|
|
44
|
-
let privateKey: string;
|
|
45
|
-
let address: string;
|
|
46
|
-
|
|
47
|
-
if (existingKey) {
|
|
48
|
-
privateKey = existingKey;
|
|
49
|
-
const account = privateKeyToAccount(existingKey as `0x${string}`);
|
|
50
|
-
address = account.address;
|
|
51
|
-
} else {
|
|
52
|
-
const user = createUser();
|
|
53
|
-
privateKey = user.key; // createUser() returns { key, account, wallet }
|
|
54
|
-
address = user.account.address;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const config: XmtpWalletConfig = {
|
|
58
|
-
privateKey,
|
|
59
|
-
address,
|
|
60
|
-
xmtpInboxId: "",
|
|
61
|
-
env: this.env,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// 持久化到磁盘
|
|
65
|
-
writeFileSync(filePath, JSON.stringify(config, null, 2));
|
|
66
|
-
this.cacheMapping(agentId, config);
|
|
67
|
-
|
|
68
|
-
return config;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** 更新 inboxId */
|
|
72
|
-
async updateInboxId(agentId: string, inboxId: string): Promise<void> {
|
|
73
|
-
const config = this.agentToConfig.get(agentId);
|
|
74
|
-
if (!config) return;
|
|
75
|
-
config.xmtpInboxId = inboxId;
|
|
76
|
-
const filePath = join(this.storePath, `${agentId}.json`);
|
|
77
|
-
writeFileSync(filePath, JSON.stringify(config, null, 2));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
getAddress(agentId: string): string | undefined {
|
|
81
|
-
return this.agentToConfig.get(agentId)?.address;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
getConfig(agentId: string): XmtpWalletConfig | undefined {
|
|
85
|
-
return this.agentToConfig.get(agentId);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
getAgentId(address: string): string | undefined {
|
|
89
|
-
return this.addressToAgent.get(address.toLowerCase());
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async resolveAgentId(address: string): Promise<string | null> {
|
|
93
|
-
return this.addressToAgent.get(address.toLowerCase()) ?? null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
listAgents(): Array<{ agentId: string; address: string; env: XmtpEnv }> {
|
|
97
|
-
return Array.from(this.agentToConfig.entries()).map(([id, config]) => ({
|
|
98
|
-
agentId: id,
|
|
99
|
-
address: config.address,
|
|
100
|
-
env: config.env,
|
|
101
|
-
}));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
has(agentId: string): boolean {
|
|
105
|
-
return this.agentToConfig.has(agentId);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
registerExternal(address: string, agentId: string): void {
|
|
109
|
-
this.addressToAgent.set(address.toLowerCase(), agentId);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private cacheMapping(agentId: string, config: XmtpWalletConfig): void {
|
|
113
|
-
this.agentToConfig.set(agentId, config);
|
|
114
|
-
this.addressToAgent.set(config.address.toLowerCase(), agentId);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 从磁盘加载所有已有 identity */
|
|
118
|
-
private loadFromDisk(): void {
|
|
119
|
-
if (!existsSync(this.storePath)) return;
|
|
120
|
-
for (const file of readdirSync(this.storePath)) {
|
|
121
|
-
if (!file.endsWith(".json")) continue;
|
|
122
|
-
const agentId = file.replace(".json", "");
|
|
123
|
-
try {
|
|
124
|
-
const config: XmtpWalletConfig = JSON.parse(
|
|
125
|
-
readFileSync(join(this.storePath, file), "utf-8"),
|
|
126
|
-
);
|
|
127
|
-
this.cacheMapping(agentId, config);
|
|
128
|
-
} catch {
|
|
129
|
-
// skip corrupt files
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|