chainlesschain 0.49.0 → 0.66.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/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent-network.js +785 -0
- package/src/commands/automation.js +654 -0
- package/src/commands/dao.js +565 -0
- package/src/commands/did-v2.js +620 -0
- package/src/commands/economy.js +578 -0
- package/src/commands/evolution.js +391 -0
- package/src/commands/hmemory.js +442 -0
- package/src/commands/ipfs.js +392 -0
- package/src/commands/multimodal.js +404 -0
- package/src/commands/perf.js +433 -0
- package/src/commands/pipeline.js +449 -0
- package/src/commands/plugin-ecosystem.js +517 -0
- package/src/commands/sandbox.js +401 -0
- package/src/commands/social.js +311 -0
- package/src/commands/sso.js +798 -0
- package/src/commands/workflow.js +320 -0
- package/src/commands/zkp.js +227 -1
- package/src/index.js +27 -0
- package/src/lib/agent-economy.js +479 -0
- package/src/lib/agent-network.js +1121 -0
- package/src/lib/automation-engine.js +948 -0
- package/src/lib/dao-governance.js +569 -0
- package/src/lib/did-v2-manager.js +1127 -0
- package/src/lib/evolution-system.js +453 -0
- package/src/lib/hierarchical-memory.js +481 -0
- package/src/lib/ipfs-storage.js +575 -0
- package/src/lib/multimodal.js +39 -12
- package/src/lib/perf-tuning.js +734 -0
- package/src/lib/pipeline-orchestrator.js +928 -0
- package/src/lib/plugin-ecosystem.js +1109 -0
- package/src/lib/sandbox-v2.js +306 -0
- package/src/lib/social-graph-analytics.js +707 -0
- package/src/lib/sso-manager.js +841 -0
- package/src/lib/workflow-engine.js +454 -1
- package/src/lib/zkp-engine.js +249 -20
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decentralized Agent Network — CLI port of Phase 24
|
|
3
|
+
* (docs/design/modules/24_去中心化Agent网络.md).
|
|
4
|
+
*
|
|
5
|
+
* Desktop ships 20 IPC handlers across 6 components (AgentDID,
|
|
6
|
+
* AgentReputation, FederatedAgentRegistry, AgentAuthenticator,
|
|
7
|
+
* AgentCredentialManager, CrossOrgTaskRouter) with 7 SQLite tables.
|
|
8
|
+
*
|
|
9
|
+
* CLI port is headless & single-process: no real Kademlia DHT, no
|
|
10
|
+
* libp2p peer sync, no EventEmitter. Kademlia k-buckets are simulated
|
|
11
|
+
* as rows in federated_registry_peers (k-bucket index derived from
|
|
12
|
+
* XOR prefix of SHA-256(peerId)). Challenge-response uses the same
|
|
13
|
+
* Ed25519 key stored against the DID.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import crypto from "crypto";
|
|
17
|
+
|
|
18
|
+
/* ── Constants ───────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
export const DID_STATUS = Object.freeze({
|
|
21
|
+
ACTIVE: "active",
|
|
22
|
+
DEACTIVATED: "deactivated",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const REG_STATUS = Object.freeze({
|
|
26
|
+
ONLINE: "online",
|
|
27
|
+
OFFLINE: "offline",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const AUTH_STATUS = Object.freeze({
|
|
31
|
+
PENDING: "pending",
|
|
32
|
+
ACTIVE: "active",
|
|
33
|
+
EXPIRED: "expired",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const CRED_STATUS = Object.freeze({
|
|
37
|
+
ACTIVE: "active",
|
|
38
|
+
REVOKED: "revoked",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const TASK_STATUS = Object.freeze({
|
|
42
|
+
PENDING: "pending",
|
|
43
|
+
ROUTED: "routed",
|
|
44
|
+
RUNNING: "running",
|
|
45
|
+
COMPLETED: "completed",
|
|
46
|
+
FAILED: "failed",
|
|
47
|
+
CANCELLED: "cancelled",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const REPUTATION_DIMENSIONS = Object.freeze([
|
|
51
|
+
"reliability",
|
|
52
|
+
"quality",
|
|
53
|
+
"speed",
|
|
54
|
+
"cooperation",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
export const REPUTATION_WEIGHTS = Object.freeze({
|
|
58
|
+
reliability: 1.2,
|
|
59
|
+
quality: 1.3,
|
|
60
|
+
speed: 0.8,
|
|
61
|
+
cooperation: 1.0,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const KADEMLIA_BITS = 160;
|
|
65
|
+
export const KADEMLIA_K = 20;
|
|
66
|
+
|
|
67
|
+
export const SESSION_TTL_MS = 15 * 60_000;
|
|
68
|
+
export const HEARTBEAT_TIMEOUT_MS = 5 * 60_000;
|
|
69
|
+
export const MAX_REPUTATION = 5.0;
|
|
70
|
+
export const MIN_REPUTATION = 0.0;
|
|
71
|
+
export const REPUTATION_DECAY_WEEKLY = 0.95;
|
|
72
|
+
|
|
73
|
+
/* ── Schema ──────────────────────────────────────────────── */
|
|
74
|
+
|
|
75
|
+
export function ensureAgentNetworkTables(db) {
|
|
76
|
+
db.exec(`
|
|
77
|
+
CREATE TABLE IF NOT EXISTS agent_dids (
|
|
78
|
+
did TEXT PRIMARY KEY,
|
|
79
|
+
public_key TEXT NOT NULL,
|
|
80
|
+
private_key TEXT NOT NULL,
|
|
81
|
+
did_document TEXT NOT NULL,
|
|
82
|
+
metadata TEXT,
|
|
83
|
+
status TEXT NOT NULL,
|
|
84
|
+
created_at INTEGER NOT NULL,
|
|
85
|
+
updated_at INTEGER NOT NULL
|
|
86
|
+
)
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
db.exec(`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS agent_reputation (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
agent_did TEXT NOT NULL,
|
|
93
|
+
dimension TEXT NOT NULL,
|
|
94
|
+
score REAL NOT NULL,
|
|
95
|
+
evidence TEXT,
|
|
96
|
+
created_at INTEGER NOT NULL
|
|
97
|
+
)
|
|
98
|
+
`);
|
|
99
|
+
|
|
100
|
+
db.exec(`
|
|
101
|
+
CREATE TABLE IF NOT EXISTS federated_agent_registry (
|
|
102
|
+
agent_did TEXT PRIMARY KEY,
|
|
103
|
+
org_id TEXT NOT NULL,
|
|
104
|
+
capabilities TEXT NOT NULL,
|
|
105
|
+
endpoint TEXT,
|
|
106
|
+
status TEXT NOT NULL,
|
|
107
|
+
last_heartbeat INTEGER NOT NULL,
|
|
108
|
+
created_at INTEGER NOT NULL
|
|
109
|
+
)
|
|
110
|
+
`);
|
|
111
|
+
|
|
112
|
+
db.exec(`
|
|
113
|
+
CREATE TABLE IF NOT EXISTS federated_registry_peers (
|
|
114
|
+
peer_id TEXT PRIMARY KEY,
|
|
115
|
+
endpoint TEXT,
|
|
116
|
+
agent_did TEXT,
|
|
117
|
+
k_bucket INTEGER NOT NULL,
|
|
118
|
+
last_seen INTEGER NOT NULL,
|
|
119
|
+
created_at INTEGER NOT NULL
|
|
120
|
+
)
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
db.exec(`
|
|
124
|
+
CREATE TABLE IF NOT EXISTS agent_auth_sessions (
|
|
125
|
+
session_id TEXT PRIMARY KEY,
|
|
126
|
+
agent_did TEXT NOT NULL,
|
|
127
|
+
token TEXT NOT NULL,
|
|
128
|
+
challenge TEXT NOT NULL,
|
|
129
|
+
status TEXT NOT NULL,
|
|
130
|
+
expires_at INTEGER NOT NULL,
|
|
131
|
+
created_at INTEGER NOT NULL
|
|
132
|
+
)
|
|
133
|
+
`);
|
|
134
|
+
|
|
135
|
+
db.exec(`
|
|
136
|
+
CREATE TABLE IF NOT EXISTS agent_credentials (
|
|
137
|
+
id TEXT PRIMARY KEY,
|
|
138
|
+
issuer_did TEXT NOT NULL,
|
|
139
|
+
subject_did TEXT NOT NULL,
|
|
140
|
+
type TEXT NOT NULL,
|
|
141
|
+
claims TEXT NOT NULL,
|
|
142
|
+
proof TEXT NOT NULL,
|
|
143
|
+
status TEXT NOT NULL,
|
|
144
|
+
issued_at INTEGER NOT NULL,
|
|
145
|
+
expires_at INTEGER
|
|
146
|
+
)
|
|
147
|
+
`);
|
|
148
|
+
|
|
149
|
+
db.exec(`
|
|
150
|
+
CREATE TABLE IF NOT EXISTS federated_task_log (
|
|
151
|
+
task_id TEXT PRIMARY KEY,
|
|
152
|
+
source_org TEXT NOT NULL,
|
|
153
|
+
target_org TEXT,
|
|
154
|
+
agent_did TEXT,
|
|
155
|
+
task_type TEXT NOT NULL,
|
|
156
|
+
payload TEXT,
|
|
157
|
+
requirements TEXT,
|
|
158
|
+
status TEXT NOT NULL,
|
|
159
|
+
result TEXT,
|
|
160
|
+
created_at INTEGER NOT NULL,
|
|
161
|
+
completed_at INTEGER
|
|
162
|
+
)
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ── Helpers ─────────────────────────────────────────────── */
|
|
167
|
+
|
|
168
|
+
function _now() {
|
|
169
|
+
return Date.now();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _uuid() {
|
|
173
|
+
return crypto.randomUUID();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function _sha256(buf) {
|
|
177
|
+
return crypto.createHash("sha256").update(buf).digest();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _hex(buf) {
|
|
181
|
+
return Buffer.from(buf).toString("hex");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _json(v) {
|
|
185
|
+
if (v == null) return null;
|
|
186
|
+
return JSON.stringify(v);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function _parse(s, fallback = null) {
|
|
190
|
+
if (s == null || s === "") return fallback;
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(s);
|
|
193
|
+
} catch {
|
|
194
|
+
return fallback;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ── Ed25519 keypair ─────────────────────────────────────── */
|
|
199
|
+
|
|
200
|
+
function _generateEd25519() {
|
|
201
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519", {
|
|
202
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
203
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
publicKey: publicKey.toString("hex"),
|
|
207
|
+
privateKey: privateKey.toString("hex"),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function _signEd25519(privateKeyHex, data) {
|
|
212
|
+
const key = crypto.createPrivateKey({
|
|
213
|
+
key: Buffer.from(privateKeyHex, "hex"),
|
|
214
|
+
format: "der",
|
|
215
|
+
type: "pkcs8",
|
|
216
|
+
});
|
|
217
|
+
return crypto.sign(null, Buffer.from(data), key).toString("hex");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _verifyEd25519(publicKeyHex, data, signatureHex) {
|
|
221
|
+
try {
|
|
222
|
+
const key = crypto.createPublicKey({
|
|
223
|
+
key: Buffer.from(publicKeyHex, "hex"),
|
|
224
|
+
format: "der",
|
|
225
|
+
type: "spki",
|
|
226
|
+
});
|
|
227
|
+
return crypto.verify(
|
|
228
|
+
null,
|
|
229
|
+
Buffer.from(data),
|
|
230
|
+
key,
|
|
231
|
+
Buffer.from(signatureHex, "hex"),
|
|
232
|
+
);
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ── Kademlia helpers ────────────────────────────────────── */
|
|
239
|
+
|
|
240
|
+
function _kBucketFor(peerId, localId = "local") {
|
|
241
|
+
const a = _sha256(Buffer.from(peerId));
|
|
242
|
+
const b = _sha256(Buffer.from(localId));
|
|
243
|
+
for (let i = 0; i < a.length; i++) {
|
|
244
|
+
const xor = a[i] ^ b[i];
|
|
245
|
+
if (xor === 0) continue;
|
|
246
|
+
const prefixZeros = Math.clz32(xor) - 24;
|
|
247
|
+
return i * 8 + prefixZeros;
|
|
248
|
+
}
|
|
249
|
+
return KADEMLIA_BITS - 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ── DID management ──────────────────────────────────────── */
|
|
253
|
+
|
|
254
|
+
export function createAgentDID(db, { displayName, metadata = {} } = {}) {
|
|
255
|
+
const { publicKey, privateKey } = _generateEd25519();
|
|
256
|
+
const idHash = _sha256(Buffer.from(publicKey, "hex"))
|
|
257
|
+
.toString("base64url")
|
|
258
|
+
.slice(0, 32);
|
|
259
|
+
const did = `did:chainless:${idHash}`;
|
|
260
|
+
const now = _now();
|
|
261
|
+
|
|
262
|
+
const didDocument = {
|
|
263
|
+
"@context": ["https://www.w3.org/ns/did/v1"],
|
|
264
|
+
id: did,
|
|
265
|
+
controller: did,
|
|
266
|
+
verificationMethod: [
|
|
267
|
+
{
|
|
268
|
+
id: `${did}#key-1`,
|
|
269
|
+
type: "Ed25519VerificationKey2020",
|
|
270
|
+
controller: did,
|
|
271
|
+
publicKeyHex: publicKey,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
authentication: [`${did}#key-1`],
|
|
275
|
+
assertionMethod: [`${did}#key-1`],
|
|
276
|
+
service: displayName
|
|
277
|
+
? [
|
|
278
|
+
{
|
|
279
|
+
id: `${did}#agent-service`,
|
|
280
|
+
type: "AgentService",
|
|
281
|
+
serviceEndpoint: { name: displayName },
|
|
282
|
+
},
|
|
283
|
+
]
|
|
284
|
+
: [],
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
db.prepare(
|
|
288
|
+
`INSERT INTO agent_dids
|
|
289
|
+
(did, public_key, private_key, did_document, metadata, status, created_at, updated_at)
|
|
290
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
291
|
+
).run(
|
|
292
|
+
did,
|
|
293
|
+
publicKey,
|
|
294
|
+
privateKey,
|
|
295
|
+
JSON.stringify(didDocument),
|
|
296
|
+
_json({ displayName, ...metadata }),
|
|
297
|
+
DID_STATUS.ACTIVE,
|
|
298
|
+
now,
|
|
299
|
+
now,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return { did, publicKey, didDocument };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function resolveAgentDID(db, did) {
|
|
306
|
+
const row = db.prepare(`SELECT * FROM agent_dids WHERE did = ?`).get(did);
|
|
307
|
+
if (!row) return null;
|
|
308
|
+
return {
|
|
309
|
+
did: row.did,
|
|
310
|
+
publicKey: row.public_key,
|
|
311
|
+
status: row.status,
|
|
312
|
+
didDocument: _parse(row.did_document),
|
|
313
|
+
metadata: _parse(row.metadata, {}),
|
|
314
|
+
createdAt: row.created_at,
|
|
315
|
+
updatedAt: row.updated_at,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function listAgentDIDs(db, { status, limit = 100 } = {}) {
|
|
320
|
+
const params = [];
|
|
321
|
+
let sql = `SELECT * FROM agent_dids`;
|
|
322
|
+
if (status) {
|
|
323
|
+
sql += ` WHERE status = ?`;
|
|
324
|
+
params.push(status);
|
|
325
|
+
}
|
|
326
|
+
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
327
|
+
params.push(limit);
|
|
328
|
+
const rows = db.prepare(sql).all(...params);
|
|
329
|
+
return rows.map((r) => ({
|
|
330
|
+
did: r.did,
|
|
331
|
+
publicKey: r.public_key,
|
|
332
|
+
status: r.status,
|
|
333
|
+
metadata: _parse(r.metadata, {}),
|
|
334
|
+
createdAt: r.created_at,
|
|
335
|
+
updatedAt: r.updated_at,
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function deactivateAgentDID(db, did) {
|
|
340
|
+
const row = db.prepare(`SELECT did FROM agent_dids WHERE did = ?`).get(did);
|
|
341
|
+
if (!row) return { changed: false };
|
|
342
|
+
const now = _now();
|
|
343
|
+
db.prepare(
|
|
344
|
+
`UPDATE agent_dids SET status = ?, updated_at = ? WHERE did = ?`,
|
|
345
|
+
).run(DID_STATUS.DEACTIVATED, now, did);
|
|
346
|
+
db.prepare(
|
|
347
|
+
`UPDATE federated_agent_registry SET status = ? WHERE agent_did = ?`,
|
|
348
|
+
).run(REG_STATUS.OFFLINE, did);
|
|
349
|
+
return { changed: true, did, deactivatedAt: now };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function signWithAgent(db, did, data) {
|
|
353
|
+
const row = db
|
|
354
|
+
.prepare(`SELECT private_key, status FROM agent_dids WHERE did = ?`)
|
|
355
|
+
.get(did);
|
|
356
|
+
if (!row) throw new Error(`Unknown agent DID: ${did}`);
|
|
357
|
+
if (row.status !== DID_STATUS.ACTIVE)
|
|
358
|
+
throw new Error(`Agent DID is not active: ${did}`);
|
|
359
|
+
return _signEd25519(row.private_key, data);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function verifyWithAgent(db, did, data, signature) {
|
|
363
|
+
const row = db
|
|
364
|
+
.prepare(`SELECT public_key FROM agent_dids WHERE did = ?`)
|
|
365
|
+
.get(did);
|
|
366
|
+
if (!row) return false;
|
|
367
|
+
return _verifyEd25519(row.public_key, data, signature);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* ── Federated registry ──────────────────────────────────── */
|
|
371
|
+
|
|
372
|
+
export function registerAgent(
|
|
373
|
+
db,
|
|
374
|
+
{ did, orgId, capabilities = [], endpoint = null } = {},
|
|
375
|
+
) {
|
|
376
|
+
if (!did) throw new Error("did is required");
|
|
377
|
+
if (!orgId) throw new Error("orgId is required");
|
|
378
|
+
const agent = resolveAgentDID(db, did);
|
|
379
|
+
if (!agent) throw new Error(`Unknown agent DID: ${did}`);
|
|
380
|
+
if (agent.status !== DID_STATUS.ACTIVE)
|
|
381
|
+
throw new Error(`Agent DID not active: ${did}`);
|
|
382
|
+
const now = _now();
|
|
383
|
+
const existing = db
|
|
384
|
+
.prepare(
|
|
385
|
+
`SELECT agent_did FROM federated_agent_registry WHERE agent_did = ?`,
|
|
386
|
+
)
|
|
387
|
+
.get(did);
|
|
388
|
+
if (existing) {
|
|
389
|
+
db.prepare(
|
|
390
|
+
`UPDATE federated_agent_registry
|
|
391
|
+
SET org_id = ?, capabilities = ?, endpoint = ?, status = ?, last_heartbeat = ?
|
|
392
|
+
WHERE agent_did = ?`,
|
|
393
|
+
).run(
|
|
394
|
+
orgId,
|
|
395
|
+
JSON.stringify(capabilities),
|
|
396
|
+
endpoint,
|
|
397
|
+
REG_STATUS.ONLINE,
|
|
398
|
+
now,
|
|
399
|
+
did,
|
|
400
|
+
);
|
|
401
|
+
} else {
|
|
402
|
+
db.prepare(
|
|
403
|
+
`INSERT INTO federated_agent_registry
|
|
404
|
+
(agent_did, org_id, capabilities, endpoint, status, last_heartbeat, created_at)
|
|
405
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
406
|
+
).run(
|
|
407
|
+
did,
|
|
408
|
+
orgId,
|
|
409
|
+
JSON.stringify(capabilities),
|
|
410
|
+
endpoint,
|
|
411
|
+
REG_STATUS.ONLINE,
|
|
412
|
+
now,
|
|
413
|
+
now,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return { did, orgId, capabilities, endpoint, registeredAt: now };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function unregisterAgent(db, did) {
|
|
420
|
+
const row = db
|
|
421
|
+
.prepare(
|
|
422
|
+
`SELECT agent_did FROM federated_agent_registry WHERE agent_did = ?`,
|
|
423
|
+
)
|
|
424
|
+
.get(did);
|
|
425
|
+
if (!row) return { changed: false };
|
|
426
|
+
db.prepare(`DELETE FROM federated_agent_registry WHERE agent_did = ?`).run(
|
|
427
|
+
did,
|
|
428
|
+
);
|
|
429
|
+
return { changed: true, did };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function heartbeatAgent(db, did) {
|
|
433
|
+
const row = db
|
|
434
|
+
.prepare(
|
|
435
|
+
`SELECT agent_did FROM federated_agent_registry WHERE agent_did = ?`,
|
|
436
|
+
)
|
|
437
|
+
.get(did);
|
|
438
|
+
if (!row) return { changed: false };
|
|
439
|
+
const now = _now();
|
|
440
|
+
db.prepare(
|
|
441
|
+
`UPDATE federated_agent_registry SET last_heartbeat = ?, status = ? WHERE agent_did = ?`,
|
|
442
|
+
).run(now, REG_STATUS.ONLINE, did);
|
|
443
|
+
return { changed: true, did, heartbeatAt: now };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function discoverAgents(
|
|
447
|
+
db,
|
|
448
|
+
{ capability, orgId, status = REG_STATUS.ONLINE, limit = 50 } = {},
|
|
449
|
+
) {
|
|
450
|
+
const params = [];
|
|
451
|
+
let sql = `SELECT * FROM federated_agent_registry`;
|
|
452
|
+
const wheres = [];
|
|
453
|
+
if (status) {
|
|
454
|
+
wheres.push(`status = ?`);
|
|
455
|
+
params.push(status);
|
|
456
|
+
}
|
|
457
|
+
if (orgId) {
|
|
458
|
+
wheres.push(`org_id = ?`);
|
|
459
|
+
params.push(orgId);
|
|
460
|
+
}
|
|
461
|
+
if (wheres.length > 0) sql += ` WHERE ` + wheres.join(` AND `);
|
|
462
|
+
sql += ` ORDER BY last_heartbeat DESC LIMIT ?`;
|
|
463
|
+
params.push(limit);
|
|
464
|
+
const rows = db.prepare(sql).all(...params);
|
|
465
|
+
let out = rows.map((r) => ({
|
|
466
|
+
agentDid: r.agent_did,
|
|
467
|
+
orgId: r.org_id,
|
|
468
|
+
capabilities: _parse(r.capabilities, []),
|
|
469
|
+
endpoint: r.endpoint,
|
|
470
|
+
status: r.status,
|
|
471
|
+
lastHeartbeat: r.last_heartbeat,
|
|
472
|
+
}));
|
|
473
|
+
if (capability) {
|
|
474
|
+
out = out.filter((a) => a.capabilities.includes(capability));
|
|
475
|
+
}
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function sweepStaleAgents(db, now = _now()) {
|
|
480
|
+
const cutoff = now - HEARTBEAT_TIMEOUT_MS;
|
|
481
|
+
const rows = db
|
|
482
|
+
.prepare(
|
|
483
|
+
`SELECT agent_did FROM federated_agent_registry WHERE status = ? AND last_heartbeat < ?`,
|
|
484
|
+
)
|
|
485
|
+
.all(REG_STATUS.ONLINE, cutoff);
|
|
486
|
+
let swept = 0;
|
|
487
|
+
for (const r of rows) {
|
|
488
|
+
db.prepare(
|
|
489
|
+
`UPDATE federated_agent_registry SET status = ? WHERE agent_did = ?`,
|
|
490
|
+
).run(REG_STATUS.OFFLINE, r.agent_did);
|
|
491
|
+
swept++;
|
|
492
|
+
}
|
|
493
|
+
return { swept };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* ── Kademlia peer bookkeeping ───────────────────────────── */
|
|
497
|
+
|
|
498
|
+
export function addPeer(db, { peerId, endpoint = null, agentDid = null } = {}) {
|
|
499
|
+
if (!peerId) throw new Error("peerId is required");
|
|
500
|
+
const now = _now();
|
|
501
|
+
const kb = _kBucketFor(peerId);
|
|
502
|
+
const existing = db
|
|
503
|
+
.prepare(`SELECT peer_id FROM federated_registry_peers WHERE peer_id = ?`)
|
|
504
|
+
.get(peerId);
|
|
505
|
+
if (existing) {
|
|
506
|
+
db.prepare(
|
|
507
|
+
`UPDATE federated_registry_peers
|
|
508
|
+
SET endpoint = ?, agent_did = ?, k_bucket = ?, last_seen = ?
|
|
509
|
+
WHERE peer_id = ?`,
|
|
510
|
+
).run(endpoint, agentDid, kb, now, peerId);
|
|
511
|
+
} else {
|
|
512
|
+
db.prepare(
|
|
513
|
+
`INSERT INTO federated_registry_peers
|
|
514
|
+
(peer_id, endpoint, agent_did, k_bucket, last_seen, created_at)
|
|
515
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
516
|
+
).run(peerId, endpoint, agentDid, kb, now, now);
|
|
517
|
+
}
|
|
518
|
+
return { peerId, kBucket: kb, lastSeen: now };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function removePeer(db, peerId) {
|
|
522
|
+
const row = db
|
|
523
|
+
.prepare(`SELECT peer_id FROM federated_registry_peers WHERE peer_id = ?`)
|
|
524
|
+
.get(peerId);
|
|
525
|
+
if (!row) return { changed: false };
|
|
526
|
+
db.prepare(`DELETE FROM federated_registry_peers WHERE peer_id = ?`).run(
|
|
527
|
+
peerId,
|
|
528
|
+
);
|
|
529
|
+
return { changed: true };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function listPeers(db, { kBucket, limit = 100 } = {}) {
|
|
533
|
+
const params = [];
|
|
534
|
+
let sql = `SELECT * FROM federated_registry_peers`;
|
|
535
|
+
if (kBucket != null) {
|
|
536
|
+
sql += ` WHERE k_bucket = ?`;
|
|
537
|
+
params.push(kBucket);
|
|
538
|
+
}
|
|
539
|
+
sql += ` ORDER BY last_seen DESC LIMIT ?`;
|
|
540
|
+
params.push(limit);
|
|
541
|
+
const rows = db.prepare(sql).all(...params);
|
|
542
|
+
return rows.map((r) => ({
|
|
543
|
+
peerId: r.peer_id,
|
|
544
|
+
endpoint: r.endpoint,
|
|
545
|
+
agentDid: r.agent_did,
|
|
546
|
+
kBucket: r.k_bucket,
|
|
547
|
+
lastSeen: r.last_seen,
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* ── Authentication (challenge-response) ─────────────────── */
|
|
552
|
+
|
|
553
|
+
export function startAuth(db, did) {
|
|
554
|
+
const agent = resolveAgentDID(db, did);
|
|
555
|
+
if (!agent) throw new Error(`Unknown agent DID: ${did}`);
|
|
556
|
+
if (agent.status !== DID_STATUS.ACTIVE)
|
|
557
|
+
throw new Error(`Agent DID not active: ${did}`);
|
|
558
|
+
const sessionId = _uuid();
|
|
559
|
+
const challenge = _hex(crypto.randomBytes(32));
|
|
560
|
+
const token = _hex(crypto.randomBytes(24));
|
|
561
|
+
const now = _now();
|
|
562
|
+
const expires = now + SESSION_TTL_MS;
|
|
563
|
+
db.prepare(
|
|
564
|
+
`INSERT INTO agent_auth_sessions
|
|
565
|
+
(session_id, agent_did, token, challenge, status, expires_at, created_at)
|
|
566
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
567
|
+
).run(sessionId, did, token, challenge, AUTH_STATUS.PENDING, expires, now);
|
|
568
|
+
return { sessionId, challenge, expiresAt: expires };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function completeAuth(db, sessionId, signatureHex) {
|
|
572
|
+
const row = db
|
|
573
|
+
.prepare(`SELECT * FROM agent_auth_sessions WHERE session_id = ?`)
|
|
574
|
+
.get(sessionId);
|
|
575
|
+
if (!row) throw new Error(`Unknown auth session: ${sessionId}`);
|
|
576
|
+
const now = _now();
|
|
577
|
+
if (row.status !== AUTH_STATUS.PENDING)
|
|
578
|
+
throw new Error(`Session not in PENDING state: ${row.status}`);
|
|
579
|
+
if (now > row.expires_at) {
|
|
580
|
+
db.prepare(
|
|
581
|
+
`UPDATE agent_auth_sessions SET status = ? WHERE session_id = ?`,
|
|
582
|
+
).run(AUTH_STATUS.EXPIRED, sessionId);
|
|
583
|
+
throw new Error(`Session expired`);
|
|
584
|
+
}
|
|
585
|
+
const ok = verifyWithAgent(db, row.agent_did, row.challenge, signatureHex);
|
|
586
|
+
if (!ok) throw new Error(`Signature verification failed`);
|
|
587
|
+
db.prepare(
|
|
588
|
+
`UPDATE agent_auth_sessions SET status = ? WHERE session_id = ?`,
|
|
589
|
+
).run(AUTH_STATUS.ACTIVE, sessionId);
|
|
590
|
+
return {
|
|
591
|
+
sessionId,
|
|
592
|
+
token: row.token,
|
|
593
|
+
agentDid: row.agent_did,
|
|
594
|
+
expiresAt: row.expires_at,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function validateSession(db, token) {
|
|
599
|
+
const row = db
|
|
600
|
+
.prepare(`SELECT * FROM agent_auth_sessions WHERE token = ?`)
|
|
601
|
+
.get(token);
|
|
602
|
+
if (!row) return null;
|
|
603
|
+
const now = _now();
|
|
604
|
+
if (row.status !== AUTH_STATUS.ACTIVE) return null;
|
|
605
|
+
if (now > row.expires_at) {
|
|
606
|
+
db.prepare(
|
|
607
|
+
`UPDATE agent_auth_sessions SET status = ? WHERE session_id = ?`,
|
|
608
|
+
).run(AUTH_STATUS.EXPIRED, row.session_id);
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
sessionId: row.session_id,
|
|
613
|
+
agentDid: row.agent_did,
|
|
614
|
+
expiresAt: row.expires_at,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function listSessions(db, { agentDid, status, limit = 50 } = {}) {
|
|
619
|
+
const params = [];
|
|
620
|
+
let sql = `SELECT * FROM agent_auth_sessions`;
|
|
621
|
+
const wheres = [];
|
|
622
|
+
if (agentDid) {
|
|
623
|
+
wheres.push(`agent_did = ?`);
|
|
624
|
+
params.push(agentDid);
|
|
625
|
+
}
|
|
626
|
+
if (status) {
|
|
627
|
+
wheres.push(`status = ?`);
|
|
628
|
+
params.push(status);
|
|
629
|
+
}
|
|
630
|
+
if (wheres.length) sql += ` WHERE ` + wheres.join(` AND `);
|
|
631
|
+
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
632
|
+
params.push(limit);
|
|
633
|
+
const rows = db.prepare(sql).all(...params);
|
|
634
|
+
return rows.map((r) => ({
|
|
635
|
+
sessionId: r.session_id,
|
|
636
|
+
agentDid: r.agent_did,
|
|
637
|
+
status: r.status,
|
|
638
|
+
expiresAt: r.expires_at,
|
|
639
|
+
createdAt: r.created_at,
|
|
640
|
+
}));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* ── Credential management (W3C VC) ──────────────────────── */
|
|
644
|
+
|
|
645
|
+
function _credentialProof(issuerPrivateKey, payload) {
|
|
646
|
+
return _signEd25519(issuerPrivateKey, JSON.stringify(payload));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function issueCredential(
|
|
650
|
+
db,
|
|
651
|
+
{
|
|
652
|
+
issuerDid,
|
|
653
|
+
subjectDid,
|
|
654
|
+
type = "AgentCapabilityCredential",
|
|
655
|
+
claims = {},
|
|
656
|
+
expiresAt = null,
|
|
657
|
+
} = {},
|
|
658
|
+
) {
|
|
659
|
+
if (!issuerDid) throw new Error("issuerDid is required");
|
|
660
|
+
if (!subjectDid) throw new Error("subjectDid is required");
|
|
661
|
+
const issuer = db
|
|
662
|
+
.prepare(`SELECT private_key, status FROM agent_dids WHERE did = ?`)
|
|
663
|
+
.get(issuerDid);
|
|
664
|
+
if (!issuer) throw new Error(`Unknown issuer DID: ${issuerDid}`);
|
|
665
|
+
if (issuer.status !== DID_STATUS.ACTIVE)
|
|
666
|
+
throw new Error(`Issuer DID not active: ${issuerDid}`);
|
|
667
|
+
const subject = resolveAgentDID(db, subjectDid);
|
|
668
|
+
if (!subject) throw new Error(`Unknown subject DID: ${subjectDid}`);
|
|
669
|
+
const now = _now();
|
|
670
|
+
const id = `vc:${_uuid()}`;
|
|
671
|
+
const payload = {
|
|
672
|
+
id,
|
|
673
|
+
type,
|
|
674
|
+
issuer: issuerDid,
|
|
675
|
+
subject: subjectDid,
|
|
676
|
+
claims,
|
|
677
|
+
issuedAt: now,
|
|
678
|
+
expiresAt,
|
|
679
|
+
};
|
|
680
|
+
const proof = _credentialProof(issuer.private_key, payload);
|
|
681
|
+
db.prepare(
|
|
682
|
+
`INSERT INTO agent_credentials
|
|
683
|
+
(id, issuer_did, subject_did, type, claims, proof, status, issued_at, expires_at)
|
|
684
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
685
|
+
).run(
|
|
686
|
+
id,
|
|
687
|
+
issuerDid,
|
|
688
|
+
subjectDid,
|
|
689
|
+
type,
|
|
690
|
+
JSON.stringify(claims),
|
|
691
|
+
proof,
|
|
692
|
+
CRED_STATUS.ACTIVE,
|
|
693
|
+
now,
|
|
694
|
+
expiresAt,
|
|
695
|
+
);
|
|
696
|
+
return { ...payload, proof, status: CRED_STATUS.ACTIVE };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function getCredential(db, id) {
|
|
700
|
+
const row = db
|
|
701
|
+
.prepare(`SELECT * FROM agent_credentials WHERE id = ?`)
|
|
702
|
+
.get(id);
|
|
703
|
+
if (!row) return null;
|
|
704
|
+
return {
|
|
705
|
+
id: row.id,
|
|
706
|
+
issuer: row.issuer_did,
|
|
707
|
+
subject: row.subject_did,
|
|
708
|
+
type: row.type,
|
|
709
|
+
claims: _parse(row.claims, {}),
|
|
710
|
+
proof: row.proof,
|
|
711
|
+
status: row.status,
|
|
712
|
+
issuedAt: row.issued_at,
|
|
713
|
+
expiresAt: row.expires_at,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function verifyCredential(db, id) {
|
|
718
|
+
const c = getCredential(db, id);
|
|
719
|
+
if (!c) return { valid: false, reason: "not-found" };
|
|
720
|
+
if (c.status === CRED_STATUS.REVOKED)
|
|
721
|
+
return { valid: false, reason: "revoked" };
|
|
722
|
+
const now = _now();
|
|
723
|
+
if (c.expiresAt && now > c.expiresAt)
|
|
724
|
+
return { valid: false, reason: "expired" };
|
|
725
|
+
const payload = {
|
|
726
|
+
id: c.id,
|
|
727
|
+
type: c.type,
|
|
728
|
+
issuer: c.issuer,
|
|
729
|
+
subject: c.subject,
|
|
730
|
+
claims: c.claims,
|
|
731
|
+
issuedAt: c.issuedAt,
|
|
732
|
+
expiresAt: c.expiresAt,
|
|
733
|
+
};
|
|
734
|
+
const issuer = db
|
|
735
|
+
.prepare(`SELECT public_key FROM agent_dids WHERE did = ?`)
|
|
736
|
+
.get(c.issuer);
|
|
737
|
+
if (!issuer) return { valid: false, reason: "issuer-unknown" };
|
|
738
|
+
const ok = _verifyEd25519(
|
|
739
|
+
issuer.public_key,
|
|
740
|
+
JSON.stringify(payload),
|
|
741
|
+
c.proof,
|
|
742
|
+
);
|
|
743
|
+
return ok
|
|
744
|
+
? { valid: true, credential: c }
|
|
745
|
+
: { valid: false, reason: "signature-invalid" };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export function revokeCredential(db, id) {
|
|
749
|
+
const row = db
|
|
750
|
+
.prepare(`SELECT status FROM agent_credentials WHERE id = ?`)
|
|
751
|
+
.get(id);
|
|
752
|
+
if (!row) return { changed: false };
|
|
753
|
+
if (row.status === CRED_STATUS.REVOKED) return { changed: false };
|
|
754
|
+
db.prepare(`UPDATE agent_credentials SET status = ? WHERE id = ?`).run(
|
|
755
|
+
CRED_STATUS.REVOKED,
|
|
756
|
+
id,
|
|
757
|
+
);
|
|
758
|
+
return { changed: true };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export function listCredentials(
|
|
762
|
+
db,
|
|
763
|
+
{ subjectDid, issuerDid, status, type, limit = 50 } = {},
|
|
764
|
+
) {
|
|
765
|
+
const params = [];
|
|
766
|
+
let sql = `SELECT * FROM agent_credentials`;
|
|
767
|
+
const wheres = [];
|
|
768
|
+
if (subjectDid) {
|
|
769
|
+
wheres.push(`subject_did = ?`);
|
|
770
|
+
params.push(subjectDid);
|
|
771
|
+
}
|
|
772
|
+
if (issuerDid) {
|
|
773
|
+
wheres.push(`issuer_did = ?`);
|
|
774
|
+
params.push(issuerDid);
|
|
775
|
+
}
|
|
776
|
+
if (status) {
|
|
777
|
+
wheres.push(`status = ?`);
|
|
778
|
+
params.push(status);
|
|
779
|
+
}
|
|
780
|
+
if (type) {
|
|
781
|
+
wheres.push(`type = ?`);
|
|
782
|
+
params.push(type);
|
|
783
|
+
}
|
|
784
|
+
if (wheres.length) sql += ` WHERE ` + wheres.join(` AND `);
|
|
785
|
+
sql += ` ORDER BY issued_at DESC LIMIT ?`;
|
|
786
|
+
params.push(limit);
|
|
787
|
+
const rows = db.prepare(sql).all(...params);
|
|
788
|
+
return rows.map((r) => ({
|
|
789
|
+
id: r.id,
|
|
790
|
+
issuer: r.issuer_did,
|
|
791
|
+
subject: r.subject_did,
|
|
792
|
+
type: r.type,
|
|
793
|
+
claims: _parse(r.claims, {}),
|
|
794
|
+
status: r.status,
|
|
795
|
+
issuedAt: r.issued_at,
|
|
796
|
+
expiresAt: r.expires_at,
|
|
797
|
+
}));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/* ── Cross-org task routing ──────────────────────────────── */
|
|
801
|
+
|
|
802
|
+
function _scoreAgent(agent, reputation) {
|
|
803
|
+
const base = reputation ? reputation.total : 2.5;
|
|
804
|
+
const load = 0; // CLI has no live task load tracker — future work
|
|
805
|
+
return base - load;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export function routeTask(
|
|
809
|
+
db,
|
|
810
|
+
{
|
|
811
|
+
sourceOrg,
|
|
812
|
+
targetOrg = null,
|
|
813
|
+
taskType,
|
|
814
|
+
requirements = {},
|
|
815
|
+
payload = null,
|
|
816
|
+
} = {},
|
|
817
|
+
) {
|
|
818
|
+
if (!sourceOrg) throw new Error("sourceOrg is required");
|
|
819
|
+
if (!taskType) throw new Error("taskType is required");
|
|
820
|
+
const now = _now();
|
|
821
|
+
const taskId = `task:${_uuid()}`;
|
|
822
|
+
const candidates = discoverAgents(db, {
|
|
823
|
+
capability: requirements.capability,
|
|
824
|
+
orgId: targetOrg,
|
|
825
|
+
status: REG_STATUS.ONLINE,
|
|
826
|
+
limit: 100,
|
|
827
|
+
});
|
|
828
|
+
if (candidates.length === 0) {
|
|
829
|
+
db.prepare(
|
|
830
|
+
`INSERT INTO federated_task_log
|
|
831
|
+
(task_id, source_org, target_org, agent_did, task_type, payload, requirements, status, result, created_at, completed_at)
|
|
832
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
833
|
+
).run(
|
|
834
|
+
taskId,
|
|
835
|
+
sourceOrg,
|
|
836
|
+
targetOrg,
|
|
837
|
+
null,
|
|
838
|
+
taskType,
|
|
839
|
+
_json(payload),
|
|
840
|
+
_json(requirements),
|
|
841
|
+
TASK_STATUS.PENDING,
|
|
842
|
+
null,
|
|
843
|
+
now,
|
|
844
|
+
null,
|
|
845
|
+
);
|
|
846
|
+
return { taskId, status: TASK_STATUS.PENDING, agentDid: null };
|
|
847
|
+
}
|
|
848
|
+
const scored = candidates
|
|
849
|
+
.map((c) => ({
|
|
850
|
+
...c,
|
|
851
|
+
reputation: getReputation(db, c.agentDid),
|
|
852
|
+
}))
|
|
853
|
+
.map((c) => ({ ...c, _score: _scoreAgent(c, c.reputation) }))
|
|
854
|
+
.sort((a, b) => b._score - a._score);
|
|
855
|
+
const picked = scored[0];
|
|
856
|
+
db.prepare(
|
|
857
|
+
`INSERT INTO federated_task_log
|
|
858
|
+
(task_id, source_org, target_org, agent_did, task_type, payload, requirements, status, result, created_at, completed_at)
|
|
859
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
860
|
+
).run(
|
|
861
|
+
taskId,
|
|
862
|
+
sourceOrg,
|
|
863
|
+
targetOrg || picked.orgId,
|
|
864
|
+
picked.agentDid,
|
|
865
|
+
taskType,
|
|
866
|
+
_json(payload),
|
|
867
|
+
_json(requirements),
|
|
868
|
+
TASK_STATUS.ROUTED,
|
|
869
|
+
null,
|
|
870
|
+
now,
|
|
871
|
+
null,
|
|
872
|
+
);
|
|
873
|
+
return {
|
|
874
|
+
taskId,
|
|
875
|
+
status: TASK_STATUS.ROUTED,
|
|
876
|
+
agentDid: picked.agentDid,
|
|
877
|
+
orgId: picked.orgId,
|
|
878
|
+
score: picked._score,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function getTask(db, taskId) {
|
|
883
|
+
const row = db
|
|
884
|
+
.prepare(`SELECT * FROM federated_task_log WHERE task_id = ?`)
|
|
885
|
+
.get(taskId);
|
|
886
|
+
if (!row) return null;
|
|
887
|
+
return {
|
|
888
|
+
taskId: row.task_id,
|
|
889
|
+
sourceOrg: row.source_org,
|
|
890
|
+
targetOrg: row.target_org,
|
|
891
|
+
agentDid: row.agent_did,
|
|
892
|
+
taskType: row.task_type,
|
|
893
|
+
payload: _parse(row.payload),
|
|
894
|
+
requirements: _parse(row.requirements, {}),
|
|
895
|
+
status: row.status,
|
|
896
|
+
result: _parse(row.result),
|
|
897
|
+
createdAt: row.created_at,
|
|
898
|
+
completedAt: row.completed_at,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export function updateTaskStatus(db, taskId, status, result = null) {
|
|
903
|
+
const row = db
|
|
904
|
+
.prepare(`SELECT status FROM federated_task_log WHERE task_id = ?`)
|
|
905
|
+
.get(taskId);
|
|
906
|
+
if (!row) return { changed: false };
|
|
907
|
+
const now = _now();
|
|
908
|
+
const terminal =
|
|
909
|
+
status === TASK_STATUS.COMPLETED ||
|
|
910
|
+
status === TASK_STATUS.FAILED ||
|
|
911
|
+
status === TASK_STATUS.CANCELLED;
|
|
912
|
+
db.prepare(
|
|
913
|
+
`UPDATE federated_task_log
|
|
914
|
+
SET status = ?, result = ?, completed_at = ?
|
|
915
|
+
WHERE task_id = ?`,
|
|
916
|
+
).run(status, _json(result), terminal ? now : null, taskId);
|
|
917
|
+
return { changed: true, taskId, status };
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
export function cancelTask(db, taskId, reason = null) {
|
|
921
|
+
return updateTaskStatus(db, taskId, TASK_STATUS.CANCELLED, {
|
|
922
|
+
reason: reason || "cancelled",
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export function listTasks(db, { orgId, agentDid, status, limit = 50 } = {}) {
|
|
927
|
+
const params = [];
|
|
928
|
+
let sql = `SELECT * FROM federated_task_log`;
|
|
929
|
+
const wheres = [];
|
|
930
|
+
if (orgId) {
|
|
931
|
+
wheres.push(`source_org = ?`);
|
|
932
|
+
params.push(orgId);
|
|
933
|
+
}
|
|
934
|
+
if (agentDid) {
|
|
935
|
+
wheres.push(`agent_did = ?`);
|
|
936
|
+
params.push(agentDid);
|
|
937
|
+
}
|
|
938
|
+
if (status) {
|
|
939
|
+
wheres.push(`status = ?`);
|
|
940
|
+
params.push(status);
|
|
941
|
+
}
|
|
942
|
+
if (wheres.length) sql += ` WHERE ` + wheres.join(` AND `);
|
|
943
|
+
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
944
|
+
params.push(limit);
|
|
945
|
+
const rows = db.prepare(sql).all(...params);
|
|
946
|
+
return rows.map((r) => ({
|
|
947
|
+
taskId: r.task_id,
|
|
948
|
+
sourceOrg: r.source_org,
|
|
949
|
+
targetOrg: r.target_org,
|
|
950
|
+
agentDid: r.agent_did,
|
|
951
|
+
taskType: r.task_type,
|
|
952
|
+
status: r.status,
|
|
953
|
+
createdAt: r.created_at,
|
|
954
|
+
completedAt: r.completed_at,
|
|
955
|
+
}));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/* ── Reputation ──────────────────────────────────────────── */
|
|
959
|
+
|
|
960
|
+
export function updateReputation(
|
|
961
|
+
db,
|
|
962
|
+
{ agentDid, dimension, score, evidence = null } = {},
|
|
963
|
+
) {
|
|
964
|
+
if (!agentDid) throw new Error("agentDid is required");
|
|
965
|
+
if (!REPUTATION_DIMENSIONS.includes(dimension))
|
|
966
|
+
throw new Error(
|
|
967
|
+
`Invalid dimension: ${dimension}. Expected one of ${REPUTATION_DIMENSIONS.join(",")}`,
|
|
968
|
+
);
|
|
969
|
+
const s = Math.max(MIN_REPUTATION, Math.min(MAX_REPUTATION, Number(score)));
|
|
970
|
+
const id = `rep:${_uuid()}`;
|
|
971
|
+
const now = _now();
|
|
972
|
+
db.prepare(
|
|
973
|
+
`INSERT INTO agent_reputation
|
|
974
|
+
(id, agent_did, dimension, score, evidence, created_at)
|
|
975
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
976
|
+
).run(id, agentDid, dimension, s, evidence, now);
|
|
977
|
+
return { id, agentDid, dimension, score: s, createdAt: now };
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
export function getReputation(db, agentDid) {
|
|
981
|
+
const rows = db
|
|
982
|
+
.prepare(
|
|
983
|
+
`SELECT dimension, score, created_at FROM agent_reputation WHERE agent_did = ?`,
|
|
984
|
+
)
|
|
985
|
+
.all(agentDid);
|
|
986
|
+
if (rows.length === 0) {
|
|
987
|
+
return {
|
|
988
|
+
agentDid,
|
|
989
|
+
dimensions: {},
|
|
990
|
+
total: 2.5,
|
|
991
|
+
samples: 0,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
const dims = {};
|
|
995
|
+
for (const r of rows) {
|
|
996
|
+
if (!dims[r.dimension]) dims[r.dimension] = { sum: 0, n: 0, latest: 0 };
|
|
997
|
+
dims[r.dimension].sum += r.score;
|
|
998
|
+
dims[r.dimension].n += 1;
|
|
999
|
+
if (r.created_at > dims[r.dimension].latest)
|
|
1000
|
+
dims[r.dimension].latest = r.created_at;
|
|
1001
|
+
}
|
|
1002
|
+
const dimensions = {};
|
|
1003
|
+
let weightedSum = 0;
|
|
1004
|
+
let weightTotal = 0;
|
|
1005
|
+
for (const d of REPUTATION_DIMENSIONS) {
|
|
1006
|
+
if (dims[d]) {
|
|
1007
|
+
const avg = dims[d].sum / dims[d].n;
|
|
1008
|
+
dimensions[d] = {
|
|
1009
|
+
score: avg,
|
|
1010
|
+
samples: dims[d].n,
|
|
1011
|
+
latest: dims[d].latest,
|
|
1012
|
+
};
|
|
1013
|
+
const w = REPUTATION_WEIGHTS[d] || 1;
|
|
1014
|
+
weightedSum += avg * w;
|
|
1015
|
+
weightTotal += w;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const total = weightTotal > 0 ? weightedSum / weightTotal : 2.5;
|
|
1019
|
+
return {
|
|
1020
|
+
agentDid,
|
|
1021
|
+
dimensions,
|
|
1022
|
+
total,
|
|
1023
|
+
samples: rows.length,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export function getReputationHistory(db, agentDid, limit = 50) {
|
|
1028
|
+
const rows = db
|
|
1029
|
+
.prepare(
|
|
1030
|
+
`SELECT * FROM agent_reputation WHERE agent_did = ? ORDER BY created_at DESC LIMIT ?`,
|
|
1031
|
+
)
|
|
1032
|
+
.all(agentDid, limit);
|
|
1033
|
+
return rows.map((r) => ({
|
|
1034
|
+
id: r.id,
|
|
1035
|
+
agentDid: r.agent_did,
|
|
1036
|
+
dimension: r.dimension,
|
|
1037
|
+
score: r.score,
|
|
1038
|
+
evidence: r.evidence,
|
|
1039
|
+
createdAt: r.created_at,
|
|
1040
|
+
}));
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export function getTopAgents(db, { dimension, limit = 10 } = {}) {
|
|
1044
|
+
const reg = db
|
|
1045
|
+
.prepare(`SELECT agent_did FROM federated_agent_registry`)
|
|
1046
|
+
.all();
|
|
1047
|
+
const dids = new Set(reg.map((r) => r.agent_did));
|
|
1048
|
+
const repRows = db.prepare(`SELECT agent_did FROM agent_reputation`).all();
|
|
1049
|
+
for (const r of repRows) dids.add(r.agent_did);
|
|
1050
|
+
const scored = [];
|
|
1051
|
+
for (const d of dids) {
|
|
1052
|
+
const rep = getReputation(db, d);
|
|
1053
|
+
if (dimension) {
|
|
1054
|
+
const dim = rep.dimensions[dimension];
|
|
1055
|
+
if (!dim) continue;
|
|
1056
|
+
scored.push({ agentDid: d, score: dim.score, samples: dim.samples });
|
|
1057
|
+
} else {
|
|
1058
|
+
scored.push({ agentDid: d, score: rep.total, samples: rep.samples });
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1062
|
+
return scored.slice(0, limit);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/* ── Network-level stats & config ────────────────────────── */
|
|
1066
|
+
|
|
1067
|
+
export function getNetworkStats(db) {
|
|
1068
|
+
const dids = db
|
|
1069
|
+
.prepare(`SELECT status, COUNT(*) as cnt FROM agent_dids GROUP BY status`)
|
|
1070
|
+
.all();
|
|
1071
|
+
const reg = db
|
|
1072
|
+
.prepare(
|
|
1073
|
+
`SELECT status, COUNT(*) as cnt FROM federated_agent_registry GROUP BY status`,
|
|
1074
|
+
)
|
|
1075
|
+
.all();
|
|
1076
|
+
const tasks = db
|
|
1077
|
+
.prepare(
|
|
1078
|
+
`SELECT status, COUNT(*) as cnt FROM federated_task_log GROUP BY status`,
|
|
1079
|
+
)
|
|
1080
|
+
.all();
|
|
1081
|
+
const creds = db
|
|
1082
|
+
.prepare(
|
|
1083
|
+
`SELECT status, COUNT(*) as cnt FROM agent_credentials GROUP BY status`,
|
|
1084
|
+
)
|
|
1085
|
+
.all();
|
|
1086
|
+
const peers = db
|
|
1087
|
+
.prepare(`SELECT COUNT(*) as cnt FROM federated_registry_peers`)
|
|
1088
|
+
.get();
|
|
1089
|
+
const sessions = db
|
|
1090
|
+
.prepare(
|
|
1091
|
+
`SELECT status, COUNT(*) as cnt FROM agent_auth_sessions GROUP BY status`,
|
|
1092
|
+
)
|
|
1093
|
+
.all();
|
|
1094
|
+
const _sum = (rows) => Object.fromEntries(rows.map((r) => [r.status, r.cnt]));
|
|
1095
|
+
return {
|
|
1096
|
+
dids: _sum(dids),
|
|
1097
|
+
registry: _sum(reg),
|
|
1098
|
+
tasks: _sum(tasks),
|
|
1099
|
+
credentials: _sum(creds),
|
|
1100
|
+
sessions: _sum(sessions),
|
|
1101
|
+
peers: peers?.cnt ?? 0,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
export function getNetworkConfig() {
|
|
1106
|
+
return {
|
|
1107
|
+
kademliaBits: KADEMLIA_BITS,
|
|
1108
|
+
kademliaK: KADEMLIA_K,
|
|
1109
|
+
sessionTtlMs: SESSION_TTL_MS,
|
|
1110
|
+
heartbeatTimeoutMs: HEARTBEAT_TIMEOUT_MS,
|
|
1111
|
+
reputationDimensions: [...REPUTATION_DIMENSIONS],
|
|
1112
|
+
reputationWeights: { ...REPUTATION_WEIGHTS },
|
|
1113
|
+
reputationRange: [MIN_REPUTATION, MAX_REPUTATION],
|
|
1114
|
+
reputationDecayWeekly: REPUTATION_DECAY_WEEKLY,
|
|
1115
|
+
taskStatuses: Object.values(TASK_STATUS),
|
|
1116
|
+
regStatuses: Object.values(REG_STATUS),
|
|
1117
|
+
didStatuses: Object.values(DID_STATUS),
|
|
1118
|
+
authStatuses: Object.values(AUTH_STATUS),
|
|
1119
|
+
credStatuses: Object.values(CRED_STATUS),
|
|
1120
|
+
};
|
|
1121
|
+
}
|