@resciencelab/agent-world-network 1.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/LICENSE +21 -0
- package/README.md +357 -0
- package/dist/address.d.ts +5 -0
- package/dist/address.d.ts.map +1 -0
- package/dist/address.js +44 -0
- package/dist/address.js.map +1 -0
- package/dist/channel.d.ts +107 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +94 -0
- package/dist/channel.js.map +1 -0
- package/dist/identity.d.ts +31 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +312 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +812 -0
- package/dist/index.js.map +1 -0
- package/dist/peer-client.d.ts +26 -0
- package/dist/peer-client.d.ts.map +1 -0
- package/dist/peer-client.js +199 -0
- package/dist/peer-client.js.map +1 -0
- package/dist/peer-db.d.ts +32 -0
- package/dist/peer-db.d.ts.map +1 -0
- package/dist/peer-db.js +299 -0
- package/dist/peer-db.js.map +1 -0
- package/dist/peer-server.d.ts +36 -0
- package/dist/peer-server.d.ts.map +1 -0
- package/dist/peer-server.js +319 -0
- package/dist/peer-server.js.map +1 -0
- package/dist/transport-quic.d.ts +32 -0
- package/dist/transport-quic.d.ts.map +1 -0
- package/dist/transport-quic.js +195 -0
- package/dist/transport-quic.js.map +1 -0
- package/dist/transport.d.ts +55 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +80 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/openclaw.plugin.json +77 -0
- package/package.json +62 -0
- package/scripts/release.sh.bak +113 -0
- package/scripts/sync-version.mjs +19 -0
- package/skills/awn/SKILL.md +95 -0
- package/skills/awn/references/discovery.md +71 -0
- package/skills/awn/references/flows.md +84 -0
- package/skills/awn/references/install.md +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.default = register;
|
|
37
|
+
/**
|
|
38
|
+
* AWN — Agent World Network — OpenClaw plugin entry point.
|
|
39
|
+
*
|
|
40
|
+
* Agent ID (sha256(publicKey)[:16]) is the primary peer identifier.
|
|
41
|
+
* Transport is plain HTTP over TCP; QUIC is available as a fast optional transport.
|
|
42
|
+
*/
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const child_process_1 = require("child_process");
|
|
46
|
+
const identity_1 = require("./identity");
|
|
47
|
+
const peer_db_1 = require("./peer-db");
|
|
48
|
+
const peer_server_1 = require("./peer-server");
|
|
49
|
+
const peer_client_1 = require("./peer-client");
|
|
50
|
+
const peer_db_2 = require("./peer-db");
|
|
51
|
+
const channel_1 = require("./channel");
|
|
52
|
+
const transport_1 = require("./transport");
|
|
53
|
+
const transport_quic_1 = require("./transport-quic");
|
|
54
|
+
const address_1 = require("./address");
|
|
55
|
+
const AWN_TOOLS = [
|
|
56
|
+
"p2p_list_peers",
|
|
57
|
+
"p2p_send_message", "p2p_status",
|
|
58
|
+
"list_worlds", "join_world",
|
|
59
|
+
];
|
|
60
|
+
function ensureToolsAllowed(config) {
|
|
61
|
+
try {
|
|
62
|
+
const alsoAllow = config?.tools?.alsoAllow ?? [];
|
|
63
|
+
const missing = AWN_TOOLS.filter(t => !alsoAllow.includes(t));
|
|
64
|
+
if (missing.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const merged = [...alsoAllow, ...missing];
|
|
67
|
+
const jsonVal = JSON.stringify(merged);
|
|
68
|
+
(0, child_process_1.execSync)(`openclaw config set tools.alsoAllow '${jsonVal}'`, { timeout: 5000, stdio: "ignore" });
|
|
69
|
+
console.log(`[awn] Auto-enabled ${missing.length} AWN tool(s) in tools.alsoAllow`);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
console.warn("[awn] Could not auto-enable tools — enable manually via: openclaw config set tools.alsoAllow");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function ensurePluginAllowed(config) {
|
|
76
|
+
try {
|
|
77
|
+
const allow = config?.plugins?.allow;
|
|
78
|
+
if (allow === undefined || allow === null) {
|
|
79
|
+
(0, child_process_1.execSync)(`openclaw config set plugins.allow '["awn"]'`, { timeout: 5000, stdio: "ignore" });
|
|
80
|
+
console.log("[awn] Set plugins.allow to [awn]");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(allow) && !allow.includes("awn")) {
|
|
84
|
+
const merged = [...allow, "awn"];
|
|
85
|
+
(0, child_process_1.execSync)(`openclaw config set plugins.allow '${JSON.stringify(merged)}'`, { timeout: 5000, stdio: "ignore" });
|
|
86
|
+
console.log("[awn] Added awn to plugins.allow");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch { /* best effort */ }
|
|
90
|
+
}
|
|
91
|
+
function ensureChannelConfig(config) {
|
|
92
|
+
try {
|
|
93
|
+
const channelCfg = config?.channels?.awn;
|
|
94
|
+
if (channelCfg && channelCfg.dmPolicy)
|
|
95
|
+
return;
|
|
96
|
+
(0, child_process_1.execSync)(`openclaw config set channels.awn.dmPolicy '"pairing"'`, { timeout: 5000, stdio: "ignore" });
|
|
97
|
+
console.log("[awn] Set channels.awn.dmPolicy to pairing");
|
|
98
|
+
}
|
|
99
|
+
catch { /* best effort */ }
|
|
100
|
+
}
|
|
101
|
+
let identity = null;
|
|
102
|
+
let dataDir = path.join(os.homedir(), ".openclaw", "awn");
|
|
103
|
+
let peerPort = 8099;
|
|
104
|
+
let _agentMeta = {};
|
|
105
|
+
let _transportManager = null;
|
|
106
|
+
let _quicTransport = null;
|
|
107
|
+
// Track joined worlds for periodic member refresh
|
|
108
|
+
const _joinedWorlds = new Map();
|
|
109
|
+
const _worldMembersByWorld = new Map();
|
|
110
|
+
const _worldScopedPeerWorlds = new Map();
|
|
111
|
+
const _worldRefreshFailures = new Map();
|
|
112
|
+
let _memberRefreshTimer = null;
|
|
113
|
+
let _welcomeTimer = null;
|
|
114
|
+
const MEMBER_REFRESH_INTERVAL_MS = 30_000;
|
|
115
|
+
const WORLD_MEMBER_REFRESH_FAILURE_LIMIT = 3;
|
|
116
|
+
function trackWorldScopedPeer(agentId, worldId) {
|
|
117
|
+
let worldIds = _worldScopedPeerWorlds.get(agentId);
|
|
118
|
+
if (!worldIds) {
|
|
119
|
+
worldIds = new Set();
|
|
120
|
+
_worldScopedPeerWorlds.set(agentId, worldIds);
|
|
121
|
+
}
|
|
122
|
+
worldIds.add(worldId);
|
|
123
|
+
}
|
|
124
|
+
function untrackWorldScopedPeer(agentId, worldId) {
|
|
125
|
+
const worldIds = _worldScopedPeerWorlds.get(agentId);
|
|
126
|
+
if (!worldIds)
|
|
127
|
+
return;
|
|
128
|
+
worldIds.delete(worldId);
|
|
129
|
+
if (worldIds.size > 0)
|
|
130
|
+
return;
|
|
131
|
+
_worldScopedPeerWorlds.delete(agentId);
|
|
132
|
+
if ((0, peer_db_1.getPeer)(agentId)?.source !== "manual") {
|
|
133
|
+
(0, peer_db_1.removePeer)(agentId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function syncWorldMembers(worldId, members) {
|
|
137
|
+
const nextMemberIds = new Set();
|
|
138
|
+
const previousMemberIds = _worldMembersByWorld.get(worldId) ?? new Set();
|
|
139
|
+
for (const member of members) {
|
|
140
|
+
if (!member.agentId || member.agentId === identity?.agentId)
|
|
141
|
+
continue;
|
|
142
|
+
nextMemberIds.add(member.agentId);
|
|
143
|
+
const existingPeer = (0, peer_db_1.getPeer)(member.agentId);
|
|
144
|
+
if (!existingPeer || existingPeer.source !== "manual") {
|
|
145
|
+
(0, peer_db_2.upsertDiscoveredPeer)(member.agentId, "", {
|
|
146
|
+
alias: member.alias,
|
|
147
|
+
endpoints: member.endpoints,
|
|
148
|
+
source: "gossip",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
trackWorldScopedPeer(member.agentId, worldId);
|
|
152
|
+
}
|
|
153
|
+
for (const agentId of previousMemberIds) {
|
|
154
|
+
if (!nextMemberIds.has(agentId)) {
|
|
155
|
+
untrackWorldScopedPeer(agentId, worldId);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
_worldMembersByWorld.set(worldId, nextMemberIds);
|
|
159
|
+
}
|
|
160
|
+
function removeWorldMembers(worldId) {
|
|
161
|
+
const memberIds = _worldMembersByWorld.get(worldId);
|
|
162
|
+
if (!memberIds)
|
|
163
|
+
return;
|
|
164
|
+
for (const agentId of memberIds) {
|
|
165
|
+
untrackWorldScopedPeer(agentId, worldId);
|
|
166
|
+
}
|
|
167
|
+
_worldMembersByWorld.delete(worldId);
|
|
168
|
+
}
|
|
169
|
+
function stopWorldMemberRefreshIfIdle() {
|
|
170
|
+
if (_joinedWorlds.size > 0 || !_memberRefreshTimer)
|
|
171
|
+
return;
|
|
172
|
+
clearInterval(_memberRefreshTimer);
|
|
173
|
+
_memberRefreshTimer = null;
|
|
174
|
+
}
|
|
175
|
+
function dropJoinedWorld(worldId) {
|
|
176
|
+
removeWorldMembers(worldId);
|
|
177
|
+
(0, peer_server_1.removeWorld)(worldId);
|
|
178
|
+
_joinedWorlds.delete(worldId);
|
|
179
|
+
_worldRefreshFailures.delete(worldId);
|
|
180
|
+
stopWorldMemberRefreshIfIdle();
|
|
181
|
+
}
|
|
182
|
+
function recordWorldRefreshFailure(worldId) {
|
|
183
|
+
const failures = (_worldRefreshFailures.get(worldId) ?? 0) + 1;
|
|
184
|
+
if (failures >= WORLD_MEMBER_REFRESH_FAILURE_LIMIT) {
|
|
185
|
+
dropJoinedWorld(worldId);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
_worldRefreshFailures.set(worldId, failures);
|
|
189
|
+
}
|
|
190
|
+
async function refreshWorldMembers() {
|
|
191
|
+
if (!identity)
|
|
192
|
+
return;
|
|
193
|
+
for (const [worldId, info] of _joinedWorlds) {
|
|
194
|
+
try {
|
|
195
|
+
const { signHttpRequest } = require("./identity");
|
|
196
|
+
const isIpv6 = info.address.includes(":") && !info.address.includes(".");
|
|
197
|
+
const host = isIpv6 ? `[${info.address}]:${info.port}` : `${info.address}:${info.port}`;
|
|
198
|
+
const url = `http://${host}/world/members`;
|
|
199
|
+
const awHeaders = signHttpRequest(identity, "GET", host, "/world/members", "");
|
|
200
|
+
const resp = await fetch(url, {
|
|
201
|
+
headers: { ...awHeaders },
|
|
202
|
+
signal: AbortSignal.timeout(10_000),
|
|
203
|
+
});
|
|
204
|
+
if (resp.status === 403 || resp.status === 404) {
|
|
205
|
+
dropJoinedWorld(worldId);
|
|
206
|
+
(0, peer_server_1.removeWorld)(worldId);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!resp.ok) {
|
|
210
|
+
recordWorldRefreshFailure(worldId);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const bodyText = await resp.text();
|
|
214
|
+
const verification = (0, identity_1.verifyHttpResponseHeaders)(Object.fromEntries(Array.from(resp.headers.entries()).map(([key, value]) => [key, value])), resp.status, bodyText, info.publicKey);
|
|
215
|
+
if (!verification.ok) {
|
|
216
|
+
recordWorldRefreshFailure(worldId);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const body = JSON.parse(bodyText);
|
|
220
|
+
const memberList = body.members ?? [];
|
|
221
|
+
syncWorldMembers(worldId, memberList);
|
|
222
|
+
(0, peer_server_1.setWorldMembers)(worldId, [info.agentId, ...memberList.map(m => m.agentId).filter(id => id !== identity.agentId)]);
|
|
223
|
+
_worldRefreshFailures.delete(worldId);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
recordWorldRefreshFailure(worldId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function leaveJoinedWorlds() {
|
|
231
|
+
if (!identity || _joinedWorlds.size === 0)
|
|
232
|
+
return;
|
|
233
|
+
await Promise.allSettled([..._joinedWorlds.values()].map((info) => (0, peer_client_1.sendP2PMessage)(identity, info.address, "world.leave", "", info.port, 3_000, buildSendOpts(info.agentId))));
|
|
234
|
+
}
|
|
235
|
+
function buildSendOpts(peerIdOrAddr) {
|
|
236
|
+
const peer = peerIdOrAddr ? (0, peer_db_1.getPeer)(peerIdOrAddr) : null;
|
|
237
|
+
return {
|
|
238
|
+
endpoints: peer?.endpoints,
|
|
239
|
+
quicTransport: _quicTransport?.isActive() ? _quicTransport : undefined,
|
|
240
|
+
expectedPublicKey: peer?.publicKey || undefined,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function getGatewayUrl() {
|
|
244
|
+
return (process.env.GATEWAY_URL ?? "http://localhost:8100").replace(/\/+$/, "");
|
|
245
|
+
}
|
|
246
|
+
async function fetchGatewayWorldRecord(worldId) {
|
|
247
|
+
try {
|
|
248
|
+
const resp = await fetch(`${getGatewayUrl()}/world/${encodeURIComponent(worldId)}`, {
|
|
249
|
+
signal: AbortSignal.timeout(10_000),
|
|
250
|
+
});
|
|
251
|
+
if (!resp.ok)
|
|
252
|
+
return null;
|
|
253
|
+
const data = await resp.json();
|
|
254
|
+
const detail = typeof data.world === "object" && data.world
|
|
255
|
+
? data.world
|
|
256
|
+
: data;
|
|
257
|
+
const host = typeof detail.host === "object" && detail.host
|
|
258
|
+
? detail.host
|
|
259
|
+
: null;
|
|
260
|
+
const endpoints = Array.isArray(detail.endpoints)
|
|
261
|
+
? detail.endpoints
|
|
262
|
+
: Array.isArray(host?.endpoints)
|
|
263
|
+
? host.endpoints
|
|
264
|
+
: undefined;
|
|
265
|
+
const publicKey = typeof detail.publicKey === "string"
|
|
266
|
+
? detail.publicKey
|
|
267
|
+
: typeof host?.publicKey === "string"
|
|
268
|
+
? host.publicKey
|
|
269
|
+
: undefined;
|
|
270
|
+
const agentId = typeof detail.agentId === "string"
|
|
271
|
+
? detail.agentId
|
|
272
|
+
: typeof host?.agentId === "string"
|
|
273
|
+
? host.agentId
|
|
274
|
+
: undefined;
|
|
275
|
+
const alias = typeof detail.name === "string"
|
|
276
|
+
? detail.name
|
|
277
|
+
: typeof detail.alias === "string"
|
|
278
|
+
? detail.alias
|
|
279
|
+
: typeof host?.name === "string"
|
|
280
|
+
? host.name
|
|
281
|
+
: typeof host?.alias === "string"
|
|
282
|
+
? host.alias
|
|
283
|
+
: undefined;
|
|
284
|
+
return { agentId, alias, endpoints, publicKey };
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function register(api) {
|
|
291
|
+
api.registerService({
|
|
292
|
+
id: "awn-node",
|
|
293
|
+
start: async () => {
|
|
294
|
+
ensurePluginAllowed(api.config);
|
|
295
|
+
ensureToolsAllowed(api.config);
|
|
296
|
+
ensureChannelConfig(api.config);
|
|
297
|
+
const cfg = api.config?.plugins?.entries?.["awn"]?.config ?? {};
|
|
298
|
+
dataDir = cfg.data_dir ?? dataDir;
|
|
299
|
+
peerPort = cfg.peer_port ?? peerPort;
|
|
300
|
+
const pluginVersion = require("../package.json").version;
|
|
301
|
+
_agentMeta = { name: cfg.agent_name ?? api.config?.identity?.name, version: pluginVersion };
|
|
302
|
+
const isFirstRun = !require("fs").existsSync(path.join(dataDir, "identity.json"));
|
|
303
|
+
identity = (0, identity_1.loadOrCreateIdentity)(dataDir);
|
|
304
|
+
(0, peer_db_1.initDb)(dataDir);
|
|
305
|
+
if (cfg.tofu_ttl_days !== undefined)
|
|
306
|
+
(0, peer_db_1.setTofuTtl)(cfg.tofu_ttl_days);
|
|
307
|
+
console.log(`[awn] Agent ID: ${identity.agentId}`);
|
|
308
|
+
if (_agentMeta.name) {
|
|
309
|
+
console.log(`[awn] Name: ${_agentMeta.name}`);
|
|
310
|
+
}
|
|
311
|
+
_transportManager = new transport_1.TransportManager();
|
|
312
|
+
_quicTransport = new transport_quic_1.UDPTransport();
|
|
313
|
+
_transportManager.register(_quicTransport);
|
|
314
|
+
const quicPort = cfg.quic_port ?? 8098;
|
|
315
|
+
const activeTransport = await _transportManager.start(identity, {
|
|
316
|
+
dataDir,
|
|
317
|
+
quicPort,
|
|
318
|
+
advertiseAddress: cfg.advertise_address,
|
|
319
|
+
advertisePort: cfg.advertise_port,
|
|
320
|
+
});
|
|
321
|
+
if (activeTransport) {
|
|
322
|
+
console.log(`[awn] Active transport: ${activeTransport.id} -> ${activeTransport.address}`);
|
|
323
|
+
if (_quicTransport.isActive()) {
|
|
324
|
+
console.log(`[awn] QUIC endpoint: ${_quicTransport.address}`);
|
|
325
|
+
_quicTransport.onMessage((from, data) => {
|
|
326
|
+
(0, peer_server_1.handleUdpMessage)(data, from);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
console.warn("[awn] No QUIC transport available — HTTP-only mode");
|
|
332
|
+
}
|
|
333
|
+
const advertisedEndpoints = _transportManager.getEndpoints();
|
|
334
|
+
if (cfg.advertise_address) {
|
|
335
|
+
advertisedEndpoints.push({
|
|
336
|
+
transport: "tcp",
|
|
337
|
+
address: cfg.advertise_address,
|
|
338
|
+
port: peerPort,
|
|
339
|
+
priority: advertisedEndpoints.length ? 2 : 1,
|
|
340
|
+
ttl: 3600,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
_agentMeta.endpoints = advertisedEndpoints;
|
|
344
|
+
await (0, peer_server_1.startPeerServer)(peerPort, { identity });
|
|
345
|
+
(0, peer_server_1.setSelfMeta)({
|
|
346
|
+
agentId: identity.agentId,
|
|
347
|
+
publicKey: identity.publicKey,
|
|
348
|
+
..._agentMeta,
|
|
349
|
+
});
|
|
350
|
+
(0, channel_1.wireInboundToGateway)(api);
|
|
351
|
+
if (isFirstRun) {
|
|
352
|
+
const welcomeLines = [
|
|
353
|
+
"Welcome to Agent World Network!",
|
|
354
|
+
"",
|
|
355
|
+
`Your Agent ID: ${identity.agentId}`,
|
|
356
|
+
_quicTransport?.isActive()
|
|
357
|
+
? `QUIC transport active: ${_quicTransport.address}`
|
|
358
|
+
: "Running in HTTP-only mode.",
|
|
359
|
+
"",
|
|
360
|
+
"Quick start:",
|
|
361
|
+
" openclaw awn status — show your agent ID",
|
|
362
|
+
" openclaw join_world <id> — join a world to discover peers",
|
|
363
|
+
];
|
|
364
|
+
_welcomeTimer = setTimeout(() => {
|
|
365
|
+
_welcomeTimer = null;
|
|
366
|
+
try {
|
|
367
|
+
api.gateway?.receiveChannelMessage?.({
|
|
368
|
+
channelId: "awn",
|
|
369
|
+
accountId: "system",
|
|
370
|
+
text: welcomeLines.join("\n"),
|
|
371
|
+
senderId: "awn-system",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
catch { /* best effort */ }
|
|
375
|
+
}, 2000);
|
|
376
|
+
}
|
|
377
|
+
console.log(`[awn] Ready — join a world to discover peers`);
|
|
378
|
+
},
|
|
379
|
+
stop: async () => {
|
|
380
|
+
if (_memberRefreshTimer) {
|
|
381
|
+
clearInterval(_memberRefreshTimer);
|
|
382
|
+
_memberRefreshTimer = null;
|
|
383
|
+
}
|
|
384
|
+
if (_welcomeTimer) {
|
|
385
|
+
clearTimeout(_welcomeTimer);
|
|
386
|
+
_welcomeTimer = null;
|
|
387
|
+
}
|
|
388
|
+
await leaveJoinedWorlds();
|
|
389
|
+
for (const worldId of _joinedWorlds.keys()) {
|
|
390
|
+
removeWorldMembers(worldId);
|
|
391
|
+
}
|
|
392
|
+
_joinedWorlds.clear();
|
|
393
|
+
(0, peer_server_1.clearWorldMembers)();
|
|
394
|
+
_worldRefreshFailures.clear();
|
|
395
|
+
if (identity) {
|
|
396
|
+
await (0, peer_client_1.broadcastLeave)(identity, (0, peer_db_1.listPeers)(), peerPort, buildSendOpts());
|
|
397
|
+
}
|
|
398
|
+
(0, peer_db_1.flushDb)();
|
|
399
|
+
await (0, peer_server_1.stopPeerServer)();
|
|
400
|
+
if (_transportManager) {
|
|
401
|
+
await _transportManager.stop();
|
|
402
|
+
_transportManager = null;
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
// ── Channel ────────────────────────────────────────────────────────────────
|
|
407
|
+
if (identity) {
|
|
408
|
+
api.registerChannel({ plugin: (0, channel_1.buildChannel)(identity, peerPort, buildSendOpts) });
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
api.registerChannel({
|
|
412
|
+
plugin: {
|
|
413
|
+
id: "awn",
|
|
414
|
+
meta: {
|
|
415
|
+
id: "awn",
|
|
416
|
+
label: "AWN",
|
|
417
|
+
selectionLabel: "AWN (Agent World Network)",
|
|
418
|
+
docsPath: "/channels/awn",
|
|
419
|
+
blurb: "Agent World Network — world-scoped agent communication.",
|
|
420
|
+
aliases: ["p2p"],
|
|
421
|
+
},
|
|
422
|
+
capabilities: { chatTypes: ["direct"] },
|
|
423
|
+
configSchema: channel_1.CHANNEL_CONFIG_SCHEMA,
|
|
424
|
+
config: {
|
|
425
|
+
listAccountIds: () => (identity ? (0, peer_db_1.getPeerIds)() : []),
|
|
426
|
+
resolveAccount: (_, accountId) => {
|
|
427
|
+
const peer = accountId ? (0, peer_db_1.getPeer)(accountId) : null;
|
|
428
|
+
return {
|
|
429
|
+
accountId: accountId ?? "",
|
|
430
|
+
agentId: peer?.agentId ?? accountId ?? "",
|
|
431
|
+
};
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
outbound: {
|
|
435
|
+
deliveryMode: "direct",
|
|
436
|
+
sendText: async ({ text, account }) => {
|
|
437
|
+
if (!identity)
|
|
438
|
+
return { ok: false };
|
|
439
|
+
const agentId = account.agentId ?? "";
|
|
440
|
+
const r = await (0, peer_client_1.sendP2PMessage)(identity, agentId, "chat", text, peerPort, 10_000, buildSendOpts(agentId));
|
|
441
|
+
return { ok: r.ok };
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
// ── CLI commands ───────────────────────────────────────────────────────────
|
|
448
|
+
api.registerCli(({ program }) => {
|
|
449
|
+
const awn = program.command("awn").description("Agent World Network node management");
|
|
450
|
+
awn
|
|
451
|
+
.command("status")
|
|
452
|
+
.description("Show this node's agent ID and status")
|
|
453
|
+
.action(() => {
|
|
454
|
+
if (!identity) {
|
|
455
|
+
console.log("Plugin not started yet. Try again after gateway restart.");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
console.log("=== AWN Node Status ===");
|
|
459
|
+
if (_agentMeta.name)
|
|
460
|
+
console.log(`Agent name: ${_agentMeta.name}`);
|
|
461
|
+
console.log(`Agent ID: ${identity.agentId}`);
|
|
462
|
+
console.log(`DID Key: ${(0, identity_1.deriveDidKey)(identity.publicKey)}`);
|
|
463
|
+
console.log(`Version: v${_agentMeta.version}`);
|
|
464
|
+
console.log(`Transport: ${_transportManager?.active?.id ?? "http-only"}`);
|
|
465
|
+
if (_quicTransport?.isActive()) {
|
|
466
|
+
console.log(`QUIC endpoint: ${_quicTransport.address}`);
|
|
467
|
+
}
|
|
468
|
+
console.log(`Peer port: ${peerPort}`);
|
|
469
|
+
console.log(`Known peers: ${(0, peer_db_1.listPeers)().length}`);
|
|
470
|
+
console.log(`Worlds joined: ${_joinedWorlds.size}`);
|
|
471
|
+
});
|
|
472
|
+
awn
|
|
473
|
+
.command("peers")
|
|
474
|
+
.description("List known peers")
|
|
475
|
+
.action(() => {
|
|
476
|
+
const peers = (0, peer_db_1.listPeers)();
|
|
477
|
+
if (peers.length === 0) {
|
|
478
|
+
console.log("No peers yet. Use 'openclaw awn add <agent-id>' to add one.");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
console.log("=== Known Peers ===");
|
|
482
|
+
for (const peer of peers) {
|
|
483
|
+
const ago = Math.round((Date.now() - peer.lastSeen) / 1000);
|
|
484
|
+
const label = peer.alias ? ` — ${peer.alias}` : "";
|
|
485
|
+
const ver = peer.version ? ` [v${peer.version}]` : "";
|
|
486
|
+
const transports = peer.endpoints?.map((e) => e.transport).join(",") || "none";
|
|
487
|
+
console.log(` ${peer.agentId}${label}${ver} [${transports}] last seen ${ago}s ago`);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
awn
|
|
491
|
+
.command("ping <agentId>")
|
|
492
|
+
.description("Check if a peer is reachable")
|
|
493
|
+
.action(async (agentId) => {
|
|
494
|
+
console.log(`Pinging ${agentId}...`);
|
|
495
|
+
const peer = (0, peer_db_1.getPeer)(agentId);
|
|
496
|
+
const ok = await (0, peer_client_1.pingPeer)(agentId, peerPort, 5_000, peer?.endpoints);
|
|
497
|
+
console.log(ok ? `Reachable` : `Unreachable`);
|
|
498
|
+
});
|
|
499
|
+
awn
|
|
500
|
+
.command("send <agentId> <message>")
|
|
501
|
+
.description("Send a direct message to a peer")
|
|
502
|
+
.action(async (agentId, message) => {
|
|
503
|
+
if (!identity) {
|
|
504
|
+
console.error("Plugin not started. Restart the gateway first.");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const result = await (0, peer_client_1.sendP2PMessage)(identity, agentId, "chat", message, 8099, 10_000, buildSendOpts(agentId));
|
|
508
|
+
if (result.ok) {
|
|
509
|
+
console.log(`Message sent to ${agentId}`);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
console.error(`Failed: ${result.error}`);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
awn
|
|
516
|
+
.command("worlds")
|
|
517
|
+
.description("Show joined worlds")
|
|
518
|
+
.action(() => {
|
|
519
|
+
if (_joinedWorlds.size === 0) {
|
|
520
|
+
console.log("Not joined any worlds yet. Use 'openclaw join_world <id>' to join one.");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
console.log("=== Joined Worlds ===");
|
|
524
|
+
for (const [id, info] of _joinedWorlds) {
|
|
525
|
+
console.log(` ${id} — ${info.address}:${info.port}`);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}, { commands: ["awn"] });
|
|
529
|
+
// ── Slash commands ─────────────────────────────────────────────────────────
|
|
530
|
+
api.registerCommand({
|
|
531
|
+
name: "awn-status",
|
|
532
|
+
description: "Show AWN node status",
|
|
533
|
+
handler: () => {
|
|
534
|
+
if (!identity)
|
|
535
|
+
return { text: "AWN: not started yet." };
|
|
536
|
+
const peers = (0, peer_db_1.listPeers)();
|
|
537
|
+
const activeTransport = _transportManager?.active;
|
|
538
|
+
return {
|
|
539
|
+
text: [
|
|
540
|
+
`**AWN Node**`,
|
|
541
|
+
`Agent ID: \`${identity.agentId}\``,
|
|
542
|
+
`DID Key: \`${(0, identity_1.deriveDidKey)(identity.publicKey)}\``,
|
|
543
|
+
`Transport: ${activeTransport?.id ?? "http-only"}`,
|
|
544
|
+
...(_quicTransport?.isActive() ? [`QUIC: \`${_quicTransport.address}\``] : []),
|
|
545
|
+
`Peers: ${peers.length} known`,
|
|
546
|
+
`Worlds: ${_joinedWorlds.size} joined`,
|
|
547
|
+
].join("\n"),
|
|
548
|
+
};
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
api.registerCommand({
|
|
552
|
+
name: "awn-peers",
|
|
553
|
+
description: "List known AWN peers",
|
|
554
|
+
handler: () => {
|
|
555
|
+
const peers = (0, peer_db_1.listPeers)();
|
|
556
|
+
if (peers.length === 0)
|
|
557
|
+
return { text: "No peers yet. Use `openclaw awn add <agent-id>`." };
|
|
558
|
+
const lines = peers.map((p) => {
|
|
559
|
+
const label = p.alias ? ` — ${p.alias}` : "";
|
|
560
|
+
const ver = p.version ? ` [v${p.version}]` : "";
|
|
561
|
+
return `\`${p.agentId}\`${label}${ver}`;
|
|
562
|
+
});
|
|
563
|
+
return { text: `**Known Peers**\n${lines.join("\n")}` };
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
// ── Agent tools ────────────────────────────────────────────────────────────
|
|
567
|
+
api.registerTool({
|
|
568
|
+
name: "p2p_send_message",
|
|
569
|
+
description: "Send a direct encrypted P2P message to a peer by their agent ID.",
|
|
570
|
+
parameters: {
|
|
571
|
+
type: "object",
|
|
572
|
+
properties: {
|
|
573
|
+
agent_id: { type: "string", description: "The recipient's agent ID" },
|
|
574
|
+
message: { type: "string", description: "The message content to send" },
|
|
575
|
+
event: { type: "string", description: "Message event type (default 'chat'). Use 'world.join' to join a world." },
|
|
576
|
+
port: { type: "integer", description: "Recipient's P2P server port (default 8099)" },
|
|
577
|
+
},
|
|
578
|
+
required: ["agent_id", "message"],
|
|
579
|
+
},
|
|
580
|
+
async execute(_id, params) {
|
|
581
|
+
if (!identity) {
|
|
582
|
+
return { content: [{ type: "text", text: "Error: AWN service not started yet." }] };
|
|
583
|
+
}
|
|
584
|
+
const event = params.event ?? "chat";
|
|
585
|
+
const result = await (0, peer_client_1.sendP2PMessage)(identity, params.agent_id, event, params.message, params.port ?? 8099, 10_000, buildSendOpts(params.agent_id));
|
|
586
|
+
if (result.ok) {
|
|
587
|
+
return { content: [{ type: "text", text: `Message delivered to ${params.agent_id} (event: ${event})` }] };
|
|
588
|
+
}
|
|
589
|
+
return { content: [{ type: "text", text: `Failed to deliver: ${result.error}` }], isError: true };
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
api.registerTool({
|
|
593
|
+
name: "p2p_list_peers",
|
|
594
|
+
description: "List all known peers. Optionally filter by capability prefix (e.g. 'world:' or 'world:pixel-city').",
|
|
595
|
+
parameters: {
|
|
596
|
+
type: "object",
|
|
597
|
+
properties: {
|
|
598
|
+
capability: { type: "string", description: "Filter peers by capability prefix (e.g. 'world:')" },
|
|
599
|
+
},
|
|
600
|
+
required: [],
|
|
601
|
+
},
|
|
602
|
+
async execute(_id, params) {
|
|
603
|
+
const peers = params.capability
|
|
604
|
+
? (0, peer_db_1.findPeersByCapability)(params.capability)
|
|
605
|
+
: (0, peer_db_1.listPeers)();
|
|
606
|
+
if (peers.length === 0) {
|
|
607
|
+
return { content: [{ type: "text", text: "No peers found." }] };
|
|
608
|
+
}
|
|
609
|
+
const lines = peers.map((p) => {
|
|
610
|
+
const ago = Math.round((Date.now() - p.lastSeen) / 1000);
|
|
611
|
+
const label = p.alias ? ` — ${p.alias}` : "";
|
|
612
|
+
const ver = p.version ? ` [v${p.version}]` : "";
|
|
613
|
+
const caps = p.capabilities?.length ? ` [${p.capabilities.join(", ")}]` : "";
|
|
614
|
+
return `${p.agentId}${label}${ver}${caps} — last seen ${ago}s ago`;
|
|
615
|
+
});
|
|
616
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
api.registerTool({
|
|
620
|
+
name: "p2p_status",
|
|
621
|
+
description: "Get this node's agent ID and AWN service status.",
|
|
622
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
623
|
+
async execute(_id, _params) {
|
|
624
|
+
if (!identity) {
|
|
625
|
+
return { content: [{ type: "text", text: "AWN service not started." }] };
|
|
626
|
+
}
|
|
627
|
+
const peers = (0, peer_db_1.listPeers)();
|
|
628
|
+
const activeTransport = _transportManager?.active;
|
|
629
|
+
const lines = [
|
|
630
|
+
...((_agentMeta.name) ? [`Agent name: ${_agentMeta.name}`] : []),
|
|
631
|
+
`Agent ID: ${identity.agentId}`,
|
|
632
|
+
`DID Key: ${(0, identity_1.deriveDidKey)(identity.publicKey)}`,
|
|
633
|
+
`Active transport: ${activeTransport?.id ?? "http-only"}`,
|
|
634
|
+
...(_quicTransport?.isActive() ? [`QUIC endpoint: ${_quicTransport.address}`] : []),
|
|
635
|
+
`Plugin version: v${_agentMeta.version}`,
|
|
636
|
+
`Known peers: ${peers.length}`,
|
|
637
|
+
`Worlds joined: ${_joinedWorlds.size}`,
|
|
638
|
+
];
|
|
639
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
api.registerTool({
|
|
643
|
+
name: "list_worlds",
|
|
644
|
+
description: "List available Agent worlds from the World Registry and local cache.",
|
|
645
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
646
|
+
async execute(_id, _params) {
|
|
647
|
+
// Fetch from Gateway
|
|
648
|
+
let registryWorlds = [];
|
|
649
|
+
try {
|
|
650
|
+
const resp = await fetch(`${getGatewayUrl()}/worlds`, { signal: AbortSignal.timeout(10_000) });
|
|
651
|
+
if (resp.ok) {
|
|
652
|
+
const data = await resp.json();
|
|
653
|
+
for (const w of data.worlds ?? []) {
|
|
654
|
+
if (w.agentId && !registryWorlds.some(rw => rw.agentId === w.agentId)) {
|
|
655
|
+
registryWorlds.push({
|
|
656
|
+
agentId: w.agentId,
|
|
657
|
+
alias: w.name,
|
|
658
|
+
capabilities: [`world:${w.worldId}`],
|
|
659
|
+
lastSeen: w.lastSeen ?? Date.now(),
|
|
660
|
+
});
|
|
661
|
+
(0, peer_db_2.upsertDiscoveredPeer)(w.agentId, "", {
|
|
662
|
+
alias: w.name,
|
|
663
|
+
capabilities: [`world:${w.worldId}`],
|
|
664
|
+
source: "gateway",
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
catch { /* gateway unreachable */ }
|
|
671
|
+
// Merge with local cache
|
|
672
|
+
const localWorlds = (0, peer_db_1.findPeersByCapability)("world:");
|
|
673
|
+
const allWorlds = [...localWorlds];
|
|
674
|
+
for (const rw of registryWorlds) {
|
|
675
|
+
if (!allWorlds.some(w => w.agentId === rw.agentId)) {
|
|
676
|
+
allWorlds.push(rw);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (!allWorlds.length) {
|
|
680
|
+
return { content: [{ type: "text", text: "No worlds found. Use join_world with a world address to connect directly." }] };
|
|
681
|
+
}
|
|
682
|
+
const lines = allWorlds.map((p) => {
|
|
683
|
+
const cap = p.capabilities?.find((c) => c.startsWith("world:")) ?? "";
|
|
684
|
+
const worldId = cap.slice("world:".length);
|
|
685
|
+
const ago = Math.round((Date.now() - (p.lastSeen ?? 0)) / 1000);
|
|
686
|
+
const reachable = p.endpoints?.length ? "reachable" : "no endpoint";
|
|
687
|
+
return `world:${worldId} — ${p.alias || worldId} [${reachable}] — last seen ${ago}s ago`;
|
|
688
|
+
});
|
|
689
|
+
return { content: [{ type: "text", text: `Found ${allWorlds.length} world(s):\n${lines.join("\n")}` }] };
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
api.registerTool({
|
|
693
|
+
name: "join_world",
|
|
694
|
+
description: "Join an Agent world. Provide a world_id (if already known) or address (host:port) to connect directly.",
|
|
695
|
+
parameters: {
|
|
696
|
+
type: "object",
|
|
697
|
+
properties: {
|
|
698
|
+
world_id: { type: "string", description: "The world ID (e.g. 'pixel-city') — looks up from known worlds" },
|
|
699
|
+
address: { type: "string", description: "Direct address of the world server (e.g. 'example.com:8099' or '1.2.3.4:8099')" },
|
|
700
|
+
alias: { type: "string", description: "Optional display name inside the world" },
|
|
701
|
+
},
|
|
702
|
+
required: [],
|
|
703
|
+
},
|
|
704
|
+
async execute(_id, params) {
|
|
705
|
+
if (!identity) {
|
|
706
|
+
return { content: [{ type: "text", text: "AWN service not started." }] };
|
|
707
|
+
}
|
|
708
|
+
if (!params.world_id && !params.address) {
|
|
709
|
+
return { content: [{ type: "text", text: "Provide either world_id or address." }], isError: true };
|
|
710
|
+
}
|
|
711
|
+
let targetAddr;
|
|
712
|
+
let targetPort = peerPort;
|
|
713
|
+
let worldAgentId;
|
|
714
|
+
let worldPublicKey;
|
|
715
|
+
if (params.address) {
|
|
716
|
+
const parsedAddress = (0, address_1.parseDirectPeerAddress)(params.address, peerPort);
|
|
717
|
+
targetAddr = parsedAddress.address;
|
|
718
|
+
targetPort = parsedAddress.port;
|
|
719
|
+
const ping = await (0, peer_client_1.getPeerPingInfo)(targetAddr, targetPort, 5_000);
|
|
720
|
+
if (!ping.ok) {
|
|
721
|
+
return { content: [{ type: "text", text: `World at ${params.address} is unreachable.` }], isError: true };
|
|
722
|
+
}
|
|
723
|
+
if (typeof ping.data?.agentId !== "string" || ping.data.agentId.length === 0) {
|
|
724
|
+
return { content: [{ type: "text", text: `World at ${params.address} did not provide a stable agent ID.` }], isError: true };
|
|
725
|
+
}
|
|
726
|
+
if (typeof ping.data?.publicKey !== "string" || ping.data.publicKey.length === 0) {
|
|
727
|
+
return { content: [{ type: "text", text: `World at ${params.address} did not provide a verifiable public key.` }], isError: true };
|
|
728
|
+
}
|
|
729
|
+
worldAgentId = ping.data.agentId;
|
|
730
|
+
worldPublicKey = ping.data.publicKey;
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
const worlds = (0, peer_db_1.findPeersByCapability)(`world:${params.world_id}`);
|
|
734
|
+
if (!worlds.length) {
|
|
735
|
+
return { content: [{ type: "text", text: `World '${params.world_id}' not found. Use address parameter to connect directly.` }] };
|
|
736
|
+
}
|
|
737
|
+
let world = worlds[0];
|
|
738
|
+
if ((!world.endpoints?.length || !world.publicKey) && params.world_id) {
|
|
739
|
+
const gatewayWorld = await fetchGatewayWorldRecord(params.world_id);
|
|
740
|
+
if (gatewayWorld?.agentId) {
|
|
741
|
+
(0, peer_db_2.upsertDiscoveredPeer)(gatewayWorld.agentId, gatewayWorld.publicKey ?? "", {
|
|
742
|
+
alias: gatewayWorld.alias ?? world.alias,
|
|
743
|
+
capabilities: world.capabilities,
|
|
744
|
+
endpoints: gatewayWorld.endpoints ?? world.endpoints,
|
|
745
|
+
source: "gateway",
|
|
746
|
+
});
|
|
747
|
+
world = (0, peer_db_1.getPeer)(gatewayWorld.agentId) ?? {
|
|
748
|
+
...world,
|
|
749
|
+
agentId: gatewayWorld.agentId,
|
|
750
|
+
alias: gatewayWorld.alias ?? world.alias,
|
|
751
|
+
endpoints: gatewayWorld.endpoints ?? world.endpoints,
|
|
752
|
+
publicKey: gatewayWorld.publicKey ?? world.publicKey,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (!world.endpoints?.length) {
|
|
757
|
+
return { content: [{ type: "text", text: `World '${params.world_id}' has no reachable endpoints.` }] };
|
|
758
|
+
}
|
|
759
|
+
targetAddr = world.endpoints[0].address;
|
|
760
|
+
targetPort = world.endpoints[0].port ?? peerPort;
|
|
761
|
+
worldAgentId = world.agentId;
|
|
762
|
+
worldPublicKey = (0, peer_db_1.getPeer)(worldAgentId)?.publicKey ?? world.publicKey ?? "";
|
|
763
|
+
}
|
|
764
|
+
if (!worldPublicKey) {
|
|
765
|
+
return { content: [{ type: "text", text: "World public key is unavailable; cannot verify signed membership refreshes." }], isError: true };
|
|
766
|
+
}
|
|
767
|
+
const myEndpoints = _agentMeta.endpoints ?? [];
|
|
768
|
+
if (myEndpoints.length === 0) {
|
|
769
|
+
return {
|
|
770
|
+
content: [{
|
|
771
|
+
type: "text",
|
|
772
|
+
text: "No reachable endpoint can be advertised. Set advertise_address (for HTTP/TCP) or configure QUIC before joining a world.",
|
|
773
|
+
}],
|
|
774
|
+
isError: true,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const joinPayload = JSON.stringify({
|
|
778
|
+
alias: params.alias ?? _agentMeta.name ?? identity.agentId.slice(0, 8),
|
|
779
|
+
endpoints: myEndpoints,
|
|
780
|
+
});
|
|
781
|
+
const sendOpts = buildSendOpts(worldAgentId);
|
|
782
|
+
sendOpts.expectedPublicKey = worldPublicKey;
|
|
783
|
+
const result = await (0, peer_client_1.sendP2PMessage)(identity, targetAddr, "world.join", joinPayload, targetPort, 10_000, sendOpts);
|
|
784
|
+
if (!result.ok) {
|
|
785
|
+
return { content: [{ type: "text", text: `Failed to join world: ${result.error}` }], isError: true };
|
|
786
|
+
}
|
|
787
|
+
const worldId = (result.data?.worldId ?? params.world_id ?? params.address);
|
|
788
|
+
const members = result.data?.members;
|
|
789
|
+
const memberCount = members?.length ?? 0;
|
|
790
|
+
const worldName = typeof result.data?.manifest === "object" && result.data?.manifest && typeof result.data.manifest.name === "string"
|
|
791
|
+
? result.data.manifest.name
|
|
792
|
+
: worldId;
|
|
793
|
+
(0, peer_db_2.upsertDiscoveredPeer)(worldAgentId, worldPublicKey, {
|
|
794
|
+
alias: worldName,
|
|
795
|
+
capabilities: [`world:${worldId}`],
|
|
796
|
+
endpoints: [{ transport: "tcp", address: targetAddr, port: targetPort, priority: 1, ttl: 3600 }],
|
|
797
|
+
source: "gossip",
|
|
798
|
+
});
|
|
799
|
+
const joinMembers = result.data?.members ?? [];
|
|
800
|
+
syncWorldMembers(worldId, joinMembers);
|
|
801
|
+
(0, peer_server_1.addWorldMembers)(worldId, [worldAgentId, ...joinMembers.map(m => m.agentId).filter(id => id !== identity.agentId)]);
|
|
802
|
+
// Track this world for periodic member refresh
|
|
803
|
+
_joinedWorlds.set(worldId, { agentId: worldAgentId, address: targetAddr, port: targetPort, publicKey: worldPublicKey });
|
|
804
|
+
_worldRefreshFailures.delete(worldId);
|
|
805
|
+
if (!_memberRefreshTimer) {
|
|
806
|
+
_memberRefreshTimer = setInterval(refreshWorldMembers, MEMBER_REFRESH_INTERVAL_MS);
|
|
807
|
+
}
|
|
808
|
+
return { content: [{ type: "text", text: `Joined world '${worldId}' — ${memberCount} other member(s) discovered` }] };
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
//# sourceMappingURL=index.js.map
|