openclaw-core 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 +15 -0
- package/README.md +122 -0
- package/dist/brain/MemoryManager.d.ts +25 -0
- package/dist/brain/MemoryManager.js +53 -0
- package/dist/common/lib/BoundaryChecker.d.ts +18 -0
- package/dist/common/lib/BoundaryChecker.js +87 -0
- package/dist/common/lib/EmbeddingService.d.ts +25 -0
- package/dist/common/lib/EmbeddingService.js +77 -0
- package/dist/common/lib/VectorStore.d.ts +16 -0
- package/dist/common/lib/VectorStore.js +42 -0
- package/dist/common/protocols/SAML.d.ts +32 -0
- package/dist/common/protocols/SAML.js +107 -0
- package/dist/harness/AgentEvaluator.d.ts +18 -0
- package/dist/harness/AgentEvaluator.js +117 -0
- package/dist/harness/AgentEvaluator.test.d.ts +1 -0
- package/dist/harness/AgentEvaluator.test.js +46 -0
- package/dist/harness/AgenticHarness.d.ts +14 -0
- package/dist/harness/AgenticHarness.js +56 -0
- package/dist/harness/AgenticHarness.test.d.ts +1 -0
- package/dist/harness/AgenticHarness.test.js +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +13 -0
- package/dist/interfaces/index.d.ts +170 -0
- package/dist/interfaces/index.js +65 -0
- package/dist/resonance/SignalManager.d.ts +39 -0
- package/dist/resonance/SignalManager.js +118 -0
- package/dist/services/PeerDiscovery.d.ts +47 -0
- package/dist/services/PeerDiscovery.js +217 -0
- package/dist/services/PeerRegistry.d.ts +102 -0
- package/dist/services/PeerRegistry.js +438 -0
- package/dist/store/BaseStore.d.ts +47 -0
- package/dist/store/BaseStore.js +77 -0
- package/package.json +47 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const HEARTBEAT_INTERVAL = 30_000;
|
|
3
|
+
const HEARTBEAT_JITTER = 5_000;
|
|
4
|
+
const OFFLINE_THRESHOLD = 3;
|
|
5
|
+
const MAX_BACKOFF = 300_000;
|
|
6
|
+
const OUTBOX_FLUSH_BATCH = 50;
|
|
7
|
+
export class PeerRegistry {
|
|
8
|
+
peers = new Map();
|
|
9
|
+
heartbeatTimers = new Map();
|
|
10
|
+
reconnectTimers = new Map();
|
|
11
|
+
db = null;
|
|
12
|
+
signalManager = null;
|
|
13
|
+
localDid;
|
|
14
|
+
localUrl;
|
|
15
|
+
localPublicKey;
|
|
16
|
+
localMachineFingerprint = '';
|
|
17
|
+
privateKey = null;
|
|
18
|
+
constructor(localDid, localUrl, localPublicKey) {
|
|
19
|
+
this.localDid = localDid;
|
|
20
|
+
this.localUrl = localUrl;
|
|
21
|
+
this.localPublicKey = localPublicKey;
|
|
22
|
+
}
|
|
23
|
+
async init(db, signalManager) {
|
|
24
|
+
this.db = db;
|
|
25
|
+
this.signalManager = signalManager;
|
|
26
|
+
await this.bootstrapSchema();
|
|
27
|
+
await this.restoreFromDB();
|
|
28
|
+
for (const [did, peer] of this.peers) {
|
|
29
|
+
if (peer.status === 'ONLINE' || peer.status === 'CONNECTING') {
|
|
30
|
+
this.startHeartbeat(did);
|
|
31
|
+
if (this.signalManager)
|
|
32
|
+
this.signalManager.addPeer(peer.url);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
setPrivateKey(key) { this.privateKey = key; }
|
|
37
|
+
setMachineFingerprint(fingerprint) { this.localMachineFingerprint = fingerprint; }
|
|
38
|
+
updateIdentity(did, url, publicKey) {
|
|
39
|
+
this.localDid = did;
|
|
40
|
+
this.localUrl = url;
|
|
41
|
+
this.localPublicKey = publicKey;
|
|
42
|
+
}
|
|
43
|
+
async bootstrapSchema() {
|
|
44
|
+
if (!this.db)
|
|
45
|
+
return;
|
|
46
|
+
const schema = [
|
|
47
|
+
`DEFINE TABLE IF NOT EXISTS peer_nodes SCHEMALESS`,
|
|
48
|
+
`DEFINE INDEX IF NOT EXISTS peer_did_idx ON peer_nodes FIELDS did UNIQUE`,
|
|
49
|
+
`DEFINE TABLE IF NOT EXISTS offline_outbox SCHEMALESS`,
|
|
50
|
+
`DEFINE INDEX IF NOT EXISTS outbox_compound ON offline_outbox FIELDS target_did, created_at`,
|
|
51
|
+
`DEFINE TABLE IF NOT EXISTS trusts TYPE RELATION`,
|
|
52
|
+
`DEFINE TABLE IF NOT EXISTS follows TYPE RELATION CHANGEFEED 30d`,
|
|
53
|
+
`DEFINE TABLE IF NOT EXISTS friends TYPE RELATION CHANGEFEED 30d`,
|
|
54
|
+
`DEFINE EVENT IF NOT EXISTS peer_follow_broadcast ON follows WHEN $event = "CREATE" THEN {
|
|
55
|
+
INSERT INTO offline_outbox {
|
|
56
|
+
id: rand::uuid::v7(),
|
|
57
|
+
target_did: "ALL_PEERS",
|
|
58
|
+
signal_type: "RELATIONSHIP_UPDATE",
|
|
59
|
+
payload: { from: $after.in, to: $after.out, action: "FOLLOW" },
|
|
60
|
+
created_at: time::now(),
|
|
61
|
+
expire_at: time::now() + 7d,
|
|
62
|
+
attempts: 0
|
|
63
|
+
}
|
|
64
|
+
}`,
|
|
65
|
+
`DEFINE TABLE IF NOT EXISTS transactions SCHEMALESS
|
|
66
|
+
PERMISSIONS
|
|
67
|
+
FOR select WHERE sender = $auth.did OR receiver = $auth.did
|
|
68
|
+
FOR create, update, delete NONE`,
|
|
69
|
+
];
|
|
70
|
+
for (const stmt of schema) {
|
|
71
|
+
try {
|
|
72
|
+
await this.db.query(stmt);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
if (!e.message?.includes('already exists')) {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async restoreFromDB() {
|
|
81
|
+
if (!this.db)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
const result = await this.db.query("SELECT * FROM peer_nodes ORDER BY updated_at DESC");
|
|
85
|
+
const rows = result[0]?.result || result[0] || [];
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
this.peers.set(row.did, {
|
|
88
|
+
did: row.did,
|
|
89
|
+
url: row.url,
|
|
90
|
+
publicKey: row.public_key || '',
|
|
91
|
+
status: 'CONNECTING',
|
|
92
|
+
rtt: row.rtt || 0,
|
|
93
|
+
syncCursor: row.sync_cursor || new Date().toISOString(),
|
|
94
|
+
lastHeartbeat: new Date(row.updated_at || Date.now()),
|
|
95
|
+
retryCount: 0,
|
|
96
|
+
location: row.location,
|
|
97
|
+
trustScore: row.trust_score || 0,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
throw new Error(`[PeerRegistry] DB restore failed: ${e.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
validateUrl(url) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = new URL(url);
|
|
108
|
+
const isLocal = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
|
|
109
|
+
if (isLocal)
|
|
110
|
+
return true;
|
|
111
|
+
return parsed.protocol === 'https:' || parsed.protocol === 'wss:';
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async connect(remoteUrl, isLAN = false) {
|
|
118
|
+
if (!this.validateUrl(remoteUrl)) {
|
|
119
|
+
throw new Error(`[PeerRegistry] Rejected insecure URL: ${remoteUrl}`);
|
|
120
|
+
}
|
|
121
|
+
const payload = {
|
|
122
|
+
did: this.localDid,
|
|
123
|
+
publicKey: this.localPublicKey,
|
|
124
|
+
url: this.localUrl,
|
|
125
|
+
machineFingerprint: this.localMachineFingerprint,
|
|
126
|
+
nodeInfo: {
|
|
127
|
+
version: '4.5',
|
|
128
|
+
capabilities: ['CHANGEFEED', 'LIVE_SELECT', 'CBOR', 'GRAPH_RAG', 'TEMPORAL', 'GEOSPATIAL', 'HNSW', 'DHT'],
|
|
129
|
+
},
|
|
130
|
+
signature: this.signPayload({ did: this.localDid, url: this.localUrl, timestamp: Date.now() }),
|
|
131
|
+
timestamp: Date.now(),
|
|
132
|
+
};
|
|
133
|
+
const response = await fetch(`${remoteUrl}/api/peer/handshake`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify(payload),
|
|
137
|
+
signal: AbortSignal.timeout(10000),
|
|
138
|
+
});
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const err = await response.json().catch(() => ({ error: 'Unknown' }));
|
|
141
|
+
throw new Error(`[PeerRegistry] Handshake rejected: ${JSON.stringify(err)}`);
|
|
142
|
+
}
|
|
143
|
+
const result = await response.json();
|
|
144
|
+
if (!result.accepted)
|
|
145
|
+
return false;
|
|
146
|
+
const peer = {
|
|
147
|
+
did: result.did,
|
|
148
|
+
url: result.url,
|
|
149
|
+
publicKey: result.publicKey,
|
|
150
|
+
status: 'ONLINE',
|
|
151
|
+
rtt: 0,
|
|
152
|
+
syncCursor: result.changefeedCursor || new Date().toISOString(),
|
|
153
|
+
lastHeartbeat: new Date(),
|
|
154
|
+
retryCount: 0,
|
|
155
|
+
location: result.location,
|
|
156
|
+
trustScore: 50,
|
|
157
|
+
jwt: result.jwt,
|
|
158
|
+
machineFingerprint: result.machineFingerprint,
|
|
159
|
+
isLocal: result.machineFingerprint === this.localMachineFingerprint,
|
|
160
|
+
isLAN,
|
|
161
|
+
};
|
|
162
|
+
await this.register(peer);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
async acceptHandshake(payload) {
|
|
166
|
+
if (!this.validateUrl(payload.url))
|
|
167
|
+
return null;
|
|
168
|
+
if (!payload.did || !payload.publicKey || !payload.url)
|
|
169
|
+
return null;
|
|
170
|
+
if (payload.did === this.localDid)
|
|
171
|
+
return null;
|
|
172
|
+
const isLocal = payload.machineFingerprint === this.localMachineFingerprint;
|
|
173
|
+
const peer = {
|
|
174
|
+
did: payload.did,
|
|
175
|
+
url: payload.url,
|
|
176
|
+
publicKey: payload.publicKey,
|
|
177
|
+
status: 'ONLINE',
|
|
178
|
+
rtt: 0,
|
|
179
|
+
syncCursor: new Date().toISOString(),
|
|
180
|
+
lastHeartbeat: new Date(),
|
|
181
|
+
retryCount: 0,
|
|
182
|
+
location: payload.nodeInfo?.location,
|
|
183
|
+
trustScore: isLocal ? 90 : 50,
|
|
184
|
+
machineFingerprint: payload.machineFingerprint,
|
|
185
|
+
isLocal,
|
|
186
|
+
};
|
|
187
|
+
await this.register(peer);
|
|
188
|
+
return {
|
|
189
|
+
accepted: true,
|
|
190
|
+
did: this.localDid,
|
|
191
|
+
publicKey: this.localPublicKey,
|
|
192
|
+
url: this.localUrl,
|
|
193
|
+
machineFingerprint: this.localMachineFingerprint,
|
|
194
|
+
jwt: this.signJWT(payload.did),
|
|
195
|
+
changefeedCursor: new Date().toISOString(),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async register(peer) {
|
|
199
|
+
const existing = this.peers.get(peer.did);
|
|
200
|
+
this.peers.set(peer.did, peer);
|
|
201
|
+
if (this.db) {
|
|
202
|
+
await this.db.query(`UPSERT peer_nodes:[$did] CONTENT {
|
|
203
|
+
did: $did, url: $url, public_key: $pubKey, status: $status,
|
|
204
|
+
rtt: $rtt, sync_cursor: $cursor, trust_score: $trust,
|
|
205
|
+
location: $location, updated_at: time::now()
|
|
206
|
+
}`, { did: peer.did, url: peer.url, pubKey: peer.publicKey, status: peer.status,
|
|
207
|
+
rtt: peer.rtt, cursor: peer.syncCursor, trust: peer.trustScore, location: peer.location || null }).catch((e) => { throw new Error(`[PeerRegistry] DB upsert failed: ${e.message}`); });
|
|
208
|
+
if (!existing) {
|
|
209
|
+
await this.db.query(`INSERT INTO trusts (in, out, score, since) VALUES ($from, $to, 50, time::now())`, { from: `agents:${this.localDid.split(':').pop()}`, to: `agents:${peer.did.split(':').pop()}` }).catch(() => { });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (this.signalManager && peer.status === 'ONLINE') {
|
|
213
|
+
this.signalManager.addPeer(peer.url);
|
|
214
|
+
}
|
|
215
|
+
this.startHeartbeat(peer.did);
|
|
216
|
+
}
|
|
217
|
+
async remove(did) {
|
|
218
|
+
const peer = this.peers.get(did);
|
|
219
|
+
this.peers.delete(did);
|
|
220
|
+
this.stopHeartbeat(did);
|
|
221
|
+
this.stopReconnect(did);
|
|
222
|
+
if (peer && this.signalManager?.removePeer)
|
|
223
|
+
this.signalManager.removePeer(peer.url);
|
|
224
|
+
if (this.db) {
|
|
225
|
+
await this.db.query("DELETE FROM peer_nodes WHERE did = $did", { did }).catch(() => { });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
list() { return Array.from(this.peers.values()); }
|
|
229
|
+
listOnline() { return this.list().filter(p => p.status === 'ONLINE'); }
|
|
230
|
+
get(did) { return this.peers.get(did); }
|
|
231
|
+
listByDistance(lat, lon, maxKm = 99999) {
|
|
232
|
+
return this.list()
|
|
233
|
+
.filter(p => p.location && p.status === 'ONLINE')
|
|
234
|
+
.map(p => ({ peer: p, dist: this.haversine(lat, lon, p.location.lat, p.location.lon) }))
|
|
235
|
+
.filter(x => x.dist <= maxKm)
|
|
236
|
+
.sort((a, b) => a.dist - b.dist)
|
|
237
|
+
.map(x => x.peer);
|
|
238
|
+
}
|
|
239
|
+
haversine(lat1, lon1, lat2, lon2) {
|
|
240
|
+
const R = 6371;
|
|
241
|
+
const dLat = (lat2 - lat1) * Math.PI / 180;
|
|
242
|
+
const dLon = (lon2 - lon1) * Math.PI / 180;
|
|
243
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
|
|
244
|
+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
245
|
+
}
|
|
246
|
+
startHeartbeat(did) {
|
|
247
|
+
this.stopHeartbeat(did);
|
|
248
|
+
const jitter = Math.random() * HEARTBEAT_JITTER * 2 - HEARTBEAT_JITTER;
|
|
249
|
+
const timer = setInterval(async () => { await this.sendHeartbeat(did); }, HEARTBEAT_INTERVAL + jitter);
|
|
250
|
+
this.heartbeatTimers.set(did, timer);
|
|
251
|
+
}
|
|
252
|
+
stopHeartbeat(did) {
|
|
253
|
+
const timer = this.heartbeatTimers.get(did);
|
|
254
|
+
if (timer) {
|
|
255
|
+
clearInterval(timer);
|
|
256
|
+
this.heartbeatTimers.delete(did);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async sendHeartbeat(did) {
|
|
260
|
+
const peer = this.peers.get(did);
|
|
261
|
+
if (!peer || peer.status === 'OFFLINE')
|
|
262
|
+
return;
|
|
263
|
+
try {
|
|
264
|
+
const startTime = Date.now();
|
|
265
|
+
const res = await fetch(`${peer.url}/api/peer/heartbeat`, {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
268
|
+
body: JSON.stringify({ did: this.localDid, timestamp: Date.now(), syncCursor: peer.syncCursor }),
|
|
269
|
+
signal: AbortSignal.timeout(10_000),
|
|
270
|
+
});
|
|
271
|
+
if (res.ok) {
|
|
272
|
+
const data = await res.json();
|
|
273
|
+
peer.rtt = Date.now() - startTime;
|
|
274
|
+
peer.lastHeartbeat = new Date();
|
|
275
|
+
peer.retryCount = 0;
|
|
276
|
+
peer.status = 'ONLINE';
|
|
277
|
+
if (data.latestCursor)
|
|
278
|
+
peer.syncCursor = data.latestCursor;
|
|
279
|
+
if (this.db) {
|
|
280
|
+
this.db.query("UPDATE peer_nodes SET rtt = $rtt, status = 'ONLINE', sync_cursor = $cursor, updated_at = time::now() WHERE did = $did", { did, rtt: peer.rtt, cursor: peer.syncCursor }).catch(() => { });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
this.handleHeartbeatFailure(did);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
this.handleHeartbeatFailure(did);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
handleHeartbeatFailure(did) {
|
|
292
|
+
const peer = this.peers.get(did);
|
|
293
|
+
if (!peer)
|
|
294
|
+
return;
|
|
295
|
+
peer.retryCount++;
|
|
296
|
+
if (peer.retryCount >= OFFLINE_THRESHOLD)
|
|
297
|
+
this.markOffline(did);
|
|
298
|
+
}
|
|
299
|
+
markOffline(did) {
|
|
300
|
+
const peer = this.peers.get(did);
|
|
301
|
+
if (!peer || peer.status === 'OFFLINE')
|
|
302
|
+
return;
|
|
303
|
+
peer.status = 'OFFLINE';
|
|
304
|
+
this.stopHeartbeat(did);
|
|
305
|
+
if (this.signalManager?.removePeer)
|
|
306
|
+
this.signalManager.removePeer(peer.url);
|
|
307
|
+
if (this.db) {
|
|
308
|
+
this.db.query("UPDATE peer_nodes SET status = 'OFFLINE', updated_at = time::now() WHERE did = $did", { did }).catch(() => { });
|
|
309
|
+
}
|
|
310
|
+
this.startReconnect(did);
|
|
311
|
+
}
|
|
312
|
+
startReconnect(did) {
|
|
313
|
+
this.stopReconnect(did);
|
|
314
|
+
const peer = this.peers.get(did);
|
|
315
|
+
if (!peer)
|
|
316
|
+
return;
|
|
317
|
+
const attempt = peer.retryCount - OFFLINE_THRESHOLD;
|
|
318
|
+
const delay = Math.min(2000 * Math.pow(2, attempt), MAX_BACKOFF) + Math.random() * 1000;
|
|
319
|
+
const timer = setTimeout(async () => {
|
|
320
|
+
const ok = await this.connect(peer.url).catch(() => false);
|
|
321
|
+
if (ok) {
|
|
322
|
+
await this.flushOutbox(did);
|
|
323
|
+
await this.syncChangeFeed(did);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
peer.retryCount++;
|
|
327
|
+
this.startReconnect(did);
|
|
328
|
+
}
|
|
329
|
+
}, delay);
|
|
330
|
+
this.reconnectTimers.set(did, timer);
|
|
331
|
+
}
|
|
332
|
+
stopReconnect(did) {
|
|
333
|
+
const timer = this.reconnectTimers.get(did);
|
|
334
|
+
if (timer) {
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
this.reconnectTimers.delete(did);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async enqueueOutbox(targetDid, signalType, payload) {
|
|
340
|
+
if (!this.db)
|
|
341
|
+
return;
|
|
342
|
+
await this.db.query(`INSERT INTO offline_outbox {
|
|
343
|
+
id: rand::uuid::v7(), target_did: $targetDid, signal_type: $signalType,
|
|
344
|
+
payload: $payload, created_at: time::now(), expire_at: time::now() + 7d, attempts: 0
|
|
345
|
+
}`, { targetDid, signalType, payload });
|
|
346
|
+
}
|
|
347
|
+
async flushOutbox(targetDid) {
|
|
348
|
+
if (!this.db)
|
|
349
|
+
return;
|
|
350
|
+
const peer = this.peers.get(targetDid);
|
|
351
|
+
if (!peer || peer.status !== 'ONLINE')
|
|
352
|
+
return;
|
|
353
|
+
const result = await this.db.query(`SELECT * FROM offline_outbox
|
|
354
|
+
WHERE (target_did = $did OR target_did = "ALL_PEERS")
|
|
355
|
+
AND expire_at > time::now() ORDER BY created_at ASC LIMIT $limit`, { did: targetDid, limit: OUTBOX_FLUSH_BATCH });
|
|
356
|
+
const messages = result[0]?.result || result[0] || [];
|
|
357
|
+
if (messages.length === 0)
|
|
358
|
+
return;
|
|
359
|
+
const res = await fetch(`${peer.url}/api/peer/deliver`, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: { 'Content-Type': 'application/json' },
|
|
362
|
+
body: JSON.stringify({ from: this.localDid, messages: messages.map((m) => ({ signal_type: m.signal_type, payload: m.payload, created_at: m.created_at })) }),
|
|
363
|
+
signal: AbortSignal.timeout(30_000),
|
|
364
|
+
});
|
|
365
|
+
if (res.ok) {
|
|
366
|
+
const ids = messages.map((m) => m.id).filter(Boolean);
|
|
367
|
+
if (ids.length > 0) {
|
|
368
|
+
await this.db.query("DELETE FROM offline_outbox WHERE id IN $ids", { ids }).catch(() => { });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
await this.db.query("DELETE FROM offline_outbox WHERE expire_at < time::now()").catch(() => { });
|
|
372
|
+
}
|
|
373
|
+
async syncChangeFeed(targetDid) {
|
|
374
|
+
const peer = this.peers.get(targetDid);
|
|
375
|
+
if (!peer || peer.status !== 'ONLINE')
|
|
376
|
+
return;
|
|
377
|
+
const res = await fetch(`${peer.url}/api/peer/sync`, {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
headers: { 'Content-Type': 'application/json' },
|
|
380
|
+
body: JSON.stringify({ did: this.localDid, since: peer.syncCursor, tables: ['follows', 'friends', 'agent_logs'] }),
|
|
381
|
+
signal: AbortSignal.timeout(30_000),
|
|
382
|
+
});
|
|
383
|
+
if (res.ok) {
|
|
384
|
+
const data = await res.json();
|
|
385
|
+
for (const change of (data.changes || [])) {
|
|
386
|
+
if (change.operation === 'CREATE' && this.db) {
|
|
387
|
+
await this.db.query(`INSERT INTO ${change.table} $data`, { data: change.data }).catch(() => { });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (data.cursor) {
|
|
391
|
+
peer.syncCursor = data.cursor;
|
|
392
|
+
if (this.db) {
|
|
393
|
+
this.db.query("UPDATE peer_nodes SET sync_cursor = $cursor WHERE did = $did", { did: targetDid, cursor: data.cursor }).catch(() => { });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async queryTrustChain(targetDid, maxDepth = 3) {
|
|
399
|
+
if (!this.db)
|
|
400
|
+
return [];
|
|
401
|
+
try {
|
|
402
|
+
const result = await this.db.query(`SELECT ->trusts->${'->trusts->'.repeat(maxDepth - 1)}?? AS chain FROM agents:[$did]`, { did: this.localDid.split(':').pop() });
|
|
403
|
+
return result[0]?.result || [];
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
signPayload(data) {
|
|
410
|
+
const str = JSON.stringify(data);
|
|
411
|
+
if (this.privateKey) {
|
|
412
|
+
const pkcs8Key = Buffer.concat([
|
|
413
|
+
Buffer.from('302e020100300506032b657004220420', 'hex'),
|
|
414
|
+
Buffer.from(this.privateKey, 'hex')
|
|
415
|
+
]);
|
|
416
|
+
return crypto.sign(undefined, Buffer.from(str), crypto.createPrivateKey({ key: pkcs8Key, format: 'der', type: 'pkcs8' })).toString('hex');
|
|
417
|
+
}
|
|
418
|
+
return crypto.createHash('sha256').update(str).digest('hex').substring(0, 32);
|
|
419
|
+
}
|
|
420
|
+
signJWT(targetDid) {
|
|
421
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
422
|
+
const payload = Buffer.from(JSON.stringify({
|
|
423
|
+
iss: this.localDid, sub: targetDid, aud: 'lobster-federation',
|
|
424
|
+
iat: Math.floor(Date.now() / 1000),
|
|
425
|
+
exp: Math.floor(Date.now() / 1000) + 86400,
|
|
426
|
+
scope: 'peer:read',
|
|
427
|
+
})).toString('base64url');
|
|
428
|
+
const secret = this.privateKey || process.env.JWT_SECRET || 'lobster-federation-secret';
|
|
429
|
+
const sig = crypto.createHmac('sha256', secret).update(`${header}.${payload}`).digest('base64url');
|
|
430
|
+
return `${header}.${payload}.${sig}`;
|
|
431
|
+
}
|
|
432
|
+
destroy() {
|
|
433
|
+
for (const [did] of this.heartbeatTimers)
|
|
434
|
+
this.stopHeartbeat(did);
|
|
435
|
+
for (const [did] of this.reconnectTimers)
|
|
436
|
+
this.stopReconnect(did);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SignalManager } from "../resonance/SignalManager.js";
|
|
2
|
+
import { MemoryManager } from "../brain/MemoryManager.js";
|
|
3
|
+
import { type IDLPManager, type ILedgerPlugin, type IVaultPlugin, type IEvolutionPlugin } from "../interfaces/index.js";
|
|
4
|
+
export interface AgentIdentityBase {
|
|
5
|
+
did: string;
|
|
6
|
+
name: string;
|
|
7
|
+
role: string;
|
|
8
|
+
version: string;
|
|
9
|
+
capabilities: string[];
|
|
10
|
+
created_at: Date;
|
|
11
|
+
last_active: Date;
|
|
12
|
+
}
|
|
13
|
+
export declare class BaseStateStore {
|
|
14
|
+
identity: AgentIdentityBase;
|
|
15
|
+
db: any;
|
|
16
|
+
signals: any[];
|
|
17
|
+
lastSync: string;
|
|
18
|
+
signalManager: SignalManager;
|
|
19
|
+
memory: MemoryManager;
|
|
20
|
+
private plugins;
|
|
21
|
+
protected listeners: any[];
|
|
22
|
+
broadcastSSE: (data: any) => void;
|
|
23
|
+
usePlugin(name: 'dlp', impl: IDLPManager): void;
|
|
24
|
+
usePlugin(name: 'ledger', impl: ILedgerPlugin): void;
|
|
25
|
+
usePlugin(name: 'vault', impl: IVaultPlugin): void;
|
|
26
|
+
usePlugin(name: 'evolution', impl: IEvolutionPlugin): void;
|
|
27
|
+
get dlp(): IDLPManager;
|
|
28
|
+
get ledger(): ILedgerPlugin;
|
|
29
|
+
get vault(): IVaultPlugin;
|
|
30
|
+
get evolution(): IEvolutionPlugin;
|
|
31
|
+
setDB(db: any): Promise<void>;
|
|
32
|
+
updateIdentity(identity: Partial<AgentIdentityBase> & {
|
|
33
|
+
did: string;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
protected onSignal(signal: any): void;
|
|
36
|
+
addSignal(signal: any): Promise<import("../resonance/SignalManager.js").Signal>;
|
|
37
|
+
registerListener(res: any): void;
|
|
38
|
+
removeListener(res: any): void;
|
|
39
|
+
initBroadcaster(): void;
|
|
40
|
+
protected internalBroadcastSSE(data: any): void;
|
|
41
|
+
snapshot(): {
|
|
42
|
+
identity: AgentIdentityBase;
|
|
43
|
+
plugins_loaded: string[];
|
|
44
|
+
signals_cached: number;
|
|
45
|
+
last_sync: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { SignalManager } from "../resonance/SignalManager.js";
|
|
2
|
+
import { MemoryManager } from "../brain/MemoryManager.js";
|
|
3
|
+
import { defaultPlugins, } from "../interfaces/index.js";
|
|
4
|
+
export class BaseStateStore {
|
|
5
|
+
identity = {
|
|
6
|
+
did: "",
|
|
7
|
+
name: "OPENCLAW_NODE",
|
|
8
|
+
role: "AGENT",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
capabilities: [],
|
|
11
|
+
created_at: new Date(),
|
|
12
|
+
last_active: new Date(),
|
|
13
|
+
};
|
|
14
|
+
db = null;
|
|
15
|
+
signals = [];
|
|
16
|
+
lastSync = new Date().toISOString();
|
|
17
|
+
signalManager = new SignalManager(this.identity.did);
|
|
18
|
+
memory = new MemoryManager();
|
|
19
|
+
plugins = { ...defaultPlugins };
|
|
20
|
+
listeners = [];
|
|
21
|
+
broadcastSSE = () => { };
|
|
22
|
+
usePlugin(name, impl) {
|
|
23
|
+
this.plugins[name] = impl;
|
|
24
|
+
console.log(`🔌 [OpenClaw] Plugin installed: ${name}`);
|
|
25
|
+
}
|
|
26
|
+
get dlp() { return this.plugins.dlp; }
|
|
27
|
+
get ledger() { return this.plugins.ledger; }
|
|
28
|
+
get vault() { return this.plugins.vault; }
|
|
29
|
+
get evolution() { return this.plugins.evolution; }
|
|
30
|
+
async setDB(db) {
|
|
31
|
+
this.db = db;
|
|
32
|
+
this.memory.db = db;
|
|
33
|
+
await this.memory.initSchema(db);
|
|
34
|
+
this.signalManager = new SignalManager(this.identity.did, this.memory, db);
|
|
35
|
+
this.signalManager.on("*", (msg) => this.onSignal(msg));
|
|
36
|
+
console.log("[BaseStore] DB connected. Schema initialized.");
|
|
37
|
+
}
|
|
38
|
+
async updateIdentity(identity) {
|
|
39
|
+
this.identity = { ...this.identity, ...identity, last_active: new Date() };
|
|
40
|
+
this.signalManager.setAgentId(identity.did);
|
|
41
|
+
if (this.db) {
|
|
42
|
+
await this.db.query("UPSERT agents CONTENT $agent", { agent: { ...this.identity, id: `agents:${identity.did.split(':').pop()}` } }).catch((e) => console.error("[BaseStore] Identity anchor failed:", e));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
onSignal(signal) {
|
|
46
|
+
this.signals.unshift(signal);
|
|
47
|
+
if (this.signals.length > 50)
|
|
48
|
+
this.signals.pop();
|
|
49
|
+
this.internalBroadcastSSE(signal);
|
|
50
|
+
}
|
|
51
|
+
async addSignal(signal) {
|
|
52
|
+
return this.signalManager.sendTo(signal.to || "", signal.type, signal.payload);
|
|
53
|
+
}
|
|
54
|
+
registerListener(res) { this.listeners.push(res); }
|
|
55
|
+
removeListener(res) { this.listeners = this.listeners.filter(l => l !== res); }
|
|
56
|
+
initBroadcaster() {
|
|
57
|
+
this.broadcastSSE = (data) => this.internalBroadcastSSE(data);
|
|
58
|
+
}
|
|
59
|
+
internalBroadcastSSE(data) {
|
|
60
|
+
this.listeners.forEach(res => {
|
|
61
|
+
try {
|
|
62
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
snapshot() {
|
|
68
|
+
return {
|
|
69
|
+
identity: this.identity,
|
|
70
|
+
plugins_loaded: Object.entries(this.plugins)
|
|
71
|
+
.filter(([, v]) => !(v.constructor.name.startsWith("Null")))
|
|
72
|
+
.map(([k]) => k),
|
|
73
|
+
signals_cached: this.signals.length,
|
|
74
|
+
last_sync: this.lastSync,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Open-source AI Agent OS primitive layer — P2P federation, sovereign protocols, and plugin-ready base store. Built on the Lobster Civilization architecture.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./protocols": "./dist/common/protocols/index.js",
|
|
11
|
+
"./store": "./dist/store/BaseStore.js",
|
|
12
|
+
"./interfaces": "./dist/interfaces/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "node -e \"const fs=require('fs'); ['dist', 'tsconfig.tsbuildinfo'].forEach(p => { if(fs.existsSync(p)) fs.rmSync(p, {recursive:true, force:true})});\"",
|
|
21
|
+
"build:tsc": "tsc",
|
|
22
|
+
"build": "npm run clean && npm run build:tsc",
|
|
23
|
+
"check": "tsc --noEmit",
|
|
24
|
+
"audit:leakage": "node -e \"const fs=require('fs'), path=require('path'); const walk=(d)=>{if(!fs.existsSync(d)) return; fs.readdirSync(d,{withFileTypes:true}).forEach(e=>{const p=path.join(d,e.name); if(e.isDirectory())walk(p); else if(e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) { console.error('🚫 [Security] Core source leak detected: ' + p); process.exit(1); }})}; walk('./dist'); console.log('✅ [Security] Core leakage check passed.');\"",
|
|
25
|
+
"prepublishOnly": "npm run build && npm run audit:leakage"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"agent",
|
|
29
|
+
"openclaw",
|
|
30
|
+
"lobster",
|
|
31
|
+
"p2p",
|
|
32
|
+
"federation",
|
|
33
|
+
"sovereign",
|
|
34
|
+
"ai-agent",
|
|
35
|
+
"did"
|
|
36
|
+
],
|
|
37
|
+
"license": "Apache-2.0",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"acorn": "^8.16.0",
|
|
40
|
+
"multicast-dns": "^7.2.5"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/acorn": "^6.0.4",
|
|
44
|
+
"typescript": "^5.0.0",
|
|
45
|
+
"vitest": "^4.1.0"
|
|
46
|
+
}
|
|
47
|
+
}
|