@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.
Files changed (145) hide show
  1. package/.test-data/05a845c4-1682-41b9-97e5-65a5263156c0/spending.sqlite +0 -0
  2. package/.test-data/10e18ba5-98f0-42e0-8899-06a09459ae85/agents.sqlite +0 -0
  3. package/.test-data/20464456-23cb-4ff7-8df5-3c129fb95a90/agents.sqlite +0 -0
  4. package/.test-data/2e2ec66c-e8b4-43eb-945f-cca84ed0a0f6/agents.sqlite +0 -0
  5. package/.test-data/2f5ef9b7-01da-4ba0-a6fa-af8063a2567c/agents.sqlite +0 -0
  6. package/.test-data/3ac13cd5-84c9-4e71-8b89-6d3ef4dd819c/agents.sqlite +0 -0
  7. package/.test-data/40b7dd4a-fae0-4a5d-a6a0-64c9048af35e/agents.sqlite +0 -0
  8. package/.test-data/59511a04-42f8-4286-bb1e-733795e08749/agents.sqlite +0 -0
  9. package/.test-data/6778e8c5-6eb5-416d-8aca-9d559cdf83e4/agents.sqlite +0 -0
  10. package/.test-data/7648dc86-df90-4460-8f9c-55cdb481324b/agents.sqlite +0 -0
  11. package/.test-data/77162d98-6b22-41b1-a50f-536fab739f8a/agents.sqlite +0 -0
  12. package/.test-data/798dbdab-cbe7-4edd-8a5b-ae99c285fe17/agents.sqlite +0 -0
  13. package/.test-data/8033d6ac-8b85-454d-b708-00ac695b22f8/agents.sqlite +0 -0
  14. package/.test-data/9155a4f4-cda3-487c-9eed-921c82d7550f/agents.sqlite +0 -0
  15. package/.test-data/9bfeee53-c231-46c3-8c93-2180933f5d50/agents.sqlite +0 -0
  16. package/.test-data/a4c64287-79f6-46e9-847d-2803c63a74fd/agents.sqlite +0 -0
  17. package/.test-data/b8f58952-1ed8-46ff-abd7-21fb86e9457f/agents.sqlite +0 -0
  18. package/.test-data/c3060504-3187-41ed-8532-82332be48b0b/spending.sqlite +0 -0
  19. package/.test-data/cc471629-8006-4fc1-b8a1-399d2df2cc4e/agents.sqlite +0 -0
  20. package/.test-data/dbca3bef-397d-4bbc-bd4c-4f7b14103e04/spending.sqlite +0 -0
  21. package/.test-data/f1283dd1-6602-4de7-a050-16aac7abc288/agents.sqlite +0 -0
  22. package/.turbo/turbo-build.log +14 -0
  23. package/dist/index.d.ts +1675 -0
  24. package/dist/index.js +8006 -0
  25. package/dist/index.js.map +1 -0
  26. package/package.json +41 -0
  27. package/src/auth/device-flow.ts +108 -0
  28. package/src/auth/ed25519-provider.ts +43 -0
  29. package/src/auth/errors.ts +82 -0
  30. package/src/auth/evm-key.ts +55 -0
  31. package/src/auth/index.ts +8 -0
  32. package/src/auth/session-state.ts +25 -0
  33. package/src/auth/session-store.ts +510 -0
  34. package/src/auth/types.ts +81 -0
  35. package/src/auth/zklogin-provider.ts +902 -0
  36. package/src/blobstore/WALRUS_FINDINGS.md +284 -0
  37. package/src/blobstore/encrypted-store.ts +56 -0
  38. package/src/blobstore/fs-store.ts +91 -0
  39. package/src/blobstore/hybrid-store.ts +144 -0
  40. package/src/blobstore/index.ts +5 -0
  41. package/src/blobstore/interface.ts +33 -0
  42. package/src/blobstore/walrus-spike.ts +345 -0
  43. package/src/blobstore/walrus-store.ts +551 -0
  44. package/src/cache/agent-cache.ts +403 -0
  45. package/src/cache/index.ts +1 -0
  46. package/src/crypto/encryption.ts +152 -0
  47. package/src/crypto/index.ts +2 -0
  48. package/src/crypto/x25519.ts +41 -0
  49. package/src/dispute/client.ts +191 -0
  50. package/src/dispute/index.ts +1 -0
  51. package/src/events/index.ts +2 -0
  52. package/src/events/parser.ts +291 -0
  53. package/src/events/subscription.ts +131 -0
  54. package/src/evm/constants.ts +6 -0
  55. package/src/evm/index.ts +2 -0
  56. package/src/evm/wallet.ts +136 -0
  57. package/src/identity/did.ts +36 -0
  58. package/src/identity/index.ts +4 -0
  59. package/src/identity/keypair.ts +199 -0
  60. package/src/identity/signing.ts +28 -0
  61. package/src/index.ts +22 -0
  62. package/src/internal/parsing.ts +416 -0
  63. package/src/marketplace/client.ts +349 -0
  64. package/src/marketplace/index.ts +1 -0
  65. package/src/metering/hash-chain.ts +94 -0
  66. package/src/metering/index.ts +4 -0
  67. package/src/metering/meter.ts +80 -0
  68. package/src/metering/streaming.ts +196 -0
  69. package/src/metering/verification.ts +104 -0
  70. package/src/payment/index.ts +1 -0
  71. package/src/payment/rail-selector.ts +41 -0
  72. package/src/registry/client.ts +328 -0
  73. package/src/registry/index.ts +1 -0
  74. package/src/relay/consumer-client.ts +497 -0
  75. package/src/relay/index.ts +1 -0
  76. package/src/relay-registry/client.ts +295 -0
  77. package/src/relay-registry/discovery.ts +109 -0
  78. package/src/relay-registry/index.ts +2 -0
  79. package/src/reputation/anchor-client.ts +126 -0
  80. package/src/reputation/event-publisher.ts +67 -0
  81. package/src/reputation/index.ts +5 -0
  82. package/src/reputation/merkle.ts +79 -0
  83. package/src/reputation/score-calculator.ts +133 -0
  84. package/src/reputation/serialization.ts +37 -0
  85. package/src/reputation/store.ts +165 -0
  86. package/src/reputation/validation.ts +135 -0
  87. package/src/routing/circuit-breaker.ts +111 -0
  88. package/src/routing/fan-out.ts +266 -0
  89. package/src/routing/index.ts +4 -0
  90. package/src/routing/performance.ts +244 -0
  91. package/src/routing/selector.ts +225 -0
  92. package/src/spending/index.ts +1 -0
  93. package/src/spending/policy.ts +271 -0
  94. package/src/staking/client.ts +319 -0
  95. package/src/staking/index.ts +1 -0
  96. package/src/sui/client.ts +214 -0
  97. package/src/sui/index.ts +2 -0
  98. package/src/sui/tx-helpers.ts +1070 -0
  99. package/src/task/client.ts +215 -0
  100. package/src/task/index.ts +1 -0
  101. package/src/x402/client.ts +295 -0
  102. package/src/x402/index.ts +1 -0
  103. package/tests/auth/device-flow.test.ts +62 -0
  104. package/tests/auth/ed25519-provider.test.ts +24 -0
  105. package/tests/auth/evm-key.test.ts +31 -0
  106. package/tests/auth/session-store.test.ts +201 -0
  107. package/tests/auth/zklogin-provider.test.ts +366 -0
  108. package/tests/blobstore/encrypted-store.test.ts +78 -0
  109. package/tests/blobstore.test.ts +91 -0
  110. package/tests/cache.test.ts +124 -0
  111. package/tests/crypto/encryption.test.ts +70 -0
  112. package/tests/crypto/x25519.test.ts +47 -0
  113. package/tests/dispute/client.test.ts +238 -0
  114. package/tests/events.test.ts +202 -0
  115. package/tests/evm/wallet.test.ts +101 -0
  116. package/tests/hybrid-store.test.ts +121 -0
  117. package/tests/identity.test.ts +161 -0
  118. package/tests/marketplace.test.ts +308 -0
  119. package/tests/metering/hash-chain.test.ts +32 -0
  120. package/tests/metering/meter.test.ts +23 -0
  121. package/tests/metering/streaming.test.ts +52 -0
  122. package/tests/metering/verification.test.ts +27 -0
  123. package/tests/payment/rail-selector.test.ts +95 -0
  124. package/tests/registry.test.ts +183 -0
  125. package/tests/relay-consumer-client.test.ts +119 -0
  126. package/tests/relay-registry/client.test.ts +261 -0
  127. package/tests/reputation/event-publisher.test.ts +70 -0
  128. package/tests/reputation/merkle.test.ts +44 -0
  129. package/tests/reputation/score-calculator.test.ts +104 -0
  130. package/tests/reputation/store.test.ts +94 -0
  131. package/tests/routing/circuit-breaker.test.ts +45 -0
  132. package/tests/routing/fan-out.test.ts +123 -0
  133. package/tests/routing/performance.test.ts +49 -0
  134. package/tests/routing/selector.test.ts +114 -0
  135. package/tests/spending.test.ts +133 -0
  136. package/tests/staking/client.test.ts +286 -0
  137. package/tests/sui-client.test.ts +85 -0
  138. package/tests/task.test.ts +249 -0
  139. package/tests/tx-helpers.test.ts +70 -0
  140. package/tests/walrus-spike.test.ts +100 -0
  141. package/tests/walrus-store.test.ts +196 -0
  142. package/tests/x402/client.test.ts +116 -0
  143. package/tsconfig.json +9 -0
  144. package/tsup.config.ts +11 -0
  145. package/vitest.config.ts +8 -0
@@ -0,0 +1,196 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ import type { StreamingPaymentState } from '@hivemind-os/collective-types';
4
+
5
+ interface StreamRow {
6
+ task_id: string;
7
+ total_paid: bigint | number | string;
8
+ max_budget: bigint | number | string;
9
+ unit_price: bigint | number | string;
10
+ current_unit: number | bigint;
11
+ last_payment_timestamp: number | bigint;
12
+ }
13
+
14
+ interface PaymentRow {
15
+ task_id: string;
16
+ unit_index: number | bigint;
17
+ amount: bigint | number | string;
18
+ timestamp: number | bigint;
19
+ }
20
+
21
+ export interface StreamingPaymentManagerOptions {
22
+ dbPath: string;
23
+ now?: () => number;
24
+ paymentProcessor?: (entry: {
25
+ taskId: string;
26
+ amount: bigint;
27
+ unitIndex: number;
28
+ totalPaid: bigint;
29
+ timestamp: number;
30
+ }) => Promise<void> | void;
31
+ }
32
+
33
+ export class StreamingPaymentManager {
34
+ private readonly db: Database.Database;
35
+ private readonly now: () => number;
36
+
37
+ constructor(private readonly options: StreamingPaymentManagerOptions) {
38
+ this.db = new Database(options.dbPath);
39
+ this.db.defaultSafeIntegers(true);
40
+ this.now = options.now ?? (() => Date.now());
41
+ this.db.exec(`
42
+ CREATE TABLE IF NOT EXISTS streaming_payment_state (
43
+ task_id TEXT PRIMARY KEY,
44
+ total_paid TEXT NOT NULL,
45
+ max_budget TEXT NOT NULL,
46
+ unit_price TEXT NOT NULL,
47
+ current_unit INTEGER NOT NULL,
48
+ last_payment_timestamp INTEGER NOT NULL
49
+ );
50
+ CREATE TABLE IF NOT EXISTS streaming_payment_log (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ task_id TEXT NOT NULL,
53
+ unit_index INTEGER NOT NULL,
54
+ amount TEXT NOT NULL,
55
+ timestamp INTEGER NOT NULL
56
+ );
57
+ `);
58
+ }
59
+
60
+ startStream(taskId: string, maxBudget: bigint, unitPrice: bigint): StreamingPaymentState {
61
+ taskId = normalizeTaskId(taskId);
62
+ if (maxBudget < 0n) {
63
+ throw new Error('maxBudget must be non-negative.');
64
+ }
65
+ if (unitPrice <= 0n) {
66
+ throw new Error('unitPrice must be positive.');
67
+ }
68
+
69
+ const timestamp = this.now();
70
+ this.db
71
+ .prepare(
72
+ `INSERT OR REPLACE INTO streaming_payment_state (
73
+ task_id, total_paid, max_budget, unit_price, current_unit, last_payment_timestamp
74
+ ) VALUES (?, ?, ?, ?, ?, ?)`,
75
+ )
76
+ .run(taskId, '0', maxBudget.toString(), unitPrice.toString(), 0, timestamp);
77
+
78
+ return {
79
+ taskId,
80
+ totalPaid: 0n,
81
+ maxBudget,
82
+ currentUnit: 0,
83
+ lastPaymentTimestamp: timestamp,
84
+ };
85
+ }
86
+
87
+ getState(taskId: string): StreamingPaymentState | null {
88
+ const row = this.db.prepare('SELECT * FROM streaming_payment_state WHERE task_id = ?').get(normalizeTaskId(taskId)) as StreamRow | undefined;
89
+ return row ? mapStreamRow(row) : null;
90
+ }
91
+
92
+ async payUnit(taskId: string): Promise<StreamingPaymentState> {
93
+ taskId = normalizeTaskId(taskId);
94
+ const row = this.requireStreamRow(taskId);
95
+ const state = mapStreamRow(row);
96
+ const unitPrice = toBigInt(row.unit_price);
97
+ const remainingBudget = state.maxBudget - state.totalPaid;
98
+ const amount = remainingBudget < unitPrice ? remainingBudget : unitPrice;
99
+ if (amount <= 0n) {
100
+ return state;
101
+ }
102
+
103
+ const nextState: StreamingPaymentState = {
104
+ taskId: state.taskId,
105
+ totalPaid: state.totalPaid + amount,
106
+ maxBudget: state.maxBudget,
107
+ currentUnit: state.currentUnit + 1,
108
+ lastPaymentTimestamp: this.now(),
109
+ };
110
+
111
+ // v1 streaming uses periodic on-chain transfers recorded locally; it is not a payment channel.
112
+ await this.options.paymentProcessor?.({
113
+ taskId,
114
+ amount,
115
+ unitIndex: nextState.currentUnit,
116
+ totalPaid: nextState.totalPaid,
117
+ timestamp: nextState.lastPaymentTimestamp,
118
+ });
119
+
120
+ this.db.transaction(() => {
121
+ this.db
122
+ .prepare(
123
+ `UPDATE streaming_payment_state
124
+ SET total_paid = ?, current_unit = ?, last_payment_timestamp = ?
125
+ WHERE task_id = ?`,
126
+ )
127
+ .run(nextState.totalPaid.toString(), nextState.currentUnit, nextState.lastPaymentTimestamp, taskId);
128
+ this.db
129
+ .prepare(
130
+ `INSERT INTO streaming_payment_log (task_id, unit_index, amount, timestamp)
131
+ VALUES (?, ?, ?, ?)`,
132
+ )
133
+ .run(taskId, nextState.currentUnit, amount.toString(), nextState.lastPaymentTimestamp);
134
+ })();
135
+
136
+ return nextState;
137
+ }
138
+
139
+ finalizeStream(taskId: string): { state: StreamingPaymentState; refundAmount: bigint } {
140
+ const state = this.getState(normalizeTaskId(taskId));
141
+ if (!state) {
142
+ throw new Error(`Unknown stream for task ${taskId}.`);
143
+ }
144
+
145
+ return {
146
+ state,
147
+ refundAmount: state.maxBudget - state.totalPaid,
148
+ };
149
+ }
150
+
151
+ getAuditTrail(taskId: string): Array<{ taskId: string; unitIndex: number; amount: bigint; timestamp: number }> {
152
+ const rows = this.db
153
+ .prepare('SELECT task_id, unit_index, amount, timestamp FROM streaming_payment_log WHERE task_id = ? ORDER BY id ASC')
154
+ .all(normalizeTaskId(taskId)) as PaymentRow[];
155
+ return rows.map((row) => ({
156
+ taskId: row.task_id,
157
+ unitIndex: Number(row.unit_index),
158
+ amount: toBigInt(row.amount),
159
+ timestamp: Number(row.timestamp),
160
+ }));
161
+ }
162
+
163
+ close(): void {
164
+ this.db.close();
165
+ }
166
+
167
+ private requireStreamRow(taskId: string): StreamRow {
168
+ const row = this.db.prepare('SELECT * FROM streaming_payment_state WHERE task_id = ?').get(normalizeTaskId(taskId)) as StreamRow | undefined;
169
+ if (!row) {
170
+ throw new Error(`Unknown stream for task ${taskId}.`);
171
+ }
172
+ return row;
173
+ }
174
+ }
175
+
176
+ function normalizeTaskId(taskId: string): string {
177
+ const normalized = taskId.trim();
178
+ if (!normalized) {
179
+ throw new Error('taskId is required.');
180
+ }
181
+ return normalized;
182
+ }
183
+
184
+ function mapStreamRow(row: StreamRow): StreamingPaymentState {
185
+ return {
186
+ taskId: row.task_id,
187
+ totalPaid: toBigInt(row.total_paid),
188
+ maxBudget: toBigInt(row.max_budget),
189
+ currentUnit: Number(row.current_unit),
190
+ lastPaymentTimestamp: Number(row.last_payment_timestamp),
191
+ };
192
+ }
193
+
194
+ function toBigInt(value: bigint | number | string): bigint {
195
+ return typeof value === 'bigint' ? value : BigInt(value);
196
+ }
@@ -0,0 +1,104 @@
1
+ import type { HashChainProof, Task } from '@hivemind-os/collective-types';
2
+
3
+ import { DEFAULT_METERING_CHUNK_SIZE, HashChain, createMeteringSeed, splitIntoMeteringUnits } from './hash-chain.js';
4
+
5
+ const decoder = new TextDecoder();
6
+ const METERED_RESULT_SCHEMA = 'agentic-mesh-metered-result/v1';
7
+ const BASE64_RESULT_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
8
+
9
+ export interface MeteredResultEnvelope {
10
+ schema: typeof METERED_RESULT_SCHEMA;
11
+ resultEncoding: 'base64';
12
+ result: string;
13
+ proof: HashChainProof;
14
+ unitChunkSize: number;
15
+ }
16
+
17
+ export class ResultVerifier {
18
+ verify(task: Pick<Task, 'id' | 'verificationHash'>, proof: HashChainProof, expectedData: Uint8Array[]): boolean {
19
+ if (!task.verificationHash) {
20
+ return false;
21
+ }
22
+
23
+ return HashChain.verifyChain(proof, expectedData, createMeteringSeed(task.id))
24
+ && task.verificationHash.toLowerCase() === proof.root.toLowerCase();
25
+ }
26
+ }
27
+
28
+ export function createMeteredResultEnvelope(
29
+ resultData: Uint8Array,
30
+ proof: HashChainProof,
31
+ unitChunkSize = DEFAULT_METERING_CHUNK_SIZE,
32
+ ): MeteredResultEnvelope {
33
+ if (!Number.isSafeInteger(unitChunkSize) || unitChunkSize <= 0) {
34
+ throw new Error('unitChunkSize must be a positive safe integer.');
35
+ }
36
+
37
+ return {
38
+ schema: METERED_RESULT_SCHEMA,
39
+ resultEncoding: 'base64',
40
+ result: Buffer.from(resultData).toString('base64'),
41
+ proof,
42
+ unitChunkSize,
43
+ };
44
+ }
45
+
46
+ export function serializeMeteredResultEnvelope(envelope: MeteredResultEnvelope): Uint8Array {
47
+ return new TextEncoder().encode(JSON.stringify(envelope));
48
+ }
49
+
50
+ export function parseMeteredResultEnvelope(data: Uint8Array): MeteredResultEnvelope | null {
51
+ let parsed: unknown;
52
+ try {
53
+ parsed = JSON.parse(decoder.decode(data)) as unknown;
54
+ } catch {
55
+ return null;
56
+ }
57
+
58
+ if (!isMeteredResultEnvelope(parsed)) {
59
+ return null;
60
+ }
61
+
62
+ return parsed;
63
+ }
64
+
65
+ export function decodeMeteredResult(envelope: MeteredResultEnvelope): Uint8Array {
66
+ return new Uint8Array(Buffer.from(envelope.result, 'base64'));
67
+ }
68
+
69
+ export function getMeteredResultUnits(envelope: MeteredResultEnvelope): Uint8Array[] {
70
+ return splitIntoMeteringUnits(decodeMeteredResult(envelope), envelope.unitChunkSize);
71
+ }
72
+
73
+ function isMeteredResultEnvelope(value: unknown): value is MeteredResultEnvelope {
74
+ if (!value || typeof value !== 'object') {
75
+ return false;
76
+ }
77
+
78
+ const candidate = value as Record<string, unknown>;
79
+ const unitChunkSize = candidate.unitChunkSize;
80
+ return candidate.schema === METERED_RESULT_SCHEMA
81
+ && candidate.resultEncoding === 'base64'
82
+ && typeof candidate.result === 'string'
83
+ && BASE64_RESULT_PATTERN.test(candidate.result)
84
+ && typeof unitChunkSize === 'number'
85
+ && Number.isSafeInteger(unitChunkSize)
86
+ && unitChunkSize > 0
87
+ && isHashChainProof(candidate.proof);
88
+ }
89
+
90
+ function isHashChainProof(value: unknown): value is HashChainProof {
91
+ if (!value || typeof value !== 'object') {
92
+ return false;
93
+ }
94
+
95
+ const candidate = value as Record<string, unknown>;
96
+ const unitCount = candidate.unitCount;
97
+ return typeof candidate.root === 'string'
98
+ && /^[a-f0-9]{64}$/i.test(candidate.root)
99
+ && Array.isArray(candidate.intermediateHashes)
100
+ && candidate.intermediateHashes.every((entry) => typeof entry === 'string' && /^[a-f0-9]{64}$/i.test(entry))
101
+ && typeof unitCount === 'number'
102
+ && Number.isSafeInteger(unitCount)
103
+ && unitCount >= 0;
104
+ }
@@ -0,0 +1 @@
1
+ export * from './rail-selector.js';
@@ -0,0 +1,41 @@
1
+ export type SelectedPaymentRail = 'sui-escrow' | 'sui-transfer' | 'x402-base';
2
+
3
+ export interface RailSelectionContext {
4
+ executionMode: 'sync' | 'async';
5
+ consumerHasSuiWallet: boolean;
6
+ consumerHasEvmWallet: boolean;
7
+ providerAcceptsSui: boolean;
8
+ providerAcceptsX402: boolean;
9
+ amount: bigint;
10
+ currency: string;
11
+ }
12
+
13
+ export class PaymentRailSelector {
14
+ selectRail(context: RailSelectionContext): SelectedPaymentRail {
15
+ const available = this.getAvailableRails(context);
16
+ const selected = available[0];
17
+ if (!selected) {
18
+ throw new Error('No compatible payment rail is available for this task.');
19
+ }
20
+
21
+ return selected;
22
+ }
23
+
24
+ getAvailableRails(context: RailSelectionContext): SelectedPaymentRail[] {
25
+ if (context.executionMode === 'async') {
26
+ return context.consumerHasSuiWallet && context.providerAcceptsSui ? ['sui-escrow'] : [];
27
+ }
28
+
29
+ const rails: SelectedPaymentRail[] = [];
30
+
31
+ if (context.consumerHasSuiWallet && context.providerAcceptsSui) {
32
+ rails.push('sui-transfer');
33
+ }
34
+
35
+ if (context.consumerHasEvmWallet && context.providerAcceptsX402) {
36
+ rails.push('x402-base');
37
+ }
38
+
39
+ return rails;
40
+ }
41
+ }
@@ -0,0 +1,328 @@
1
+ import pino from 'pino';
2
+
3
+ import type { AgentCard, Capability, NetworkConfig, ReputationScore } from '@hivemind-os/collective-types';
4
+ import type { Signer } from '@mysten/sui/cryptography';
5
+
6
+ import { parseAgentCardFields } from '../internal/parsing.js';
7
+ import { parseRawEvent } from '../events/parser.js';
8
+ import { ReputationScoreCalculator } from '../reputation/score-calculator.js';
9
+ import { MeshSuiClient } from '../sui/client.js';
10
+ import { StakingClient } from '../staking/client.js';
11
+ import {
12
+ buildDeactivateAgentTx,
13
+ buildReactivateAgentTx,
14
+ buildRegisterAgentTx,
15
+ buildSetEncryptionKeyTx,
16
+ buildUpdateAgentTx,
17
+ buildUpdateCapabilitiesTx,
18
+ } from '../sui/tx-helpers.js';
19
+
20
+ const logger = pino({ name: '@hivemind-os/collective-core:registry' });
21
+
22
+ export class RegistryClient {
23
+ private readonly scoreCalculator = new ReputationScoreCalculator();
24
+ private readonly stakingClient: StakingClient;
25
+
26
+ constructor(
27
+ private readonly suiClient: MeshSuiClient,
28
+ private readonly config: NetworkConfig,
29
+ ) {
30
+ this.stakingClient = new StakingClient(suiClient, config);
31
+ }
32
+
33
+ async registerAgent(params: {
34
+ name: string;
35
+ did: string;
36
+ description: string;
37
+ capabilities: Capability[];
38
+ endpoint: string;
39
+ encryptionPublicKey?: Uint8Array;
40
+ keypair: Signer;
41
+ }): Promise<{ txDigest: string; agentCardId: string }> {
42
+ const tx = buildRegisterAgentTx({
43
+ packageId: this.config.packageId,
44
+ registryId: this.config.registryId,
45
+ name: params.name,
46
+ did: params.did,
47
+ description: params.description,
48
+ capabilities: params.capabilities,
49
+ endpoint: params.endpoint,
50
+ });
51
+
52
+ const response = await this.suiClient.executeTransaction(tx, params.keypair);
53
+ const agentCardId = extractObjectId(response.objectChanges, /::registry::AgentCard$/);
54
+
55
+ if (!agentCardId) {
56
+ logger.warn({ response }, 'Agent registration succeeded without an AgentCard object change.');
57
+ throw new Error('Unable to determine AgentCard object id from transaction response.');
58
+ }
59
+
60
+ if (params.encryptionPublicKey !== undefined) {
61
+ await this.setEncryptionKey({
62
+ cardId: agentCardId,
63
+ encryptionPublicKey: params.encryptionPublicKey,
64
+ keypair: params.keypair,
65
+ });
66
+ }
67
+
68
+ return { txDigest: response.digest, agentCardId };
69
+ }
70
+
71
+ async updateAgent(params: {
72
+ cardId: string;
73
+ name: string;
74
+ description: string;
75
+ endpoint: string;
76
+ encryptionPublicKey?: Uint8Array;
77
+ keypair: Signer;
78
+ }): Promise<{ txDigest: string }> {
79
+ const tx = buildUpdateAgentTx({
80
+ packageId: this.config.packageId,
81
+ registryId: this.config.registryId,
82
+ cardId: params.cardId,
83
+ name: params.name,
84
+ description: params.description,
85
+ endpoint: params.endpoint,
86
+ });
87
+
88
+ const response = await this.suiClient.executeTransaction(tx, params.keypair);
89
+ if (params.encryptionPublicKey !== undefined) {
90
+ await this.setEncryptionKey({
91
+ cardId: params.cardId,
92
+ encryptionPublicKey: params.encryptionPublicKey,
93
+ keypair: params.keypair,
94
+ });
95
+ }
96
+ return { txDigest: response.digest };
97
+ }
98
+
99
+ async setEncryptionKey(params: {
100
+ cardId: string;
101
+ encryptionPublicKey: Uint8Array;
102
+ keypair: Signer;
103
+ }): Promise<{ txDigest: string }> {
104
+ const tx = buildSetEncryptionKeyTx({
105
+ packageId: this.config.packageId,
106
+ cardId: params.cardId,
107
+ encryptionPublicKey: params.encryptionPublicKey,
108
+ });
109
+
110
+ const response = await this.suiClient.executeTransaction(tx, params.keypair);
111
+ return { txDigest: response.digest };
112
+ }
113
+
114
+ async updateCapabilities(params: {
115
+ cardId: string;
116
+ capabilities: Capability[];
117
+ keypair: Signer;
118
+ }): Promise<{ txDigest: string }> {
119
+ const tx = buildUpdateCapabilitiesTx({
120
+ packageId: this.config.packageId,
121
+ registryId: this.config.registryId,
122
+ cardId: params.cardId,
123
+ capabilities: params.capabilities,
124
+ });
125
+
126
+ const response = await this.suiClient.executeTransaction(tx, params.keypair);
127
+ return { txDigest: response.digest };
128
+ }
129
+
130
+ async deactivateAgent(params: {
131
+ cardId: string;
132
+ keypair: Signer;
133
+ }): Promise<{ txDigest: string }> {
134
+ const tx = buildDeactivateAgentTx({
135
+ packageId: this.config.packageId,
136
+ registryId: this.config.registryId,
137
+ cardId: params.cardId,
138
+ });
139
+
140
+ const response = await this.suiClient.executeTransaction(tx, params.keypair);
141
+ return { txDigest: response.digest };
142
+ }
143
+
144
+ async reactivateAgent(params: {
145
+ cardId: string;
146
+ keypair: Signer;
147
+ }): Promise<{ txDigest: string }> {
148
+ const tx = buildReactivateAgentTx({
149
+ packageId: this.config.packageId,
150
+ registryId: this.config.registryId,
151
+ cardId: params.cardId,
152
+ });
153
+
154
+ const response = await this.suiClient.executeTransaction(tx, params.keypair);
155
+ return { txDigest: response.digest };
156
+ }
157
+
158
+ async getAgentCard(cardId: string): Promise<AgentCard | null> {
159
+ try {
160
+ const object = await this.suiClient.getObject<Record<string, unknown>>(cardId);
161
+ return await this.enrichAgentStake(parseAgentCardFields(object, cardId));
162
+ } catch (error) {
163
+ if (isObjectMissingError(error)) {
164
+ return null;
165
+ }
166
+
167
+ throw error;
168
+ }
169
+ }
170
+
171
+ async getAgentCardByOwner(owner: string): Promise<AgentCard | null> {
172
+ const page = await this.suiClient.client.getOwnedObjects({
173
+ owner,
174
+ filter: { StructType: `${this.config.packageId}::registry::AgentCard` },
175
+ limit: 20,
176
+ });
177
+
178
+ const cards = await Promise.all(
179
+ page.data
180
+ .map((entry) => entry.data?.objectId)
181
+ .filter((objectId): objectId is string => typeof objectId === 'string')
182
+ .map(async (objectId) => await this.getAgentCard(objectId)),
183
+ );
184
+
185
+ return cards
186
+ .filter((card): card is AgentCard => Boolean(card))
187
+ .sort((left, right) => right.updatedAt - left.updatedAt)
188
+ .find((card) => card.active) ?? null;
189
+ }
190
+
191
+ async findAgentByDid(did: string): Promise<AgentCard | null> {
192
+ const eventType = `${this.config.packageId}::registry::AgentRegistered`;
193
+ let cursor = null;
194
+
195
+ do {
196
+ const page = await this.suiClient.queryEvents(eventType, cursor, 100);
197
+ for (const event of page.events) {
198
+ const parsed = parseRawEvent(event, this.config.packageId);
199
+ if (parsed?.type !== 'agent.registered' || parsed.agent.did !== did) {
200
+ continue;
201
+ }
202
+
203
+ return await this.getAgentCard(parsed.agent.id);
204
+ }
205
+
206
+ cursor = page.nextCursor;
207
+ if (!page.hasMore) {
208
+ break;
209
+ }
210
+ } while (cursor);
211
+
212
+ return null;
213
+ }
214
+
215
+ async discoverByCapability(
216
+ capability: string,
217
+ limit = 20,
218
+ options: { sortByReputation?: boolean; scores?: Map<string, ReputationScore> } = {},
219
+ ): Promise<AgentCard[]> {
220
+ const eventType = `${this.config.packageId}::registry::AgentRegistered`;
221
+ const matches: AgentCard[] = [];
222
+ const seen = new Set<string>();
223
+ let cursor = null;
224
+
225
+ do {
226
+ const page = await this.suiClient.queryEvents(eventType, cursor, Math.max(limit * 2, 20));
227
+ for (const event of page.events) {
228
+ const parsed = parseRawEvent(event, this.config.packageId);
229
+ if (parsed?.type !== 'agent.registered') {
230
+ continue;
231
+ }
232
+
233
+ const hasMatch = parsed.agent.capabilities.some(
234
+ (entry) => entry.name.toLowerCase() === capability.toLowerCase(),
235
+ );
236
+ if (!hasMatch) {
237
+ continue;
238
+ }
239
+
240
+ const current = await this.getAgentCard(parsed.agent.id);
241
+ if (current?.active && !seen.has(current.id)) {
242
+ matches.push(current);
243
+ seen.add(current.id);
244
+ }
245
+
246
+ if (!options.sortByReputation && matches.length >= limit) {
247
+ return matches;
248
+ }
249
+ }
250
+
251
+ cursor = page.nextCursor;
252
+ if (!page.hasMore) {
253
+ break;
254
+ }
255
+ } while (cursor);
256
+
257
+ if (!options.sortByReputation) {
258
+ return [...matches].sort(compareStakePreference).slice(0, limit);
259
+ }
260
+
261
+ const scores = options.scores ?? new Map(matches.map((agent) => [agent.did, this.scoreCalculator.computeScore(agent, [])]));
262
+ return this.scoreCalculator.rankByReputation(matches, scores).slice(0, limit);
263
+ }
264
+
265
+ private async enrichAgentStake(agent: AgentCard): Promise<AgentCard> {
266
+ try {
267
+ const stake = await this.stakingClient.getStakeByOwner(agent.owner);
268
+ if (!stake) {
269
+ return { ...agent, hasStake: false, stakeMist: undefined, stakeType: undefined };
270
+ }
271
+
272
+ return {
273
+ ...agent,
274
+ hasStake: Boolean(stake.isActive && stake.meetsMinium),
275
+ stakeMist: stake.balanceMist,
276
+ stakeType: stake.stakeType,
277
+ };
278
+ } catch {
279
+ return { ...agent, hasStake: false, stakeMist: undefined, stakeType: undefined };
280
+ }
281
+ }
282
+ }
283
+
284
+ function extractObjectId(
285
+ objectChanges: Array<Record<string, unknown>> | null | undefined,
286
+ objectTypePattern: RegExp,
287
+ ): string | undefined {
288
+ return objectChanges?.find(
289
+ (change) =>
290
+ (change.type === 'created' || change.type === 'transferred' || change.type === 'mutated') &&
291
+ typeof change.objectType === 'string' &&
292
+ objectTypePattern.test(change.objectType) &&
293
+ typeof change.objectId === 'string',
294
+ )?.objectId as string | undefined;
295
+ }
296
+
297
+ function isObjectMissingError(error: unknown): boolean {
298
+ return error instanceof Error && /not found|does not contain move object data/i.test(error.message);
299
+ }
300
+
301
+ function compareStakePreference(left: AgentCard, right: AgentCard): number {
302
+ return (
303
+ compareBoolean(left.hasStake ?? false, right.hasStake ?? false) ||
304
+ compareBigInt(left.stakeMist ?? 0n, right.stakeMist ?? 0n) ||
305
+ compareNumber(left.updatedAt, right.updatedAt)
306
+ );
307
+ }
308
+
309
+ function compareBoolean(left: boolean, right: boolean): number {
310
+ if (left === right) {
311
+ return 0;
312
+ }
313
+ return left ? -1 : 1;
314
+ }
315
+
316
+ function compareBigInt(left: bigint, right: bigint): number {
317
+ if (left === right) {
318
+ return 0;
319
+ }
320
+ return left > right ? -1 : 1;
321
+ }
322
+
323
+ function compareNumber(left: number, right: number): number {
324
+ if (left === right) {
325
+ return 0;
326
+ }
327
+ return left > right ? -1 : 1;
328
+ }
@@ -0,0 +1 @@
1
+ export * from './client.js';