@hivemind-os/collective-mcp-server 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 (49) hide show
  1. package/.turbo/turbo-build.log +14 -0
  2. package/dist/index.d.ts +493 -0
  3. package/dist/index.js +2129 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +31 -0
  6. package/src/context.ts +58 -0
  7. package/src/encryption.ts +25 -0
  8. package/src/index.ts +41 -0
  9. package/src/resources/agent.ts +77 -0
  10. package/src/resources/capabilities.ts +24 -0
  11. package/src/resources/index.ts +70 -0
  12. package/src/resources/task.ts +58 -0
  13. package/src/resources/wallet.ts +32 -0
  14. package/src/tools/analytics.ts +122 -0
  15. package/src/tools/balance.ts +36 -0
  16. package/src/tools/deactivate.ts +33 -0
  17. package/src/tools/discover.ts +256 -0
  18. package/src/tools/dispute.ts +135 -0
  19. package/src/tools/execute-async.ts +39 -0
  20. package/src/tools/execute.ts +418 -0
  21. package/src/tools/index.ts +152 -0
  22. package/src/tools/indexer-client.ts +163 -0
  23. package/src/tools/marketplace-accept-bid.ts +43 -0
  24. package/src/tools/marketplace-bid.ts +56 -0
  25. package/src/tools/marketplace-browse.ts +66 -0
  26. package/src/tools/marketplace-post.ts +96 -0
  27. package/src/tools/metering.ts +214 -0
  28. package/src/tools/multi-execute.ts +218 -0
  29. package/src/tools/policy-update.ts +94 -0
  30. package/src/tools/register.ts +78 -0
  31. package/src/tools/relay-registry.ts +95 -0
  32. package/src/tools/stake.ts +103 -0
  33. package/src/tools/task-history.ts +86 -0
  34. package/src/tools/task-status.ts +66 -0
  35. package/tests/analytics.test.ts +41 -0
  36. package/tests/auth-errors.test.ts +85 -0
  37. package/tests/balance.test.ts +32 -0
  38. package/tests/context.test.ts +112 -0
  39. package/tests/discover.test.ts +207 -0
  40. package/tests/dispute.test.ts +140 -0
  41. package/tests/execute.test.ts +150 -0
  42. package/tests/marketplace.test.ts +117 -0
  43. package/tests/metering.test.ts +173 -0
  44. package/tests/multi-execute.test.ts +123 -0
  45. package/tests/relay-registry.test.ts +71 -0
  46. package/tests/stake.test.ts +90 -0
  47. package/tsconfig.json +9 -0
  48. package/tsup.config.ts +10 -0
  49. package/vitest.config.ts +8 -0
@@ -0,0 +1,214 @@
1
+ import { decodeMeteredResult, getMeteredResultUnits, parseMeteredResultEnvelope, ResultVerifier } from '@hivemind-os/collective-core';
2
+ import { PaymentRail, PaymentScheme, TaskStatus } from '@hivemind-os/collective-types';
3
+
4
+ import type { MeshToolContext } from '../context.js';
5
+ import { fetchMeshBlob, hexToBytes, supportsEncryptedBlobs } from '../encryption.js';
6
+ import { resolveProviderCapability } from './discover.js';
7
+ import { waitForTaskCompletion } from './execute.js';
8
+
9
+ const encoder = new TextEncoder();
10
+ const decoder = new TextDecoder();
11
+ const DEFAULT_TIMEOUT_SECONDS = 120;
12
+ const DEFAULT_DISPUTE_WINDOW_MS = 5 * 60_000;
13
+ const DEFAULT_EXPIRY_HOURS = 24;
14
+
15
+ export interface MeshMeteredExecuteParams {
16
+ capability: string;
17
+ provider_did?: string;
18
+ input: string;
19
+ max_price_mist: number;
20
+ unit_price_mist: number;
21
+ timeout_seconds?: number;
22
+ }
23
+
24
+ export interface MeshVerifyResultParams {
25
+ task_id: string;
26
+ }
27
+
28
+ export const meshMeteredExecuteTool = {
29
+ name: 'collective_metered_execute',
30
+ description: 'Execute a metered mesh task with capped escrow and result verification',
31
+ inputSchema: {
32
+ type: 'object' as const,
33
+ properties: {
34
+ capability: { type: 'string', description: 'Capability name to execute' },
35
+ provider_did: { type: 'string', description: 'Specific provider DID to use' },
36
+ input: { type: 'string', description: 'Task input payload' },
37
+ max_price_mist: { type: 'number', description: 'Maximum escrow in MIST' },
38
+ unit_price_mist: { type: 'number', description: 'Price per metered unit in MIST' },
39
+ timeout_seconds: { type: 'number', description: 'Polling timeout in seconds (default 120)' },
40
+ },
41
+ required: ['capability', 'input', 'max_price_mist', 'unit_price_mist'],
42
+ },
43
+ };
44
+
45
+ export const meshVerifyResultTool = {
46
+ name: 'collective_verify_result',
47
+ description: 'Verify a metered task result blob against its on-chain hash chain root',
48
+ inputSchema: {
49
+ type: 'object' as const,
50
+ properties: {
51
+ task_id: { type: 'string', description: 'Task object id' },
52
+ },
53
+ required: ['task_id'],
54
+ },
55
+ };
56
+
57
+ export async function runMeshMeteredExecute(
58
+ params: MeshMeteredExecuteParams,
59
+ context: MeshToolContext,
60
+ ): Promise<{
61
+ task_id: string;
62
+ provider_did: string;
63
+ result: string;
64
+ status: string;
65
+ payment_rail: PaymentRail;
66
+ payment_scheme: PaymentScheme.UPTO;
67
+ max_price_mist: string;
68
+ actual_price_mist: string;
69
+ unit_price_mist: string;
70
+ metered_units: number;
71
+ verification_hash: string;
72
+ verified: boolean;
73
+ }> {
74
+ const resolved = await resolveProviderCapability(params.capability, context, params.provider_did);
75
+ if (resolved.capability.pricing.rail !== PaymentRail.SUI_ESCROW) {
76
+ throw new Error(`Capability ${resolved.capability.name} does not support SUI escrow execution.`);
77
+ }
78
+
79
+ const maxPriceMist = toRequiredBigInt(params.max_price_mist, 'max_price_mist');
80
+ const unitPriceMist = toRequiredBigInt(params.unit_price_mist, 'unit_price_mist');
81
+ const spendingDecision = context.spendingPolicy.evaluate({
82
+ amountMist: maxPriceMist,
83
+ rail: PaymentRail.SUI_ESCROW,
84
+ appId: resolved.agent.did,
85
+ originAppName: context.originAppName,
86
+ });
87
+ if (!spendingDecision.approved) {
88
+ throw new Error(spendingDecision.reason ?? 'Spending policy rejected the request.');
89
+ }
90
+
91
+ const inputBlob = await storeTaskInput(context, resolved.agent, encoder.encode(params.input));
92
+ const posted = await context.taskClient.postMeteredTask({
93
+ capability: resolved.capability.name,
94
+ category: 'general',
95
+ inputBlobId: inputBlob.blobId,
96
+ agreementHash: `metered:${resolved.capability.name}`,
97
+ maxPriceMist,
98
+ unitPriceMist,
99
+ disputeWindowMs: DEFAULT_DISPUTE_WINDOW_MS,
100
+ expiryHours: DEFAULT_EXPIRY_HOURS,
101
+ keypair: context.keypair,
102
+ });
103
+ const task = await waitForTaskCompletion(posted.taskId, context, params.timeout_seconds ?? DEFAULT_TIMEOUT_SECONDS);
104
+ const verification = await verifyMeteredTaskResult(task.id, context);
105
+
106
+ await context.taskClient.releaseMeteredPayment({
107
+ taskId: task.id,
108
+ keypair: context.keypair,
109
+ });
110
+ context.spendingPolicy.record({
111
+ amountMist: task.price,
112
+ rail: PaymentRail.SUI_ESCROW,
113
+ taskId: task.id,
114
+ appId: resolved.agent.did,
115
+ originAppName: context.originAppName,
116
+ });
117
+
118
+ return {
119
+ task_id: task.id,
120
+ provider_did: resolved.agent.did,
121
+ result: verification.result,
122
+ status: TaskStatus[TaskStatus.RELEASED],
123
+ payment_rail: PaymentRail.SUI_ESCROW,
124
+ payment_scheme: PaymentScheme.UPTO,
125
+ max_price_mist: (task.maxPrice ?? maxPriceMist).toString(),
126
+ actual_price_mist: task.price.toString(),
127
+ unit_price_mist: (task.unitPrice ?? unitPriceMist).toString(),
128
+ metered_units: task.meteredUnits ?? verification.metered_units,
129
+ verification_hash: verification.verification_hash,
130
+ verified: verification.verified,
131
+ };
132
+ }
133
+
134
+ export async function runMeshVerifyResult(
135
+ params: MeshVerifyResultParams,
136
+ context: MeshToolContext,
137
+ ): Promise<{
138
+ task_id: string;
139
+ verified: boolean;
140
+ verification_hash: string;
141
+ metered_units: number;
142
+ result: string;
143
+ }> {
144
+ return await verifyMeteredTaskResult(params.task_id, context);
145
+ }
146
+
147
+ async function verifyMeteredTaskResult(
148
+ taskId: string,
149
+ context: MeshToolContext,
150
+ ): Promise<{
151
+ task_id: string;
152
+ verified: boolean;
153
+ verification_hash: string;
154
+ metered_units: number;
155
+ result: string;
156
+ }> {
157
+ const task = await context.taskClient.getTask(taskId);
158
+ if (!task) {
159
+ throw new Error(`Task ${taskId} was not found.`);
160
+ }
161
+ if (!task.resultBlobId) {
162
+ throw new Error(`Task ${taskId} does not have a result blob.`);
163
+ }
164
+
165
+ const resultBytes = await fetchMeshBlob(context.blobStore, task.resultBlobId);
166
+ if (!resultBytes) {
167
+ throw new Error(`Result blob ${task.resultBlobId} was not found.`);
168
+ }
169
+
170
+ const envelope = parseMeteredResultEnvelope(resultBytes);
171
+ if (!envelope) {
172
+ throw new Error(`Result blob ${task.resultBlobId} is not a metered result envelope.`);
173
+ }
174
+
175
+ const verifier = new ResultVerifier();
176
+ const verified = verifier.verify(task, envelope.proof, getMeteredResultUnits(envelope));
177
+ return {
178
+ task_id: task.id,
179
+ verified,
180
+ verification_hash: task.verificationHash ?? envelope.proof.root,
181
+ metered_units: task.meteredUnits ?? envelope.proof.unitCount,
182
+ result: decoder.decode(decodeMeteredResult(envelope)),
183
+ };
184
+ }
185
+
186
+ function toRequiredBigInt(value: number, name: string): bigint {
187
+ if (typeof value !== 'number' || Number.isNaN(value) || value < 0) {
188
+ throw new Error(`Invalid ${name}.`);
189
+ }
190
+
191
+ return BigInt(Math.floor(value));
192
+ }
193
+
194
+ async function storeTaskInput(
195
+ context: MeshToolContext,
196
+ provider: Awaited<ReturnType<typeof resolveProviderCapability>>['agent'],
197
+ input: Uint8Array,
198
+ ): Promise<{ blobId: string }> {
199
+ const providerEncryptionKey = hexToBytes(provider.encryptionPublicKey) ?? undefined;
200
+ const encryptionEnabled = context.encryption?.enabled ?? supportsEncryptedBlobs(context.blobStore);
201
+ const requireEncryption = context.encryption?.requireEncryption ?? false;
202
+
203
+ if (encryptionEnabled && providerEncryptionKey) {
204
+ if (!supportsEncryptedBlobs(context.blobStore)) {
205
+ throw new Error('Encryption is enabled, but the configured blobstore does not support encrypted payloads.');
206
+ }
207
+
208
+ return await context.blobStore.storeEncrypted(input, providerEncryptionKey);
209
+ }
210
+ if (requireEncryption) {
211
+ throw new Error(`Provider ${provider.did} does not publish an encryption key.`);
212
+ }
213
+ return await context.blobStore.store(input);
214
+ }
@@ -0,0 +1,218 @@
1
+ import {
2
+ AggregationMode,
3
+ ProviderSelectionStrategy,
4
+ type AgentCard,
5
+ type MultiProviderRequest,
6
+ } from '@hivemind-os/collective-types';
7
+ import {
8
+ CircuitBreaker,
9
+ FanOutExecutor,
10
+ PerformanceTracker,
11
+ ProviderSelector,
12
+ } from '@hivemind-os/collective-core';
13
+
14
+ import type { MeshToolContext } from '../context.js';
15
+ import { discoverAgentsByCapability } from './discover.js';
16
+ import { runMeshExecute } from './execute.js';
17
+
18
+ const routingCircuitBreaker = new CircuitBreaker();
19
+ const routingPerformanceTracker = new PerformanceTracker();
20
+ const providerSelector = new ProviderSelector({
21
+ circuitBreaker: routingCircuitBreaker,
22
+ performanceTracker: routingPerformanceTracker,
23
+ });
24
+
25
+ export interface MeshMultiExecuteParams {
26
+ capability: string;
27
+ input: unknown;
28
+ fanOutCount?: number;
29
+ strategy?: ProviderSelectionStrategy | `${ProviderSelectionStrategy}`;
30
+ aggregation?: AggregationMode | `${AggregationMode}`;
31
+ timeout?: number;
32
+ maxPricePerProvider?: number;
33
+ }
34
+
35
+ export const meshMultiExecuteTool = {
36
+ name: 'collective_multi_execute',
37
+ description: 'Execute a mesh task across multiple providers and aggregate the results',
38
+ inputSchema: {
39
+ type: 'object' as const,
40
+ properties: {
41
+ capability: { type: 'string', description: 'Capability name to execute' },
42
+ input: { description: 'Task input payload' },
43
+ fanOutCount: { type: 'number', description: 'How many providers to fan out to (default 3)' },
44
+ strategy: {
45
+ type: 'string',
46
+ enum: Object.values(ProviderSelectionStrategy),
47
+ description: 'Provider selection strategy (default weighted)',
48
+ },
49
+ aggregation: {
50
+ type: 'string',
51
+ enum: Object.values(AggregationMode),
52
+ description: 'Aggregation mode (default first_success)',
53
+ },
54
+ timeout: { type: 'number', description: 'Per-provider timeout in milliseconds' },
55
+ maxPricePerProvider: { type: 'number', description: 'Maximum price per provider in MIST' },
56
+ },
57
+ required: ['capability', 'input'],
58
+ },
59
+ };
60
+
61
+ export interface MeshMultiExecuteResult {
62
+ capability: string;
63
+ strategy: ProviderSelectionStrategy;
64
+ aggregation: AggregationMode;
65
+ providers: Array<{
66
+ did: string;
67
+ price_mist: string;
68
+ reputation: number;
69
+ estimated_latency_ms?: number;
70
+ composite_score: number;
71
+ }>;
72
+ results: Array<{
73
+ provider: string;
74
+ status: 'success' | 'failure' | 'timeout';
75
+ result?: unknown;
76
+ duration_ms: number;
77
+ error?: string;
78
+ }>;
79
+ aggregated_result?: unknown;
80
+ total_cost_mist: string;
81
+ }
82
+
83
+ export async function runMeshMultiExecute(
84
+ params: MeshMultiExecuteParams,
85
+ context: MeshToolContext,
86
+ ): Promise<MeshMultiExecuteResult> {
87
+ const capability = params.capability.trim();
88
+ if (!capability) {
89
+ throw new Error('Capability is required.');
90
+ }
91
+
92
+ const fanOutCount = normalizePositiveInteger(params.fanOutCount, 3);
93
+ const strategy = parseSelectionStrategy(params.strategy);
94
+ const aggregation = parseAggregation(params.aggregation);
95
+ const request: MultiProviderRequest = {
96
+ capability,
97
+ input: params.input,
98
+ fanOutCount,
99
+ strategy,
100
+ aggregation,
101
+ timeout: normalizeOptionalInteger(params.timeout),
102
+ maxPricePerProvider: toOptionalBigInt(params.maxPricePerProvider),
103
+ };
104
+
105
+ const { agents: discoveredAgents } = await discoverAgentsByCapability(
106
+ capability,
107
+ context,
108
+ Math.max(fanOutCount * 3, fanOutCount),
109
+ 'reputation',
110
+ );
111
+ const candidates = filterByMaxPrice(discoveredAgents, capability, request.maxPricePerProvider);
112
+ const selected = providerSelector.selectProviders(candidates, capability, strategy, fanOutCount);
113
+ if (selected.length === 0) {
114
+ throw new Error(`No eligible providers found for capability ${capability}.`);
115
+ }
116
+
117
+ const executor = new FanOutExecutor({
118
+ circuitBreaker: routingCircuitBreaker,
119
+ performanceTracker: routingPerformanceTracker,
120
+ executeProvider: async (provider, executionRequest, executionContext) => {
121
+ if (executionContext.signal.aborted) {
122
+ throw executionContext.signal.reason ?? new Error('Provider execution aborted.');
123
+ }
124
+
125
+ const value = await runMeshExecute({
126
+ capability: executionRequest.capability,
127
+ provider_did: provider.did,
128
+ input: serializeInput(executionRequest.input),
129
+ timeout_seconds: executionRequest.timeout ? Math.max(1, Math.ceil(executionRequest.timeout / 1_000)) : undefined,
130
+ }, context);
131
+ return {
132
+ value,
133
+ aggregateValue: value.result,
134
+ cost: BigInt(value.price_mist),
135
+ };
136
+ },
137
+ });
138
+
139
+ const result = await executor.execute(request, selected);
140
+
141
+ return {
142
+ capability,
143
+ strategy,
144
+ aggregation,
145
+ providers: selected.map((provider) => ({
146
+ did: provider.did,
147
+ price_mist: provider.price.toString(),
148
+ reputation: provider.reputation,
149
+ estimated_latency_ms: provider.estimatedLatency,
150
+ composite_score: provider.compositeScore,
151
+ })),
152
+ results: result.results.map((entry) => ({
153
+ provider: entry.provider,
154
+ status: entry.status,
155
+ result: entry.result,
156
+ duration_ms: entry.durationMs,
157
+ error: entry.error,
158
+ })),
159
+ aggregated_result: result.aggregatedResult,
160
+ total_cost_mist: result.totalCost.toString(),
161
+ };
162
+ }
163
+
164
+ function filterByMaxPrice(agents: AgentCard[], capability: string, maxPricePerProvider?: bigint): AgentCard[] {
165
+ if (maxPricePerProvider === undefined) {
166
+ return agents;
167
+ }
168
+
169
+ return agents.filter((agent) => {
170
+ const matched = agent.capabilities.find((entry) => entry.name.toLowerCase() === capability.toLowerCase());
171
+ return matched ? matched.pricing.amount <= maxPricePerProvider : false;
172
+ });
173
+ }
174
+
175
+ function parseSelectionStrategy(strategy?: MeshMultiExecuteParams['strategy']): ProviderSelectionStrategy {
176
+ if (!strategy) {
177
+ return ProviderSelectionStrategy.WEIGHTED;
178
+ }
179
+ if (Object.values(ProviderSelectionStrategy).includes(strategy as ProviderSelectionStrategy)) {
180
+ return strategy as ProviderSelectionStrategy;
181
+ }
182
+ throw new Error(`Unsupported provider selection strategy: ${String(strategy)}.`);
183
+ }
184
+
185
+ function parseAggregation(aggregation?: MeshMultiExecuteParams['aggregation']): AggregationMode {
186
+ if (!aggregation) {
187
+ return AggregationMode.FIRST_SUCCESS;
188
+ }
189
+ if (Object.values(AggregationMode).includes(aggregation as AggregationMode)) {
190
+ return aggregation as AggregationMode;
191
+ }
192
+ throw new Error(`Unsupported aggregation mode: ${String(aggregation)}.`);
193
+ }
194
+
195
+ function normalizePositiveInteger(value: number | undefined, fallback: number): number {
196
+ if (typeof value !== 'number' || Number.isNaN(value)) {
197
+ return fallback;
198
+ }
199
+ return Math.max(1, Math.floor(value));
200
+ }
201
+
202
+ function normalizeOptionalInteger(value?: number): number | undefined {
203
+ if (typeof value !== 'number' || Number.isNaN(value)) {
204
+ return undefined;
205
+ }
206
+ return Math.max(1, Math.floor(value));
207
+ }
208
+
209
+ function toOptionalBigInt(value?: number): bigint | undefined {
210
+ if (typeof value !== 'number' || Number.isNaN(value)) {
211
+ return undefined;
212
+ }
213
+ return BigInt(Math.max(0, Math.floor(value)));
214
+ }
215
+
216
+ function serializeInput(input: unknown): string {
217
+ return typeof input === 'string' ? input : JSON.stringify(input);
218
+ }
@@ -0,0 +1,94 @@
1
+ import { PaymentRail, type SpendingPolicy } from '@hivemind-os/collective-types';
2
+
3
+ import type { MeshToolContext } from '../context.js';
4
+
5
+ export interface MeshPolicyUpdateParams {
6
+ daily_limit_mist?: number;
7
+ monthly_limit_mist?: number;
8
+ per_task_limit_mist?: number;
9
+ }
10
+
11
+ export const meshPolicyUpdateTool = {
12
+ name: 'collective_policy_update',
13
+ description: 'Update local spending policy limits',
14
+ inputSchema: {
15
+ type: 'object' as const,
16
+ properties: {
17
+ daily_limit_mist: { type: 'number', description: 'Daily MIST limit' },
18
+ monthly_limit_mist: { type: 'number', description: 'Monthly MIST limit' },
19
+ per_task_limit_mist: { type: 'number', description: 'Per-task MIST limit' },
20
+ },
21
+ required: [],
22
+ },
23
+ };
24
+
25
+ export async function runMeshPolicyUpdate(
26
+ params: MeshPolicyUpdateParams,
27
+ context: MeshToolContext,
28
+ ): Promise<{
29
+ updated: true;
30
+ limits: Array<{ interval: string; amount_mist: string; rail: string }>;
31
+ }> {
32
+ const currentPolicy = clonePolicy(readPolicySnapshot(context));
33
+ const nextLimits = [...currentPolicy.limits];
34
+
35
+ applyLimit(nextLimits, 'transaction', params.per_task_limit_mist);
36
+ applyLimit(nextLimits, 'day', params.daily_limit_mist);
37
+ applyLimit(nextLimits, 'month', params.monthly_limit_mist);
38
+
39
+ const updatedPolicy: SpendingPolicy = {
40
+ ...currentPolicy,
41
+ limits: nextLimits,
42
+ };
43
+
44
+ context.spendingPolicy.updatePolicy(updatedPolicy);
45
+
46
+ return {
47
+ updated: true,
48
+ limits: updatedPolicy.limits.map((limit) => ({
49
+ interval: limit.interval,
50
+ amount_mist: limit.amount.toString(),
51
+ rail: limit.rail ?? PaymentRail.SUI_ESCROW,
52
+ })),
53
+ };
54
+ }
55
+
56
+ function readPolicySnapshot(context: MeshToolContext): SpendingPolicy {
57
+ const engineWithPolicy = context.spendingPolicy as unknown as { policy?: SpendingPolicy };
58
+ return engineWithPolicy.policy ?? { limits: [] };
59
+ }
60
+
61
+ function clonePolicy(policy: SpendingPolicy): SpendingPolicy {
62
+ return {
63
+ ...policy,
64
+ limits: policy.limits.map((limit) => ({ ...limit })),
65
+ allowlist: policy.allowlist ? [...policy.allowlist] : undefined,
66
+ denylist: policy.denylist ? [...policy.denylist] : undefined,
67
+ };
68
+ }
69
+
70
+ function applyLimit(
71
+ limits: SpendingPolicy['limits'],
72
+ interval: 'transaction' | 'day' | 'month',
73
+ amount?: number,
74
+ ): void {
75
+ if (typeof amount !== 'number' || Number.isNaN(amount)) {
76
+ return;
77
+ }
78
+
79
+ const nextLimit = {
80
+ amount: BigInt(Math.max(0, Math.floor(amount))),
81
+ interval,
82
+ rail: PaymentRail.SUI_ESCROW,
83
+ } as const;
84
+ const index = limits.findIndex(
85
+ (entry) => entry.interval === interval && (entry.rail ?? PaymentRail.SUI_ESCROW) === PaymentRail.SUI_ESCROW,
86
+ );
87
+
88
+ if (index >= 0) {
89
+ limits[index] = nextLimit;
90
+ return;
91
+ }
92
+
93
+ limits.push(nextLimit);
94
+ }
@@ -0,0 +1,78 @@
1
+ import { PaymentRail, type Capability } from '@hivemind-os/collective-types';
2
+
3
+ import type { MeshToolContext } from '../context.js';
4
+ import { hexToBytes } from '../encryption.js';
5
+
6
+ export interface MeshRegisterParams {
7
+ name: string;
8
+ description: string;
9
+ capabilities: Array<{
10
+ name: string;
11
+ description: string;
12
+ version: string;
13
+ price_mist: number;
14
+ }>;
15
+ }
16
+
17
+ export const meshRegisterTool = {
18
+ name: 'collective_register',
19
+ description: 'Register the current daemon identity as a provider',
20
+ inputSchema: {
21
+ type: 'object' as const,
22
+ properties: {
23
+ name: { type: 'string', description: 'Display name for the agent' },
24
+ description: { type: 'string', description: 'Agent description' },
25
+ capabilities: {
26
+ type: 'array' as const,
27
+ items: {
28
+ type: 'object' as const,
29
+ properties: {
30
+ name: { type: 'string' },
31
+ description: { type: 'string' },
32
+ version: { type: 'string' },
33
+ price_mist: { type: 'number' },
34
+ },
35
+ required: ['name', 'description', 'version', 'price_mist'],
36
+ },
37
+ },
38
+ },
39
+ required: ['name', 'description', 'capabilities'],
40
+ },
41
+ };
42
+
43
+ export async function runMeshRegister(
44
+ params: MeshRegisterParams,
45
+ context: MeshToolContext,
46
+ ): Promise<{ agent_card_id: string; did: string; tx_digest: string }> {
47
+ const capabilities: Capability[] = params.capabilities.map((entry) => ({
48
+ name: entry.name,
49
+ description: entry.description,
50
+ version: entry.version,
51
+ pricing: {
52
+ rail: PaymentRail.SUI_ESCROW,
53
+ amount: BigInt(Math.max(0, Math.floor(entry.price_mist))),
54
+ currency: 'MIST',
55
+ },
56
+ }));
57
+
58
+ const result = await context.registryClient.registerAgent({
59
+ name: params.name,
60
+ did: context.did,
61
+ description: params.description,
62
+ capabilities,
63
+ endpoint: `mesh://agent/${context.did}`,
64
+ encryptionPublicKey: context.encryption?.enabled ? (hexToBytes(context.encryption.publicKey) ?? undefined) : undefined,
65
+ keypair: context.keypair,
66
+ });
67
+
68
+ const card = await context.registryClient.getAgentCard(result.agentCardId);
69
+ if (card) {
70
+ context.agentCache.upsertAgent(card);
71
+ }
72
+
73
+ return {
74
+ agent_card_id: result.agentCardId,
75
+ did: context.did,
76
+ tx_digest: result.txDigest,
77
+ };
78
+ }
@@ -0,0 +1,95 @@
1
+ import { RelayRegistryClient } from '@hivemind-os/collective-core';
2
+
3
+ import type { MeshToolContext } from '../context.js';
4
+
5
+ export interface MeshRelayRegistryParams {
6
+ action: 'list' | 'register';
7
+ endpoint?: string;
8
+ stake_id?: string;
9
+ region?: string;
10
+ routing_fee_bps?: number;
11
+ capabilities?: string[];
12
+ }
13
+
14
+ export const meshRelayRegistryTool = {
15
+ name: 'collective_relay_registry',
16
+ description: 'List registered community relays or register this node as a relay operator',
17
+ inputSchema: {
18
+ type: 'object' as const,
19
+ properties: {
20
+ action: { type: 'string', enum: ['list', 'register'] },
21
+ endpoint: { type: 'string', description: 'Relay websocket or HTTPS endpoint when action=register' },
22
+ stake_id: { type: 'string', description: 'Relay stake position object id when action=register' },
23
+ region: { type: 'string', description: 'Relay region label when action=register' },
24
+ routing_fee_bps: { type: 'number', description: 'Routing fee in basis points when action=register' },
25
+ capabilities: {
26
+ type: 'array',
27
+ items: { type: 'string' },
28
+ description: 'Optional relay capabilities when action=register (defaults to ["routing"])',
29
+ },
30
+ },
31
+ required: ['action'],
32
+ },
33
+ };
34
+
35
+ export async function runMeshRelayRegistry(
36
+ params: MeshRelayRegistryParams,
37
+ context: MeshToolContext,
38
+ ): Promise<Record<string, unknown>> {
39
+ const client = context.relayRegistryClient ?? new RelayRegistryClient(context.suiClient, context.networkConfig);
40
+
41
+ switch (params.action) {
42
+ case 'list': {
43
+ const relays = await client.listRelays();
44
+ return {
45
+ action: 'list',
46
+ count: relays.length,
47
+ relays,
48
+ };
49
+ }
50
+ case 'register': {
51
+ const endpoint = requireNonEmpty(params.endpoint, 'endpoint');
52
+ const stakeId = requireNonEmpty(params.stake_id, 'stake_id');
53
+ const region = requireNonEmpty(params.region, 'region');
54
+ if (params.routing_fee_bps === undefined) {
55
+ throw new Error('routing_fee_bps is required when action=register');
56
+ }
57
+ if (!Number.isInteger(params.routing_fee_bps) || params.routing_fee_bps < 0 || params.routing_fee_bps > 10_000) {
58
+ throw new Error('routing_fee_bps must be an integer between 0 and 10000');
59
+ }
60
+ const capabilities = normalizeCapabilities(params.capabilities);
61
+ const result = await client.registerRelay({
62
+ endpoint,
63
+ stakeId,
64
+ region,
65
+ routingFeeBps: params.routing_fee_bps,
66
+ capabilities,
67
+ signer: context.keypair as never,
68
+ });
69
+ return {
70
+ action: 'register',
71
+ relay_id: result.relayId,
72
+ tx_digest: result.txDigest,
73
+ endpoint,
74
+ region,
75
+ routing_fee_bps: params.routing_fee_bps,
76
+ capabilities,
77
+ };
78
+ }
79
+ default:
80
+ throw new Error(`Unknown relay registry action: ${String(params.action)}`);
81
+ }
82
+ }
83
+
84
+ function requireNonEmpty(value: string | undefined, field: string): string {
85
+ const normalized = value?.trim();
86
+ if (!normalized) {
87
+ throw new Error(`${field} is required when action=register`);
88
+ }
89
+ return normalized;
90
+ }
91
+
92
+ function normalizeCapabilities(capabilities: string[] | undefined): string[] {
93
+ const normalized = capabilities?.map((capability) => capability.trim()).filter(Boolean) ?? [];
94
+ return normalized.length > 0 ? [...new Set(normalized)] : ['routing'];
95
+ }