a2a-xmtp 1.3.0 → 1.4.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/README.md +19 -11
- package/openclaw.plugin.json +9 -1
- package/package.json +4 -5
- package/src/coordination/group-scheduler.ts +192 -0
- package/src/coordination/message-orchestrator.ts +338 -0
- package/src/coordination/policy-engine.ts +120 -0
- package/src/index.ts +216 -0
- package/src/tools/xmtp-agents.ts +38 -0
- package/src/tools/xmtp-group.ts +153 -0
- package/src/tools/xmtp-inbox.ts +47 -0
- package/src/tools/xmtp-send.ts +77 -0
- package/src/transport/identity-registry.ts +133 -0
- package/src/transport/xmtp-bridge.ts +314 -0
- package/src/types.ts +181 -0
- package/dist/coordination/message-orchestrator.d.ts +0 -55
- package/dist/coordination/message-orchestrator.d.ts.map +0 -1
- package/dist/coordination/message-orchestrator.js +0 -142
- package/dist/coordination/message-orchestrator.js.map +0 -1
- package/dist/coordination/policy-engine.d.ts +0 -34
- package/dist/coordination/policy-engine.d.ts.map +0 -1
- package/dist/coordination/policy-engine.js +0 -92
- package/dist/coordination/policy-engine.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -163
- package/dist/index.js.map +0 -1
- package/dist/tools/xmtp-agents.d.ts +0 -19
- package/dist/tools/xmtp-agents.d.ts.map +0 -1
- package/dist/tools/xmtp-agents.js +0 -27
- package/dist/tools/xmtp-agents.js.map +0 -1
- package/dist/tools/xmtp-group.d.ts +0 -95
- package/dist/tools/xmtp-group.d.ts.map +0 -1
- package/dist/tools/xmtp-group.js +0 -134
- package/dist/tools/xmtp-group.js.map +0 -1
- package/dist/tools/xmtp-inbox.d.ts +0 -22
- package/dist/tools/xmtp-inbox.d.ts.map +0 -1
- package/dist/tools/xmtp-inbox.js +0 -36
- package/dist/tools/xmtp-inbox.js.map +0 -1
- package/dist/tools/xmtp-send.d.ts +0 -28
- package/dist/tools/xmtp-send.d.ts.map +0 -1
- package/dist/tools/xmtp-send.js +0 -63
- package/dist/tools/xmtp-send.js.map +0 -1
- package/dist/transport/identity-registry.d.ts +0 -30
- package/dist/transport/identity-registry.d.ts.map +0 -1
- package/dist/transport/identity-registry.js +0 -117
- package/dist/transport/identity-registry.js.map +0 -1
- package/dist/transport/xmtp-bridge.d.ts +0 -55
- package/dist/transport/xmtp-bridge.d.ts.map +0 -1
- package/dist/transport/xmtp-bridge.js +0 -265
- package/dist/transport/xmtp-bridge.js.map +0 -1
- package/dist/types.d.ts +0 -100
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -51
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Module 5: Policy Engine
|
|
3
|
+
// 三重保护:Turn Budget / Cool-down / Consent
|
|
4
|
+
// Depth Guard 已移除,由 GroupScheduler round-robin + turn budget 取代
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
import type { ConversationPolicy, ConversationState, ConsentState } from "../types.js";
|
|
8
|
+
|
|
9
|
+
export interface PolicyCheckParams {
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
conversationId: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PolicyCheckResult {
|
|
16
|
+
allowed: boolean;
|
|
17
|
+
reason?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PolicyEngine {
|
|
21
|
+
private conversations = new Map<string, ConversationState>();
|
|
22
|
+
private consents = new Map<string, ConsentState>();
|
|
23
|
+
private localAgentIds = new Set<string>();
|
|
24
|
+
|
|
25
|
+
constructor(private policy: ConversationPolicy) {}
|
|
26
|
+
|
|
27
|
+
registerLocalAgent(agentId: string): void {
|
|
28
|
+
this.localAgentIds.add(agentId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
checkOutgoing(params: PolicyCheckParams): PolicyCheckResult {
|
|
32
|
+
return this.check(params);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
checkIncoming(params: PolicyCheckParams): PolicyCheckResult {
|
|
36
|
+
const consent = this.getConsent(params.from);
|
|
37
|
+
if (consent === "deny") {
|
|
38
|
+
return { allowed: false, reason: `Sender ${params.from} is denied` };
|
|
39
|
+
}
|
|
40
|
+
if (this.policy.consentMode === "explicit-only" && consent !== "allow") {
|
|
41
|
+
return {
|
|
42
|
+
allowed: false,
|
|
43
|
+
reason: `Sender ${params.from} not explicitly allowed (consent: ${consent})`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return this.check(params);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private check(params: PolicyCheckParams): PolicyCheckResult {
|
|
50
|
+
const state = this.getOrCreateState(params.conversationId);
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
|
|
53
|
+
const ttlMs = this.policy.ttlMinutes * 60 * 1000;
|
|
54
|
+
if (now - state.createdAt > ttlMs) {
|
|
55
|
+
return { allowed: false, reason: `Conversation TTL expired (${this.policy.ttlMinutes} min)` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (state.turn >= this.policy.maxTurns) {
|
|
59
|
+
return { allowed: false, reason: `Turn budget exhausted (${state.turn}/${this.policy.maxTurns})` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (state.lastSendTime > 0 && now - state.lastSendTime < this.policy.minIntervalMs) {
|
|
63
|
+
return { allowed: false, reason: `Cool-down active (${this.policy.minIntervalMs}ms between sends)` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { allowed: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
recordTurn(conversationId: string): void {
|
|
70
|
+
const state = this.getOrCreateState(conversationId);
|
|
71
|
+
state.turn += 1;
|
|
72
|
+
state.lastSendTime = Date.now();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 检查此 agent 在该会话中的 turn 是否已用完 */
|
|
76
|
+
isTurnExhausted(conversationId: string): boolean {
|
|
77
|
+
const state = this.conversations.get(conversationId);
|
|
78
|
+
if (!state) return false;
|
|
79
|
+
return state.turn >= this.policy.maxTurns;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getConversationState(conversationId: string): ConversationState | null {
|
|
83
|
+
return this.conversations.get(conversationId) ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
resetConversation(conversationId: string): void {
|
|
87
|
+
this.conversations.delete(conversationId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setConsent(address: string, consent: ConsentState): void {
|
|
91
|
+
this.consents.set(address.toLowerCase(), consent);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getConsent(identifier: string): ConsentState {
|
|
95
|
+
const normalized = identifier.toLowerCase();
|
|
96
|
+
if (this.policy.consentMode === "auto-allow-local" && this.localAgentIds.has(identifier)) {
|
|
97
|
+
return "allow";
|
|
98
|
+
}
|
|
99
|
+
return this.consents.get(normalized) ?? "unknown";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
loadAclRules(rules: Array<{ address: string; consent: ConsentState }>): void {
|
|
103
|
+
for (const rule of rules) {
|
|
104
|
+
this.consents.set(rule.address.toLowerCase(), rule.consent);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getPolicy(): ConversationPolicy {
|
|
109
|
+
return { ...this.policy };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getOrCreateState(conversationId: string): ConversationState {
|
|
113
|
+
let state = this.conversations.get(conversationId);
|
|
114
|
+
if (!state) {
|
|
115
|
+
state = { turn: 0, lastSendTime: 0, createdAt: Date.now() };
|
|
116
|
+
this.conversations.set(conversationId, state);
|
|
117
|
+
}
|
|
118
|
+
return state;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
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 { IdentityRegistry } from "./transport/identity-registry.js";
|
|
9
|
+
import { PolicyEngine } from "./coordination/policy-engine.js";
|
|
10
|
+
import { GroupScheduler } from "./coordination/group-scheduler.js";
|
|
11
|
+
import { MessageOrchestrator } from "./coordination/message-orchestrator.js";
|
|
12
|
+
import { XmtpBridge } from "./transport/xmtp-bridge.js";
|
|
13
|
+
import { handleXmtpSend } from "./tools/xmtp-send.js";
|
|
14
|
+
import { handleXmtpInbox } from "./tools/xmtp-inbox.js";
|
|
15
|
+
import { handleDiscoverAgents } from "./tools/xmtp-agents.js";
|
|
16
|
+
import { handleXmtpGroup } from "./tools/xmtp-group.js";
|
|
17
|
+
import { DEFAULT_PLUGIN_CONFIG, type PluginConfig } from "./types.js";
|
|
18
|
+
|
|
19
|
+
const AGENT_ID = "main"; // OpenClaw 默认 agent
|
|
20
|
+
|
|
21
|
+
export default definePluginEntry({
|
|
22
|
+
id: "a2a-xmtp",
|
|
23
|
+
name: "Agent-to-Agent IM (XMTP)",
|
|
24
|
+
description: "Decentralized Agent-to-Agent E2EE messaging powered by XMTP protocol",
|
|
25
|
+
|
|
26
|
+
register(api) {
|
|
27
|
+
const bridges = new Map<string, XmtpBridge>();
|
|
28
|
+
let registry: IdentityRegistry;
|
|
29
|
+
let policyEngine: PolicyEngine;
|
|
30
|
+
|
|
31
|
+
// ── 1. 注册 Tools ──
|
|
32
|
+
|
|
33
|
+
api.registerTool({
|
|
34
|
+
name: "xmtp_send",
|
|
35
|
+
label: "Send XMTP Message",
|
|
36
|
+
description:
|
|
37
|
+
"Send an E2EE message to another agent via XMTP. " +
|
|
38
|
+
"Supports cross-gateway and cross-organization communication.",
|
|
39
|
+
parameters: Type.Object({
|
|
40
|
+
to: Type.Optional(Type.String({ description: "Target agent ID or XMTP address (0x...). Optional when conversationId is provided." })),
|
|
41
|
+
message: Type.String({ description: "Message content to send" }),
|
|
42
|
+
conversationId: Type.Optional(Type.String({ description: "Reuse existing conversation. When set, 'to' is optional." })),
|
|
43
|
+
contentType: Type.Optional(
|
|
44
|
+
Type.String({ description: "Message type: text or markdown", default: "text" }),
|
|
45
|
+
),
|
|
46
|
+
}),
|
|
47
|
+
async execute(_toolCallId, params) {
|
|
48
|
+
const p = params as { to?: string; message: string; conversationId?: string; contentType?: "text" | "markdown" };
|
|
49
|
+
return await handleXmtpSend(bridges, registry, policyEngine, p, AGENT_ID);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
api.registerTool({
|
|
54
|
+
name: "xmtp_inbox",
|
|
55
|
+
label: "XMTP Inbox",
|
|
56
|
+
description:
|
|
57
|
+
"Check your XMTP inbox for messages from other agents. " +
|
|
58
|
+
"Messages are E2E encrypted and only you can read them.",
|
|
59
|
+
parameters: Type.Object({
|
|
60
|
+
limit: Type.Optional(Type.Number({ description: "Max messages to return (default: 10)" })),
|
|
61
|
+
from: Type.Optional(Type.String({ description: "Filter by sender agent ID or address" })),
|
|
62
|
+
}),
|
|
63
|
+
async execute(_toolCallId, params) {
|
|
64
|
+
const p = params as { limit?: number; from?: string };
|
|
65
|
+
return await handleXmtpInbox(bridges, registry, p, AGENT_ID);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
api.registerTool({
|
|
70
|
+
name: "xmtp_agents",
|
|
71
|
+
label: "Discover XMTP Agents",
|
|
72
|
+
description:
|
|
73
|
+
"Discover agents available for XMTP communication. " +
|
|
74
|
+
"Lists registered agents and their connection status.",
|
|
75
|
+
parameters: Type.Object({
|
|
76
|
+
includeExternal: Type.Optional(
|
|
77
|
+
Type.Boolean({ description: "Include ERC-8004 external registry (Phase 3)" }),
|
|
78
|
+
),
|
|
79
|
+
}),
|
|
80
|
+
async execute(_toolCallId, params) {
|
|
81
|
+
const p = params as { includeExternal?: boolean };
|
|
82
|
+
return await handleDiscoverAgents(bridges, registry, p);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
api.registerTool({
|
|
87
|
+
name: "xmtp_group",
|
|
88
|
+
label: "XMTP Group Management",
|
|
89
|
+
description:
|
|
90
|
+
"Manage XMTP group conversations. Actions: create (new group), list (all groups), " +
|
|
91
|
+
"members (view members), add_member, remove_member.",
|
|
92
|
+
parameters: Type.Object({
|
|
93
|
+
action: Type.String({ description: "Action: create | list | members | add_member | remove_member" }),
|
|
94
|
+
members: Type.Optional(Type.Array(Type.String(), { description: "Agent IDs or 0x addresses (for create/add_member/remove_member)" })),
|
|
95
|
+
conversationId: Type.Optional(Type.String({ description: "Group conversation ID (for members/add_member/remove_member)" })),
|
|
96
|
+
name: Type.Optional(Type.String({ description: "Group name (for create)" })),
|
|
97
|
+
}),
|
|
98
|
+
async execute(_toolCallId, params) {
|
|
99
|
+
const p = params as { action: string; members?: string[]; conversationId?: string; name?: string };
|
|
100
|
+
return await handleXmtpGroup(bridges, registry, p, AGENT_ID);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── 2. 注册 HTTP 状态路由 ──
|
|
105
|
+
|
|
106
|
+
api.registerHttpRoute({
|
|
107
|
+
path: "/a2a-xmtp/status",
|
|
108
|
+
auth: "gateway",
|
|
109
|
+
handler: async (_req, res) => {
|
|
110
|
+
const status = {
|
|
111
|
+
plugin: "a2a-xmtp",
|
|
112
|
+
bridgeCount: bridges.size,
|
|
113
|
+
agents: Array.from(bridges.entries()).map(([id, b]) => ({
|
|
114
|
+
agentId: id,
|
|
115
|
+
xmtpAddress: b.address,
|
|
116
|
+
connected: b.isConnected,
|
|
117
|
+
env: b.env,
|
|
118
|
+
})),
|
|
119
|
+
policy: policyEngine?.getPolicy(),
|
|
120
|
+
};
|
|
121
|
+
res.setHeader("Content-Type", "application/json");
|
|
122
|
+
res.end(JSON.stringify(status, null, 2));
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ── 3. 注册 Service(生命周期管理) ──
|
|
128
|
+
|
|
129
|
+
api.registerService({
|
|
130
|
+
id: "a2a-xmtp-bridge",
|
|
131
|
+
async start(ctx) {
|
|
132
|
+
ctx.logger.info("[a2a-xmtp] Starting XMTP Bridge Service...");
|
|
133
|
+
|
|
134
|
+
// 加载配置
|
|
135
|
+
const pluginCfg = (ctx.config as any)?.plugins?.entries?.["a2a-xmtp"]?.config;
|
|
136
|
+
const config: PluginConfig = {
|
|
137
|
+
xmtp: {
|
|
138
|
+
env: pluginCfg?.xmtp?.env ?? DEFAULT_PLUGIN_CONFIG.xmtp.env,
|
|
139
|
+
dbPath: pluginCfg?.xmtp?.dbPath ?? DEFAULT_PLUGIN_CONFIG.xmtp.dbPath,
|
|
140
|
+
},
|
|
141
|
+
policy: {
|
|
142
|
+
maxTurns: pluginCfg?.policy?.maxTurns ?? DEFAULT_PLUGIN_CONFIG.policy.maxTurns,
|
|
143
|
+
minIntervalMs: pluginCfg?.policy?.minIntervalMs ?? DEFAULT_PLUGIN_CONFIG.policy.minIntervalMs,
|
|
144
|
+
ttlMinutes: pluginCfg?.policy?.ttlMinutes ?? DEFAULT_PLUGIN_CONFIG.policy.ttlMinutes,
|
|
145
|
+
consentMode: pluginCfg?.policy?.consentMode ?? DEFAULT_PLUGIN_CONFIG.policy.consentMode,
|
|
146
|
+
},
|
|
147
|
+
groupScheduling: {
|
|
148
|
+
baseDelayMs: pluginCfg?.groupScheduling?.baseDelayMs ?? DEFAULT_PLUGIN_CONFIG.groupScheduling.baseDelayMs,
|
|
149
|
+
slotTimeoutMs: pluginCfg?.groupScheduling?.slotTimeoutMs ?? DEFAULT_PLUGIN_CONFIG.groupScheduling.slotTimeoutMs,
|
|
150
|
+
claimExpireMs: pluginCfg?.groupScheduling?.claimExpireMs ?? DEFAULT_PLUGIN_CONFIG.groupScheduling.claimExpireMs,
|
|
151
|
+
},
|
|
152
|
+
walletKey: pluginCfg?.walletKey,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// 初始化核心组件
|
|
156
|
+
registry = new IdentityRegistry(ctx.stateDir, config.xmtp.env);
|
|
157
|
+
policyEngine = new PolicyEngine(config.policy);
|
|
158
|
+
|
|
159
|
+
// 初始化群聊调度器和消息编排器
|
|
160
|
+
const groupScheduler = new GroupScheduler(config.groupScheduling);
|
|
161
|
+
const orchestrator = new MessageOrchestrator(
|
|
162
|
+
api.runtime.subagent,
|
|
163
|
+
ctx.logger,
|
|
164
|
+
policyEngine,
|
|
165
|
+
groupScheduler,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// 初始化主 Agent 的 XMTP Bridge
|
|
169
|
+
try {
|
|
170
|
+
const walletConfig = await registry.initAgent(AGENT_ID, config.walletKey);
|
|
171
|
+
policyEngine.registerLocalAgent(AGENT_ID);
|
|
172
|
+
|
|
173
|
+
const bridge = new XmtpBridge(
|
|
174
|
+
AGENT_ID,
|
|
175
|
+
walletConfig,
|
|
176
|
+
policyEngine,
|
|
177
|
+
registry,
|
|
178
|
+
config.xmtp.dbPath,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// 消息回调:委托给 orchestrator 处理
|
|
182
|
+
bridge.onMessage = (agentId, payload) =>
|
|
183
|
+
orchestrator.handleMessage(bridge, agentId, payload);
|
|
184
|
+
|
|
185
|
+
// Claim 回调:通知 orchestrator 取消 failover 等待
|
|
186
|
+
bridge.onClaim = (conversationId, senderAddress, messageId) =>
|
|
187
|
+
orchestrator.handleClaim(conversationId, senderAddress, messageId);
|
|
188
|
+
|
|
189
|
+
await bridge.start();
|
|
190
|
+
bridges.set(AGENT_ID, bridge);
|
|
191
|
+
|
|
192
|
+
ctx.logger.info(
|
|
193
|
+
`[a2a-xmtp] Bridge started: ${AGENT_ID} → ${bridge.address} (env: ${config.xmtp.env})`,
|
|
194
|
+
);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
ctx.logger.error(
|
|
197
|
+
`[a2a-xmtp] Failed to start bridge: ${err instanceof Error ? err.message : String(err)}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
async stop(ctx) {
|
|
203
|
+
ctx.logger.info("[a2a-xmtp] Stopping XMTP bridges...");
|
|
204
|
+
for (const [id, bridge] of bridges) {
|
|
205
|
+
try {
|
|
206
|
+
await bridge.stop();
|
|
207
|
+
ctx.logger.info(`[a2a-xmtp] Bridge stopped: ${id}`);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
ctx.logger.error(`[a2a-xmtp] Error stopping bridge ${id}: ${err}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
bridges.clear();
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|