a2a-xmtp 1.4.3 → 1.4.5
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/README.md +1 -31
- package/openclaw.plugin.json +1 -0
- package/package.json +3 -5
- package/src/coordination/message-orchestrator.ts +70 -45
- package/src/index.ts +15 -5
- package/src/transport/xmtp-bridge.ts +3 -2
package/README.md
CHANGED
|
@@ -17,37 +17,7 @@ Decentralized Agent-to-Agent E2EE messaging for [OpenClaw](https://openclaw.ai)
|
|
|
17
17
|
openclaw plugins install a2a-xmtp
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Edit `~/.openclaw/openclaw.json`:
|
|
23
|
-
|
|
24
|
-
```json
|
|
25
|
-
{
|
|
26
|
-
"tools": {
|
|
27
|
-
"profile": "full"
|
|
28
|
-
},
|
|
29
|
-
"plugins": {
|
|
30
|
-
"entries": {
|
|
31
|
-
"a2a-xmtp": {
|
|
32
|
-
"enabled": true,
|
|
33
|
-
"config": {
|
|
34
|
-
"xmtp": {
|
|
35
|
-
"env": "dev",
|
|
36
|
-
"dbPath": "~/.openclaw/xmtp-data"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Then restart:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
mkdir -p ~/.openclaw/xmtp-data
|
|
49
|
-
openclaw gateway restart
|
|
50
|
-
```
|
|
20
|
+
That's it. The plugin auto-generates an XMTP wallet, creates data directories, and starts with sensible defaults.
|
|
51
21
|
|
|
52
22
|
## Verify
|
|
53
23
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "a2a-xmtp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"description": "Decentralized Agent-to-Agent E2EE messaging for OpenClaw via XMTP",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"build": "tsc",
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"test:watch": "vitest",
|
|
21
|
-
"lint": "tsc --noEmit"
|
|
21
|
+
"lint": "tsc --noEmit",
|
|
22
|
+
"server": "node --import tsx src/server/index.ts"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@xmtp/agent-sdk": "^2.3.0",
|
|
@@ -29,9 +30,6 @@
|
|
|
29
30
|
"vitest": "^3.0.0",
|
|
30
31
|
"@types/node": "^22.0.0"
|
|
31
32
|
},
|
|
32
|
-
"peerDependencies": {
|
|
33
|
-
"openclaw": "*"
|
|
34
|
-
},
|
|
35
33
|
"openclaw": {
|
|
36
34
|
"extensions": [
|
|
37
35
|
"./src/index.ts"
|
|
@@ -108,48 +108,49 @@ export class MessageOrchestrator {
|
|
|
108
108
|
const result = await this.subagentApi.waitForRun({ runId, timeoutMs: 60000 });
|
|
109
109
|
if (result.status === "error") {
|
|
110
110
|
this.logger.error(`[a2a-xmtp] Subagent error: ${result.error}`);
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (result.status === "timeout") {
|
|
114
|
+
this.logger.warn(`[a2a-xmtp] Subagent timeout for ${sessionKey}, checking for late reply...`);
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
// timeout 时 LLM 可能已经返回了内容(如限流重试后成功),仍尝试提取回复
|
|
118
|
+
const { messages } = await this.subagentApi.getSessionMessages({
|
|
119
|
+
sessionKey,
|
|
120
|
+
limit: 5,
|
|
121
|
+
});
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
// 安全检查:白名单机制
|
|
124
|
+
if (this.hasForbiddenToolCalls(messages)) {
|
|
125
|
+
this.logger.warn(
|
|
126
|
+
`[a2a-xmtp] SECURITY: Blocked reply — LLM called non-whitelisted tool, triggered by XMTP message from ${senderLabel}. Possible prompt injection.`,
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
// 提取回复文本
|
|
132
|
+
const replyText = this.extractReplyText(messages);
|
|
133
|
+
if (!replyText) return;
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// recordTurn 由 bridge.sendMessage 内部调用,此处不重复
|
|
142
|
-
await bridge.sendMessage(payload.from.xmtpAddress, replyText, {
|
|
143
|
-
conversationId: payload.conversation.id,
|
|
144
|
-
});
|
|
135
|
+
// 发送前最终检查 turn budget(收窄 race window)
|
|
136
|
+
if (payload.conversation.isGroup && this.policyEngine.isTurnExhausted(payload.conversation.id)) {
|
|
137
|
+
this.logger.info(
|
|
138
|
+
`[a2a-xmtp] Turn budget exhausted before send in ${payload.conversation.id}, dropping reply`,
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
145
142
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
// recordTurn 由 bridge.sendMessage 内部调用,此处不重复
|
|
144
|
+
await bridge.sendMessage(payload.from.xmtpAddress, replyText, {
|
|
145
|
+
conversationId: payload.conversation.id,
|
|
146
|
+
});
|
|
150
147
|
|
|
151
|
-
|
|
148
|
+
// 群聊:清除 claim(self 消息计数由 bridge.onSelfGroupMessage 处理)
|
|
149
|
+
if (payload.conversation.isGroup) {
|
|
150
|
+
this.groupScheduler.clearClaim(payload.conversation.id);
|
|
152
151
|
}
|
|
152
|
+
|
|
153
|
+
this.logger.info(`[a2a-xmtp] Replied to ${senderLabel} in ${payload.conversation.id}`);
|
|
153
154
|
} catch (err) {
|
|
154
155
|
this.logger.error(
|
|
155
156
|
`[a2a-xmtp] Failed to trigger subagent: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -208,7 +209,10 @@ export class MessageOrchestrator {
|
|
|
208
209
|
|
|
209
210
|
// 发送 claim
|
|
210
211
|
try {
|
|
211
|
-
await bridge.sendClaim(convId, payload.message.id);
|
|
212
|
+
const claimMsgId = await bridge.sendClaim(convId, payload.message.id);
|
|
213
|
+
this.logger.info(
|
|
214
|
+
`[a2a-xmtp] Claim sent: ${claimMsgId} in ${convId}`,
|
|
215
|
+
);
|
|
212
216
|
} catch (err) {
|
|
213
217
|
this.logger.warn(
|
|
214
218
|
`[a2a-xmtp] Failed to send claim: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -229,27 +233,48 @@ export class MessageOrchestrator {
|
|
|
229
233
|
const timedOut = await waitWithAbort(decision.timeoutMs, abortCtrl.signal);
|
|
230
234
|
|
|
231
235
|
if (!timedOut) {
|
|
232
|
-
//
|
|
236
|
+
// 收到了 claim → 等待 claim 过期,看指定回复者是否真的发出了回复
|
|
233
237
|
this.logger.info(
|
|
234
|
-
`[a2a-xmtp] Saw claim
|
|
238
|
+
`[a2a-xmtp] Saw claim in ${convId}, waiting for actual reply...`,
|
|
235
239
|
);
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
const claimExpireMs = this.groupScheduler.getConfig().claimExpireMs;
|
|
241
|
+
await sleep(claimExpireMs);
|
|
242
|
+
|
|
243
|
+
// claim 已被清除 = 回复已发送 → 不需要接管
|
|
244
|
+
if (!this.groupScheduler.hasActiveClaim(convId) && !this.groupScheduler.isClaimExpired(convId)) {
|
|
245
|
+
this.logger.info(
|
|
246
|
+
`[a2a-xmtp] Claim cleared (reply sent) in ${convId}, staying silent`,
|
|
247
|
+
);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// claim 过期且未清除 = 指定回复者失败 → failover
|
|
252
|
+
this.logger.warn(
|
|
253
|
+
`[a2a-xmtp] Claim expired without reply in ${convId}, failing over`,
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
// 超时且没有 claim → 检查是否有未过期的 claim(可能在等待期间收到)
|
|
257
|
+
if (this.groupScheduler.hasActiveClaim(convId)) {
|
|
258
|
+
this.logger.info(
|
|
259
|
+
`[a2a-xmtp] Active claim exists in ${convId}, staying silent`,
|
|
260
|
+
);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
238
263
|
|
|
239
|
-
// 超时且没有 claim → 检查是否有未过期的 claim(可能在等待期间收到)
|
|
240
|
-
if (this.groupScheduler.hasActiveClaim(convId)) {
|
|
241
264
|
this.logger.info(
|
|
242
|
-
`[a2a-xmtp]
|
|
265
|
+
`[a2a-xmtp] No claim received in ${convId}, failing over`,
|
|
243
266
|
);
|
|
244
|
-
return false;
|
|
245
267
|
}
|
|
246
268
|
|
|
247
|
-
//
|
|
269
|
+
// Failover:接管回复
|
|
248
270
|
this.logger.info(
|
|
249
271
|
`[a2a-xmtp] Failover: taking over msg #${msgIndex} in ${convId}`,
|
|
250
272
|
);
|
|
251
273
|
try {
|
|
252
|
-
await bridge.sendClaim(convId, payload.message.id);
|
|
274
|
+
const claimMsgId = await bridge.sendClaim(convId, payload.message.id);
|
|
275
|
+
this.logger.info(
|
|
276
|
+
`[a2a-xmtp] Failover claim sent: ${claimMsgId} in ${convId}`,
|
|
277
|
+
);
|
|
253
278
|
} catch (err) {
|
|
254
279
|
this.logger.warn(
|
|
255
280
|
`[a2a-xmtp] Failed to send failover claim: ${err instanceof Error ? err.message : String(err)}`,
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
7
7
|
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
8
10
|
import { IdentityRegistry } from "./transport/identity-registry.js";
|
|
9
11
|
import { PolicyEngine } from "./coordination/policy-engine.js";
|
|
10
12
|
import { GroupScheduler } from "./coordination/group-scheduler.js";
|
|
@@ -18,15 +20,19 @@ import { DEFAULT_PLUGIN_CONFIG, type PluginConfig } from "./types.js";
|
|
|
18
20
|
|
|
19
21
|
const AGENT_ID = "main"; // OpenClaw 默认 agent
|
|
20
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
|
+
|
|
21
30
|
export default definePluginEntry({
|
|
22
31
|
id: "a2a-xmtp",
|
|
23
32
|
name: "Agent-to-Agent IM (XMTP)",
|
|
24
33
|
description: "Decentralized Agent-to-Agent E2EE messaging powered by XMTP protocol",
|
|
25
34
|
|
|
26
35
|
register(api) {
|
|
27
|
-
const bridges = new Map<string, XmtpBridge>();
|
|
28
|
-
let registry: IdentityRegistry;
|
|
29
|
-
let policyEngine: PolicyEngine;
|
|
30
36
|
|
|
31
37
|
// ── 1. 注册 Tools ──
|
|
32
38
|
|
|
@@ -131,12 +137,13 @@ export default definePluginEntry({
|
|
|
131
137
|
async start(ctx) {
|
|
132
138
|
ctx.logger.info("[a2a-xmtp] Starting XMTP Bridge Service...");
|
|
133
139
|
|
|
134
|
-
//
|
|
140
|
+
// 加载配置(所有选项均有默认值,用户无需手动编辑配置文件)
|
|
135
141
|
const pluginCfg = (ctx.config as any)?.plugins?.entries?.["a2a-xmtp"]?.config;
|
|
142
|
+
const defaultDbPath = join(ctx.stateDir, "xmtp-data");
|
|
136
143
|
const config: PluginConfig = {
|
|
137
144
|
xmtp: {
|
|
138
145
|
env: pluginCfg?.xmtp?.env ?? DEFAULT_PLUGIN_CONFIG.xmtp.env,
|
|
139
|
-
dbPath: pluginCfg?.xmtp?.dbPath ??
|
|
146
|
+
dbPath: pluginCfg?.xmtp?.dbPath ?? defaultDbPath,
|
|
140
147
|
},
|
|
141
148
|
policy: {
|
|
142
149
|
maxTurns: pluginCfg?.policy?.maxTurns ?? DEFAULT_PLUGIN_CONFIG.policy.maxTurns,
|
|
@@ -152,6 +159,9 @@ export default definePluginEntry({
|
|
|
152
159
|
walletKey: pluginCfg?.walletKey,
|
|
153
160
|
};
|
|
154
161
|
|
|
162
|
+
// 自动创建数据目录
|
|
163
|
+
mkdirSync(config.xmtp.dbPath, { recursive: true });
|
|
164
|
+
|
|
155
165
|
// 初始化核心组件
|
|
156
166
|
registry = new IdentityRegistry(ctx.stateDir, config.xmtp.env);
|
|
157
167
|
policyEngine = new PolicyEngine(config.policy);
|
|
@@ -109,12 +109,13 @@ export class XmtpBridge {
|
|
|
109
109
|
/**
|
|
110
110
|
* 发送 claim 消息到群聊,表明本 agent 将要回复。
|
|
111
111
|
*/
|
|
112
|
-
async sendClaim(conversationId: string, messageId: string): Promise<
|
|
112
|
+
async sendClaim(conversationId: string, messageId: string): Promise<string> {
|
|
113
113
|
if (!this.agent) throw new Error(`Bridge for ${this.agentId} not started`);
|
|
114
114
|
const conv = await this.agent.client.conversations.getConversationById(conversationId);
|
|
115
115
|
if (!conv) throw new Error(`Conversation ${conversationId} not found`);
|
|
116
116
|
const claimText = GroupScheduler.formatClaimMessage(messageId);
|
|
117
|
-
await conv.sendText(claimText);
|
|
117
|
+
const claimMsgId = await conv.sendText(claimText);
|
|
118
|
+
return String(claimMsgId);
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
async getInbox(opts?: { limit?: number; from?: string }): Promise<InboxMessage[]> {
|