@hivemind-os/collective-core 0.2.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/.test-data/05a845c4-1682-41b9-97e5-65a5263156c0/spending.sqlite +0 -0
- package/.test-data/10e18ba5-98f0-42e0-8899-06a09459ae85/agents.sqlite +0 -0
- package/.test-data/20464456-23cb-4ff7-8df5-3c129fb95a90/agents.sqlite +0 -0
- package/.test-data/2e2ec66c-e8b4-43eb-945f-cca84ed0a0f6/agents.sqlite +0 -0
- package/.test-data/2f5ef9b7-01da-4ba0-a6fa-af8063a2567c/agents.sqlite +0 -0
- package/.test-data/3ac13cd5-84c9-4e71-8b89-6d3ef4dd819c/agents.sqlite +0 -0
- package/.test-data/40b7dd4a-fae0-4a5d-a6a0-64c9048af35e/agents.sqlite +0 -0
- package/.test-data/59511a04-42f8-4286-bb1e-733795e08749/agents.sqlite +0 -0
- package/.test-data/6778e8c5-6eb5-416d-8aca-9d559cdf83e4/agents.sqlite +0 -0
- package/.test-data/7648dc86-df90-4460-8f9c-55cdb481324b/agents.sqlite +0 -0
- package/.test-data/77162d98-6b22-41b1-a50f-536fab739f8a/agents.sqlite +0 -0
- package/.test-data/798dbdab-cbe7-4edd-8a5b-ae99c285fe17/agents.sqlite +0 -0
- package/.test-data/8033d6ac-8b85-454d-b708-00ac695b22f8/agents.sqlite +0 -0
- package/.test-data/9155a4f4-cda3-487c-9eed-921c82d7550f/agents.sqlite +0 -0
- package/.test-data/9bfeee53-c231-46c3-8c93-2180933f5d50/agents.sqlite +0 -0
- package/.test-data/a4c64287-79f6-46e9-847d-2803c63a74fd/agents.sqlite +0 -0
- package/.test-data/b8f58952-1ed8-46ff-abd7-21fb86e9457f/agents.sqlite +0 -0
- package/.test-data/c3060504-3187-41ed-8532-82332be48b0b/spending.sqlite +0 -0
- package/.test-data/cc471629-8006-4fc1-b8a1-399d2df2cc4e/agents.sqlite +0 -0
- package/.test-data/dbca3bef-397d-4bbc-bd4c-4f7b14103e04/spending.sqlite +0 -0
- package/.test-data/f1283dd1-6602-4de7-a050-16aac7abc288/agents.sqlite +0 -0
- package/.turbo/turbo-build.log +14 -0
- package/dist/index.d.ts +1675 -0
- package/dist/index.js +8006 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/src/auth/device-flow.ts +108 -0
- package/src/auth/ed25519-provider.ts +43 -0
- package/src/auth/errors.ts +82 -0
- package/src/auth/evm-key.ts +55 -0
- package/src/auth/index.ts +8 -0
- package/src/auth/session-state.ts +25 -0
- package/src/auth/session-store.ts +510 -0
- package/src/auth/types.ts +81 -0
- package/src/auth/zklogin-provider.ts +902 -0
- package/src/blobstore/WALRUS_FINDINGS.md +284 -0
- package/src/blobstore/encrypted-store.ts +56 -0
- package/src/blobstore/fs-store.ts +91 -0
- package/src/blobstore/hybrid-store.ts +144 -0
- package/src/blobstore/index.ts +5 -0
- package/src/blobstore/interface.ts +33 -0
- package/src/blobstore/walrus-spike.ts +345 -0
- package/src/blobstore/walrus-store.ts +551 -0
- package/src/cache/agent-cache.ts +403 -0
- package/src/cache/index.ts +1 -0
- package/src/crypto/encryption.ts +152 -0
- package/src/crypto/index.ts +2 -0
- package/src/crypto/x25519.ts +41 -0
- package/src/dispute/client.ts +191 -0
- package/src/dispute/index.ts +1 -0
- package/src/events/index.ts +2 -0
- package/src/events/parser.ts +291 -0
- package/src/events/subscription.ts +131 -0
- package/src/evm/constants.ts +6 -0
- package/src/evm/index.ts +2 -0
- package/src/evm/wallet.ts +136 -0
- package/src/identity/did.ts +36 -0
- package/src/identity/index.ts +4 -0
- package/src/identity/keypair.ts +199 -0
- package/src/identity/signing.ts +28 -0
- package/src/index.ts +22 -0
- package/src/internal/parsing.ts +416 -0
- package/src/marketplace/client.ts +349 -0
- package/src/marketplace/index.ts +1 -0
- package/src/metering/hash-chain.ts +94 -0
- package/src/metering/index.ts +4 -0
- package/src/metering/meter.ts +80 -0
- package/src/metering/streaming.ts +196 -0
- package/src/metering/verification.ts +104 -0
- package/src/payment/index.ts +1 -0
- package/src/payment/rail-selector.ts +41 -0
- package/src/registry/client.ts +328 -0
- package/src/registry/index.ts +1 -0
- package/src/relay/consumer-client.ts +497 -0
- package/src/relay/index.ts +1 -0
- package/src/relay-registry/client.ts +295 -0
- package/src/relay-registry/discovery.ts +109 -0
- package/src/relay-registry/index.ts +2 -0
- package/src/reputation/anchor-client.ts +126 -0
- package/src/reputation/event-publisher.ts +67 -0
- package/src/reputation/index.ts +5 -0
- package/src/reputation/merkle.ts +79 -0
- package/src/reputation/score-calculator.ts +133 -0
- package/src/reputation/serialization.ts +37 -0
- package/src/reputation/store.ts +165 -0
- package/src/reputation/validation.ts +135 -0
- package/src/routing/circuit-breaker.ts +111 -0
- package/src/routing/fan-out.ts +266 -0
- package/src/routing/index.ts +4 -0
- package/src/routing/performance.ts +244 -0
- package/src/routing/selector.ts +225 -0
- package/src/spending/index.ts +1 -0
- package/src/spending/policy.ts +271 -0
- package/src/staking/client.ts +319 -0
- package/src/staking/index.ts +1 -0
- package/src/sui/client.ts +214 -0
- package/src/sui/index.ts +2 -0
- package/src/sui/tx-helpers.ts +1070 -0
- package/src/task/client.ts +215 -0
- package/src/task/index.ts +1 -0
- package/src/x402/client.ts +295 -0
- package/src/x402/index.ts +1 -0
- package/tests/auth/device-flow.test.ts +62 -0
- package/tests/auth/ed25519-provider.test.ts +24 -0
- package/tests/auth/evm-key.test.ts +31 -0
- package/tests/auth/session-store.test.ts +201 -0
- package/tests/auth/zklogin-provider.test.ts +366 -0
- package/tests/blobstore/encrypted-store.test.ts +78 -0
- package/tests/blobstore.test.ts +91 -0
- package/tests/cache.test.ts +124 -0
- package/tests/crypto/encryption.test.ts +70 -0
- package/tests/crypto/x25519.test.ts +47 -0
- package/tests/dispute/client.test.ts +238 -0
- package/tests/events.test.ts +202 -0
- package/tests/evm/wallet.test.ts +101 -0
- package/tests/hybrid-store.test.ts +121 -0
- package/tests/identity.test.ts +161 -0
- package/tests/marketplace.test.ts +308 -0
- package/tests/metering/hash-chain.test.ts +32 -0
- package/tests/metering/meter.test.ts +23 -0
- package/tests/metering/streaming.test.ts +52 -0
- package/tests/metering/verification.test.ts +27 -0
- package/tests/payment/rail-selector.test.ts +95 -0
- package/tests/registry.test.ts +183 -0
- package/tests/relay-consumer-client.test.ts +119 -0
- package/tests/relay-registry/client.test.ts +261 -0
- package/tests/reputation/event-publisher.test.ts +70 -0
- package/tests/reputation/merkle.test.ts +44 -0
- package/tests/reputation/score-calculator.test.ts +104 -0
- package/tests/reputation/store.test.ts +94 -0
- package/tests/routing/circuit-breaker.test.ts +45 -0
- package/tests/routing/fan-out.test.ts +123 -0
- package/tests/routing/performance.test.ts +49 -0
- package/tests/routing/selector.test.ts +114 -0
- package/tests/spending.test.ts +133 -0
- package/tests/staking/client.test.ts +286 -0
- package/tests/sui-client.test.ts +85 -0
- package/tests/task.test.ts +249 -0
- package/tests/tx-helpers.test.ts +70 -0
- package/tests/walrus-spike.test.ts +100 -0
- package/tests/walrus-store.test.ts +196 -0
- package/tests/x402/client.test.ts +116 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
import type { AgentCard, Capability, ReputationScore } from '@hivemind-os/collective-types';
|
|
4
|
+
|
|
5
|
+
import { ReputationScoreCalculator } from '../reputation/score-calculator.js';
|
|
6
|
+
|
|
7
|
+
export interface AdvancedAgentQueryFilters {
|
|
8
|
+
capability?: string;
|
|
9
|
+
minReputation?: number;
|
|
10
|
+
category?: string;
|
|
11
|
+
search?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
offset?: number;
|
|
14
|
+
sortBy?: 'stake' | 'reputation';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type AdvancedAgentQueryDelegate = (filters: AdvancedAgentQueryFilters) => Promise<AgentCard[]>;
|
|
18
|
+
|
|
19
|
+
interface AgentRow {
|
|
20
|
+
rowid: number;
|
|
21
|
+
id: string;
|
|
22
|
+
owner: string;
|
|
23
|
+
did: string;
|
|
24
|
+
name: string;
|
|
25
|
+
description: string | null;
|
|
26
|
+
capabilities_json: string | null;
|
|
27
|
+
capabilities_text: string | null;
|
|
28
|
+
endpoint: string | null;
|
|
29
|
+
encryption_public_key: string | null;
|
|
30
|
+
active: number | bigint;
|
|
31
|
+
version: number | bigint;
|
|
32
|
+
registered_at: number | bigint | null;
|
|
33
|
+
updated_at: number | bigint | null;
|
|
34
|
+
total_tasks_completed: number | bigint | null;
|
|
35
|
+
total_tasks_failed: number | bigint | null;
|
|
36
|
+
total_tasks_disputed: number | bigint | null;
|
|
37
|
+
total_earnings_mist: number | bigint | string | null;
|
|
38
|
+
has_stake: number | bigint | null;
|
|
39
|
+
stake_mist: number | bigint | string | null;
|
|
40
|
+
stake_type: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class AgentCache {
|
|
44
|
+
private readonly db: Database.Database;
|
|
45
|
+
private readonly scoreCalculator = new ReputationScoreCalculator();
|
|
46
|
+
private readonly queryDelegate?: AdvancedAgentQueryDelegate;
|
|
47
|
+
|
|
48
|
+
constructor(dbPath: string, options: { queryDelegate?: AdvancedAgentQueryDelegate } = {}) {
|
|
49
|
+
this.queryDelegate = options.queryDelegate;
|
|
50
|
+
this.db = new Database(dbPath);
|
|
51
|
+
this.db.defaultSafeIntegers(true);
|
|
52
|
+
this.db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
owner TEXT NOT NULL,
|
|
56
|
+
did TEXT UNIQUE NOT NULL,
|
|
57
|
+
name TEXT NOT NULL,
|
|
58
|
+
description TEXT,
|
|
59
|
+
capabilities_json TEXT,
|
|
60
|
+
capabilities_text TEXT,
|
|
61
|
+
endpoint TEXT,
|
|
62
|
+
encryption_public_key TEXT,
|
|
63
|
+
active INTEGER DEFAULT 1,
|
|
64
|
+
version INTEGER DEFAULT 1,
|
|
65
|
+
registered_at INTEGER,
|
|
66
|
+
updated_at INTEGER,
|
|
67
|
+
total_tasks_completed INTEGER,
|
|
68
|
+
total_tasks_failed INTEGER,
|
|
69
|
+
total_tasks_disputed INTEGER,
|
|
70
|
+
total_earnings_mist TEXT,
|
|
71
|
+
has_stake INTEGER,
|
|
72
|
+
stake_mist TEXT,
|
|
73
|
+
stake_type TEXT
|
|
74
|
+
);
|
|
75
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS agents_fts USING fts5(
|
|
76
|
+
agent_id UNINDEXED, name, description, capabilities_text
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
ensureAgentsColumn(this.db, 'encryption_public_key', 'TEXT');
|
|
80
|
+
ensureAgentsColumn(this.db, 'total_tasks_completed', 'INTEGER');
|
|
81
|
+
ensureAgentsColumn(this.db, 'total_tasks_failed', 'INTEGER');
|
|
82
|
+
ensureAgentsColumn(this.db, 'total_tasks_disputed', 'INTEGER');
|
|
83
|
+
ensureAgentsColumn(this.db, 'total_earnings_mist', 'TEXT');
|
|
84
|
+
ensureAgentsColumn(this.db, 'has_stake', 'INTEGER');
|
|
85
|
+
ensureAgentsColumn(this.db, 'stake_mist', 'TEXT');
|
|
86
|
+
ensureAgentsColumn(this.db, 'stake_type', 'TEXT');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
upsertAgent(agent: AgentCard): void {
|
|
90
|
+
const capabilitiesJson = JSON.stringify(agent.capabilities, bigintReplacer);
|
|
91
|
+
const capabilitiesText = agent.capabilities
|
|
92
|
+
.map((entry) => `${entry.name} ${entry.description} ${entry.version}`)
|
|
93
|
+
.join(' ');
|
|
94
|
+
this.db
|
|
95
|
+
.prepare(
|
|
96
|
+
`INSERT INTO agents (
|
|
97
|
+
id, owner, did, name, description, capabilities_json, capabilities_text, endpoint,
|
|
98
|
+
encryption_public_key, active, version, registered_at, updated_at,
|
|
99
|
+
total_tasks_completed, total_tasks_failed, total_tasks_disputed, total_earnings_mist,
|
|
100
|
+
has_stake, stake_mist, stake_type
|
|
101
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
102
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
103
|
+
owner = excluded.owner,
|
|
104
|
+
did = excluded.did,
|
|
105
|
+
name = excluded.name,
|
|
106
|
+
description = excluded.description,
|
|
107
|
+
capabilities_json = excluded.capabilities_json,
|
|
108
|
+
capabilities_text = excluded.capabilities_text,
|
|
109
|
+
endpoint = excluded.endpoint,
|
|
110
|
+
encryption_public_key = excluded.encryption_public_key,
|
|
111
|
+
active = excluded.active,
|
|
112
|
+
version = excluded.version,
|
|
113
|
+
registered_at = excluded.registered_at,
|
|
114
|
+
updated_at = excluded.updated_at,
|
|
115
|
+
total_tasks_completed = excluded.total_tasks_completed,
|
|
116
|
+
total_tasks_failed = excluded.total_tasks_failed,
|
|
117
|
+
total_tasks_disputed = excluded.total_tasks_disputed,
|
|
118
|
+
total_earnings_mist = excluded.total_earnings_mist,
|
|
119
|
+
has_stake = excluded.has_stake,
|
|
120
|
+
stake_mist = excluded.stake_mist,
|
|
121
|
+
stake_type = excluded.stake_type`,
|
|
122
|
+
)
|
|
123
|
+
.run(
|
|
124
|
+
agent.id,
|
|
125
|
+
agent.owner,
|
|
126
|
+
agent.did,
|
|
127
|
+
agent.name,
|
|
128
|
+
agent.description,
|
|
129
|
+
capabilitiesJson,
|
|
130
|
+
capabilitiesText,
|
|
131
|
+
agent.endpoint ?? null,
|
|
132
|
+
agent.encryptionPublicKey ?? null,
|
|
133
|
+
agent.active ? 1 : 0,
|
|
134
|
+
agent.version,
|
|
135
|
+
agent.registeredAt,
|
|
136
|
+
agent.updatedAt,
|
|
137
|
+
agent.totalTasksCompleted ?? null,
|
|
138
|
+
agent.totalTasksFailed ?? null,
|
|
139
|
+
agent.totalTasksDisputed ?? null,
|
|
140
|
+
agent.totalEarningsMist?.toString() ?? null,
|
|
141
|
+
agent.hasStake == null ? null : agent.hasStake ? 1 : 0,
|
|
142
|
+
agent.stakeMist?.toString() ?? null,
|
|
143
|
+
agent.stakeType ?? null,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
this.db.prepare('DELETE FROM agents_fts WHERE agent_id = ?').run(agent.id);
|
|
147
|
+
this.db
|
|
148
|
+
.prepare(
|
|
149
|
+
'INSERT INTO agents_fts (agent_id, name, description, capabilities_text) VALUES (?, ?, ?, ?)',
|
|
150
|
+
)
|
|
151
|
+
.run(agent.id, agent.name, agent.description, capabilitiesText);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
removeAgent(agentId: string): void {
|
|
155
|
+
this.db.prepare('DELETE FROM agents_fts WHERE agent_id = ?').run(agentId);
|
|
156
|
+
this.db.prepare('DELETE FROM agents WHERE id = ?').run(agentId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getAgent(agentId: string): AgentCard | null {
|
|
160
|
+
const row = this.db.prepare('SELECT rowid, * FROM agents WHERE id = ?').get(agentId) as
|
|
161
|
+
| AgentRow
|
|
162
|
+
| undefined;
|
|
163
|
+
return row ? mapAgentRow(row) : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getAgentByDID(did: string): AgentCard | null {
|
|
167
|
+
const row = this.db.prepare('SELECT rowid, * FROM agents WHERE did = ?').get(did) as
|
|
168
|
+
| AgentRow
|
|
169
|
+
| undefined;
|
|
170
|
+
return row ? mapAgentRow(row) : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
searchByCapability(
|
|
174
|
+
query: string,
|
|
175
|
+
limit = 20,
|
|
176
|
+
options: { sortByReputation?: boolean; scores?: Map<string, ReputationScore> } = {},
|
|
177
|
+
): AgentCard[] {
|
|
178
|
+
const ftsQuery = buildFtsQuery(query);
|
|
179
|
+
if (!ftsQuery) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const rows = this.db
|
|
185
|
+
.prepare(
|
|
186
|
+
`SELECT a.rowid, a.*
|
|
187
|
+
FROM agents_fts
|
|
188
|
+
JOIN agents a ON a.id = agents_fts.agent_id
|
|
189
|
+
WHERE agents_fts MATCH ? AND a.active = 1
|
|
190
|
+
LIMIT ?`,
|
|
191
|
+
)
|
|
192
|
+
.all(ftsQuery, limit) as AgentRow[];
|
|
193
|
+
return rankAgents(rows.map(mapAgentRow), options, this.scoreCalculator).slice(0, limit);
|
|
194
|
+
} catch {
|
|
195
|
+
const like = `%${query}%`;
|
|
196
|
+
const rows = this.db
|
|
197
|
+
.prepare(
|
|
198
|
+
`SELECT rowid, * FROM agents
|
|
199
|
+
WHERE active = 1
|
|
200
|
+
AND (name LIKE ? OR description LIKE ? OR capabilities_json LIKE ? OR capabilities_text LIKE ?)
|
|
201
|
+
LIMIT ?`,
|
|
202
|
+
)
|
|
203
|
+
.all(like, like, like, like, limit) as AgentRow[];
|
|
204
|
+
return rankAgents(rows.map(mapAgentRow), options, this.scoreCalculator).slice(0, limit);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getAllActive(limit = 100): AgentCard[] {
|
|
209
|
+
const rows = this.db
|
|
210
|
+
.prepare('SELECT rowid, * FROM agents WHERE active = 1 ORDER BY updated_at DESC LIMIT ?')
|
|
211
|
+
.all(limit) as AgentRow[];
|
|
212
|
+
return rows.map(mapAgentRow);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async queryAgentsAdvanced(filters: AdvancedAgentQueryFilters = {}): Promise<AgentCard[]> {
|
|
216
|
+
if (this.queryDelegate) {
|
|
217
|
+
try {
|
|
218
|
+
return await this.queryDelegate(filters);
|
|
219
|
+
} catch {
|
|
220
|
+
// Fall through to the local cache query.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const limit = normalizeLimit(filters.limit, 20);
|
|
225
|
+
const offset = normalizeOffset(filters.offset);
|
|
226
|
+
const search = filters.search?.trim();
|
|
227
|
+
const capability = filters.capability?.trim();
|
|
228
|
+
|
|
229
|
+
let agents = search || capability
|
|
230
|
+
? this.searchByCapability(search ?? capability ?? '', Math.max(limit + offset, limit), {
|
|
231
|
+
sortByReputation: filters.sortBy === 'reputation',
|
|
232
|
+
})
|
|
233
|
+
: this.getAllActive(Math.max(limit + offset, 100));
|
|
234
|
+
|
|
235
|
+
if (capability) {
|
|
236
|
+
agents = agents.filter((agent) => agent.capabilities.some((entry) => equalsIgnoreCase(entry.name, capability)));
|
|
237
|
+
}
|
|
238
|
+
if (search) {
|
|
239
|
+
const searchLower = search.toLowerCase();
|
|
240
|
+
agents = agents.filter((agent) => matchesSearch(agent, searchLower));
|
|
241
|
+
}
|
|
242
|
+
if (filters.category) {
|
|
243
|
+
const categoryLower = filters.category.toLowerCase();
|
|
244
|
+
agents = agents.filter((agent) =>
|
|
245
|
+
agent.capabilities.some(
|
|
246
|
+
(entry) =>
|
|
247
|
+
entry.name.toLowerCase().includes(categoryLower) ||
|
|
248
|
+
entry.description.toLowerCase().includes(categoryLower),
|
|
249
|
+
) || agent.description.toLowerCase().includes(categoryLower),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const scores = new Map(agents.map((agent) => [agent.did, this.scoreCalculator.computeScore(agent, [])]));
|
|
254
|
+
const minReputation = typeof filters.minReputation === 'number' ? filters.minReputation : undefined;
|
|
255
|
+
if (minReputation !== undefined) {
|
|
256
|
+
agents = agents.filter((agent) => (scores.get(agent.did)?.successRate ?? 0) >= minReputation);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const ranked = rankAgents(
|
|
260
|
+
agents,
|
|
261
|
+
{ sortByReputation: filters.sortBy === 'reputation', scores },
|
|
262
|
+
this.scoreCalculator,
|
|
263
|
+
);
|
|
264
|
+
return ranked.slice(offset, offset + limit);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
close(): void {
|
|
268
|
+
this.db.close();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function mapAgentRow(row: AgentRow): AgentCard {
|
|
273
|
+
return {
|
|
274
|
+
id: row.id,
|
|
275
|
+
owner: row.owner,
|
|
276
|
+
did: row.did as AgentCard['did'],
|
|
277
|
+
name: row.name,
|
|
278
|
+
description: row.description ?? '',
|
|
279
|
+
capabilities: parseCapabilities(row.capabilities_json),
|
|
280
|
+
endpoint: row.endpoint ?? undefined,
|
|
281
|
+
encryptionPublicKey: row.encryption_public_key ?? undefined,
|
|
282
|
+
active: Number(row.active) === 1,
|
|
283
|
+
version: Number(row.version),
|
|
284
|
+
registeredAt: Number(row.registered_at ?? 0),
|
|
285
|
+
updatedAt: Number(row.updated_at ?? 0),
|
|
286
|
+
totalTasksCompleted: row.total_tasks_completed == null ? undefined : Number(row.total_tasks_completed),
|
|
287
|
+
totalTasksFailed: row.total_tasks_failed == null ? undefined : Number(row.total_tasks_failed),
|
|
288
|
+
totalTasksDisputed: row.total_tasks_disputed == null ? undefined : Number(row.total_tasks_disputed),
|
|
289
|
+
totalEarningsMist: row.total_earnings_mist == null ? undefined : BigInt(row.total_earnings_mist),
|
|
290
|
+
hasStake: row.has_stake == null ? undefined : Number(row.has_stake) === 1,
|
|
291
|
+
stakeMist: row.stake_mist == null ? undefined : BigInt(row.stake_mist),
|
|
292
|
+
stakeType: row.stake_type === 'agent' || row.stake_type === 'relay' ? row.stake_type : undefined,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function parseCapabilities(value: string | null): Capability[] {
|
|
297
|
+
if (!value) {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const parsed = JSON.parse(value) as Array<Capability & { pricing: Capability['pricing'] & { amount: string } }>;
|
|
302
|
+
return parsed.map((entry) => ({
|
|
303
|
+
...entry,
|
|
304
|
+
pricing: {
|
|
305
|
+
...entry.pricing,
|
|
306
|
+
amount: BigInt(entry.pricing.amount),
|
|
307
|
+
},
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function bigintReplacer(_key: string, value: unknown): unknown {
|
|
312
|
+
return typeof value === 'bigint' ? value.toString() : value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function ensureAgentsColumn(db: Database.Database, column: string, type: string): void {
|
|
316
|
+
const columns = db.prepare('PRAGMA table_info(agents)').all() as Array<{ name: string }>;
|
|
317
|
+
if (!columns.some((entry) => entry.name === column)) {
|
|
318
|
+
db.exec(`ALTER TABLE agents ADD COLUMN ${column} ${type}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function matchesSearch(agent: AgentCard, searchLower: string): boolean {
|
|
323
|
+
return (
|
|
324
|
+
agent.name.toLowerCase().includes(searchLower) ||
|
|
325
|
+
agent.description.toLowerCase().includes(searchLower) ||
|
|
326
|
+
agent.capabilities.some(
|
|
327
|
+
(entry) =>
|
|
328
|
+
entry.name.toLowerCase().includes(searchLower) ||
|
|
329
|
+
entry.description.toLowerCase().includes(searchLower) ||
|
|
330
|
+
entry.version.toLowerCase().includes(searchLower),
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function equalsIgnoreCase(left: string, right: string): boolean {
|
|
336
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeLimit(limit: number | undefined, fallback: number): number {
|
|
340
|
+
if (typeof limit !== 'number' || Number.isNaN(limit)) {
|
|
341
|
+
return fallback;
|
|
342
|
+
}
|
|
343
|
+
return Math.max(1, Math.floor(limit));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function normalizeOffset(offset: number | undefined): number {
|
|
347
|
+
if (typeof offset !== 'number' || Number.isNaN(offset)) {
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
return Math.max(0, Math.floor(offset));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function buildFtsQuery(query: string): string {
|
|
354
|
+
const terms = query
|
|
355
|
+
.trim()
|
|
356
|
+
.split(/\s+/)
|
|
357
|
+
.map((term) => term.replace(/[^\p{L}\p{N}_-]/gu, ''))
|
|
358
|
+
.filter(Boolean);
|
|
359
|
+
|
|
360
|
+
return terms.map((term) => `"${term}"*`).join(' OR ');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function rankAgents(
|
|
364
|
+
agents: AgentCard[],
|
|
365
|
+
options: { sortByReputation?: boolean; scores?: Map<string, ReputationScore> },
|
|
366
|
+
calculator: ReputationScoreCalculator,
|
|
367
|
+
): AgentCard[] {
|
|
368
|
+
if (!options.sortByReputation) {
|
|
369
|
+
return [...agents].sort(compareStakePreference);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const scores = options.scores ?? new Map(agents.map((agent) => [agent.did, calculator.computeScore(agent, [])]));
|
|
373
|
+
return calculator.rankByReputation(agents, scores);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function compareStakePreference(left: AgentCard, right: AgentCard): number {
|
|
377
|
+
return (
|
|
378
|
+
compareBoolean(left.hasStake ?? false, right.hasStake ?? false) ||
|
|
379
|
+
compareBigInt(left.stakeMist ?? 0n, right.stakeMist ?? 0n) ||
|
|
380
|
+
compareNumber(left.updatedAt, right.updatedAt)
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function compareBoolean(left: boolean, right: boolean): number {
|
|
385
|
+
if (left === right) {
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
return left ? -1 : 1;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function compareBigInt(left: bigint, right: bigint): number {
|
|
392
|
+
if (left === right) {
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
|
395
|
+
return left > right ? -1 : 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function compareNumber(left: number, right: number): number {
|
|
399
|
+
if (left === right) {
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
return left > right ? -1 : 1;
|
|
403
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './agent-cache.js';
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
4
|
+
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
5
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
6
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
7
|
+
|
|
8
|
+
import { computeSharedSecret } from './x25519.js';
|
|
9
|
+
|
|
10
|
+
export interface EncryptedPayload {
|
|
11
|
+
version: 1;
|
|
12
|
+
senderPublicKey: string;
|
|
13
|
+
nonce: string;
|
|
14
|
+
ciphertext: string;
|
|
15
|
+
tag: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
const decoder = new TextDecoder();
|
|
20
|
+
const ENCRYPTION_CONTEXT = 'agentic-mesh:encrypt:v1';
|
|
21
|
+
const NONCE_BYTES = 24;
|
|
22
|
+
const PUBLIC_KEY_BYTES = 32;
|
|
23
|
+
const TAG_BYTES = 16;
|
|
24
|
+
|
|
25
|
+
export async function encryptForRecipient(
|
|
26
|
+
plaintext: Uint8Array,
|
|
27
|
+
senderPrivateKey: Uint8Array,
|
|
28
|
+
recipientPublicKey: Uint8Array,
|
|
29
|
+
): Promise<EncryptedPayload> {
|
|
30
|
+
assertByteLength(senderPrivateKey, PUBLIC_KEY_BYTES, 'senderPrivateKey');
|
|
31
|
+
assertByteLength(recipientPublicKey, PUBLIC_KEY_BYTES, 'recipientPublicKey');
|
|
32
|
+
|
|
33
|
+
const nonce = randomBytes(NONCE_BYTES);
|
|
34
|
+
const senderPublicKey = x25519.getPublicKey(senderPrivateKey);
|
|
35
|
+
const sharedSecret = computeSharedSecret(senderPrivateKey, recipientPublicKey);
|
|
36
|
+
const encryptionKey = deriveEncryptionKey(sharedSecret, nonce, senderPublicKey, recipientPublicKey);
|
|
37
|
+
const ciphertext = xchacha20poly1305(encryptionKey, nonce).encrypt(plaintext);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
version: 1,
|
|
41
|
+
senderPublicKey: toHex(senderPublicKey),
|
|
42
|
+
nonce: toHex(nonce),
|
|
43
|
+
ciphertext: toHex(ciphertext),
|
|
44
|
+
tag: toHex(ciphertext.slice(-TAG_BYTES)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function decryptFromSender(
|
|
49
|
+
payload: EncryptedPayload,
|
|
50
|
+
recipientPrivateKey: Uint8Array,
|
|
51
|
+
): Promise<Uint8Array> {
|
|
52
|
+
assertEncryptedPayload(payload);
|
|
53
|
+
assertByteLength(recipientPrivateKey, PUBLIC_KEY_BYTES, 'recipientPrivateKey');
|
|
54
|
+
|
|
55
|
+
const senderPublicKey = fromHexExact(payload.senderPublicKey, PUBLIC_KEY_BYTES, 'senderPublicKey');
|
|
56
|
+
const nonce = fromHexExact(payload.nonce, NONCE_BYTES, 'nonce');
|
|
57
|
+
const ciphertext = fromHexAtLeast(payload.ciphertext, TAG_BYTES, 'ciphertext');
|
|
58
|
+
const tag = fromHexExact(payload.tag, TAG_BYTES, 'tag');
|
|
59
|
+
|
|
60
|
+
const actualTag = ciphertext.slice(-TAG_BYTES);
|
|
61
|
+
if (!timingSafeEqual(Buffer.from(actualTag), Buffer.from(tag))) {
|
|
62
|
+
throw new Error('Encrypted payload tag does not match ciphertext.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const recipientPublicKey = x25519.getPublicKey(recipientPrivateKey);
|
|
66
|
+
const sharedSecret = computeSharedSecret(recipientPrivateKey, senderPublicKey);
|
|
67
|
+
const encryptionKey = deriveEncryptionKey(sharedSecret, nonce, senderPublicKey, recipientPublicKey);
|
|
68
|
+
return xchacha20poly1305(encryptionKey, nonce).decrypt(ciphertext);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function serializeEncryptedPayload(payload: EncryptedPayload): Uint8Array {
|
|
72
|
+
assertEncryptedPayload(payload);
|
|
73
|
+
return encoder.encode(JSON.stringify(payload));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseEncryptedPayload(data: Uint8Array): EncryptedPayload | null {
|
|
77
|
+
let parsed: unknown;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(decoder.decode(data)) as unknown;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!isEncryptedPayload(parsed)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isEncryptedPayload(value: unknown): value is EncryptedPayload {
|
|
92
|
+
if (!value || typeof value !== 'object') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const candidate = value as Record<string, unknown>;
|
|
97
|
+
return candidate.version === 1
|
|
98
|
+
&& isExactHexString(candidate.senderPublicKey, PUBLIC_KEY_BYTES)
|
|
99
|
+
&& isExactHexString(candidate.nonce, NONCE_BYTES)
|
|
100
|
+
&& isHexStringAtLeast(candidate.ciphertext, TAG_BYTES)
|
|
101
|
+
&& isExactHexString(candidate.tag, TAG_BYTES);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function deriveEncryptionKey(
|
|
105
|
+
sharedSecret: Uint8Array,
|
|
106
|
+
nonce: Uint8Array,
|
|
107
|
+
senderPublicKey: Uint8Array,
|
|
108
|
+
recipientPublicKey: Uint8Array,
|
|
109
|
+
): Uint8Array {
|
|
110
|
+
const info = encoder.encode(`${ENCRYPTION_CONTEXT}:sender:${toHex(senderPublicKey)}:recipient:${toHex(recipientPublicKey)}`);
|
|
111
|
+
return hkdf(sha256, sharedSecret, nonce, info, 32);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function assertEncryptedPayload(payload: EncryptedPayload): void {
|
|
115
|
+
if (!isEncryptedPayload(payload)) {
|
|
116
|
+
throw new Error('Invalid encrypted payload.');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertByteLength(value: Uint8Array, expectedLength: number, field: string): void {
|
|
121
|
+
if (value.length !== expectedLength) {
|
|
122
|
+
throw new Error(`${field} must be ${expectedLength} bytes, received ${value.length}.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function fromHexExact(value: string, exactBytes: number, field: string): Uint8Array {
|
|
127
|
+
if (!isExactHexString(value, exactBytes)) {
|
|
128
|
+
throw new Error(`${field} must be exactly ${exactBytes} bytes of hex.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return new Uint8Array(Buffer.from(value, 'hex'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function fromHexAtLeast(value: string, minimumBytes: number, field: string): Uint8Array {
|
|
135
|
+
if (!isHexStringAtLeast(value, minimumBytes)) {
|
|
136
|
+
throw new Error(`${field} must be at least ${minimumBytes} bytes of hex.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return new Uint8Array(Buffer.from(value, 'hex'));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isExactHexString(value: unknown, exactBytes: number): value is string {
|
|
143
|
+
return typeof value === 'string' && value.length === exactBytes * 2 && /^[a-f0-9]+$/i.test(value);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isHexStringAtLeast(value: unknown, minimumBytes: number): value is string {
|
|
147
|
+
return typeof value === 'string' && value.length % 2 === 0 && value.length >= minimumBytes * 2 && /^[a-f0-9]+$/i.test(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function toHex(value: Uint8Array): string {
|
|
151
|
+
return Buffer.from(value).toString('hex');
|
|
152
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ed25519, x25519 } from '@noble/curves/ed25519.js';
|
|
2
|
+
|
|
3
|
+
export interface X25519KeyPair {
|
|
4
|
+
publicKey: Uint8Array;
|
|
5
|
+
privateKey: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const X25519_KEY_SIZE = 32;
|
|
9
|
+
|
|
10
|
+
export function generateX25519KeyPair(): X25519KeyPair {
|
|
11
|
+
const privateKey = x25519.utils.randomSecretKey();
|
|
12
|
+
return {
|
|
13
|
+
privateKey,
|
|
14
|
+
publicKey: x25519.getPublicKey(privateKey),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ed25519ToX25519(ed25519PrivateKey: Uint8Array): X25519KeyPair {
|
|
19
|
+
assertKeyLength(ed25519PrivateKey, 'ed25519PrivateKey');
|
|
20
|
+
|
|
21
|
+
const privateKey = ed25519.utils.toMontgomerySecret(new Uint8Array(ed25519PrivateKey));
|
|
22
|
+
return {
|
|
23
|
+
privateKey,
|
|
24
|
+
publicKey: x25519.getPublicKey(privateKey),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function computeSharedSecret(
|
|
29
|
+
myPrivateKey: Uint8Array,
|
|
30
|
+
theirPublicKey: Uint8Array,
|
|
31
|
+
): Uint8Array {
|
|
32
|
+
assertKeyLength(myPrivateKey, 'myPrivateKey');
|
|
33
|
+
assertKeyLength(theirPublicKey, 'theirPublicKey');
|
|
34
|
+
return x25519.getSharedSecret(myPrivateKey, theirPublicKey);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertKeyLength(key: Uint8Array, field: string): void {
|
|
38
|
+
if (key.length !== X25519_KEY_SIZE) {
|
|
39
|
+
throw new Error(`${field} must be ${X25519_KEY_SIZE} bytes, received ${key.length}.`);
|
|
40
|
+
}
|
|
41
|
+
}
|