@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.
- package/.turbo/turbo-build.log +14 -0
- package/dist/index.d.ts +493 -0
- package/dist/index.js +2129 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
- package/src/context.ts +58 -0
- package/src/encryption.ts +25 -0
- package/src/index.ts +41 -0
- package/src/resources/agent.ts +77 -0
- package/src/resources/capabilities.ts +24 -0
- package/src/resources/index.ts +70 -0
- package/src/resources/task.ts +58 -0
- package/src/resources/wallet.ts +32 -0
- package/src/tools/analytics.ts +122 -0
- package/src/tools/balance.ts +36 -0
- package/src/tools/deactivate.ts +33 -0
- package/src/tools/discover.ts +256 -0
- package/src/tools/dispute.ts +135 -0
- package/src/tools/execute-async.ts +39 -0
- package/src/tools/execute.ts +418 -0
- package/src/tools/index.ts +152 -0
- package/src/tools/indexer-client.ts +163 -0
- package/src/tools/marketplace-accept-bid.ts +43 -0
- package/src/tools/marketplace-bid.ts +56 -0
- package/src/tools/marketplace-browse.ts +66 -0
- package/src/tools/marketplace-post.ts +96 -0
- package/src/tools/metering.ts +214 -0
- package/src/tools/multi-execute.ts +218 -0
- package/src/tools/policy-update.ts +94 -0
- package/src/tools/register.ts +78 -0
- package/src/tools/relay-registry.ts +95 -0
- package/src/tools/stake.ts +103 -0
- package/src/tools/task-history.ts +86 -0
- package/src/tools/task-status.ts +66 -0
- package/tests/analytics.test.ts +41 -0
- package/tests/auth-errors.test.ts +85 -0
- package/tests/balance.test.ts +32 -0
- package/tests/context.test.ts +112 -0
- package/tests/discover.test.ts +207 -0
- package/tests/dispute.test.ts +140 -0
- package/tests/execute.test.ts +150 -0
- package/tests/marketplace.test.ts +117 -0
- package/tests/metering.test.ts +173 -0
- package/tests/multi-execute.test.ts +123 -0
- package/tests/relay-registry.test.ts +71 -0
- package/tests/stake.test.ts +90 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { DisputeClient } from '@hivemind-os/collective-core';
|
|
2
|
+
|
|
3
|
+
import type { MeshToolContext } from '../context.js';
|
|
4
|
+
|
|
5
|
+
export interface MeshDisputeParams {
|
|
6
|
+
action: 'open' | 'respond' | 'accept' | 'status';
|
|
7
|
+
task_id?: string;
|
|
8
|
+
dispute_id?: string;
|
|
9
|
+
evidence?: string;
|
|
10
|
+
evidence_blob_id?: string;
|
|
11
|
+
proposed_split_mist?: string | number;
|
|
12
|
+
arbitrator_address?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const meshDisputeTool = {
|
|
16
|
+
name: 'collective_dispute',
|
|
17
|
+
description: 'Open, respond to, accept, or inspect on-chain task disputes',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: 'object' as const,
|
|
20
|
+
properties: {
|
|
21
|
+
action: { type: 'string', enum: ['open', 'respond', 'accept', 'status'] },
|
|
22
|
+
task_id: { type: 'string', description: 'Task object id' },
|
|
23
|
+
dispute_id: { type: 'string', description: 'Dispute object id' },
|
|
24
|
+
evidence: { type: 'string', description: 'Plain text or JSON-encoded evidence to store before opening/responding' },
|
|
25
|
+
evidence_blob_id: { type: 'string', description: 'Existing Walrus/blobstore blob id containing dispute evidence' },
|
|
26
|
+
proposed_split_mist: { type: 'string', description: 'Amount, in MIST, to allocate back to the requester' },
|
|
27
|
+
arbitrator_address: { type: 'string', description: 'Optional arbitrator address for open actions' },
|
|
28
|
+
},
|
|
29
|
+
required: ['action'],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export async function runMeshDispute(params: MeshDisputeParams, context: MeshToolContext): Promise<Record<string, unknown>> {
|
|
34
|
+
const client = context.disputeClient ?? new DisputeClient(context.suiClient, context.networkConfig);
|
|
35
|
+
|
|
36
|
+
switch (params.action) {
|
|
37
|
+
case 'open': {
|
|
38
|
+
if (!params.task_id) {
|
|
39
|
+
throw new Error('task_id is required when action=open');
|
|
40
|
+
}
|
|
41
|
+
const evidenceBlobId = await resolveEvidenceBlobId(params, context);
|
|
42
|
+
const result = await client.openDispute({
|
|
43
|
+
taskId: params.task_id,
|
|
44
|
+
evidenceBlobId,
|
|
45
|
+
proposedSplitMist: parseMist(params.proposed_split_mist, 'proposed_split_mist'),
|
|
46
|
+
arbitratorAddress: params.arbitrator_address,
|
|
47
|
+
signer: context.keypair as never,
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
action: 'open',
|
|
51
|
+
dispute_id: result.disputeId,
|
|
52
|
+
task_id: params.task_id,
|
|
53
|
+
evidence_blob_id: evidenceBlobId,
|
|
54
|
+
tx_digest: result.txDigest,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
case 'respond': {
|
|
58
|
+
if (!params.dispute_id) {
|
|
59
|
+
throw new Error('dispute_id is required when action=respond');
|
|
60
|
+
}
|
|
61
|
+
const evidenceBlobId = await resolveEvidenceBlobId(params, context);
|
|
62
|
+
const result = await client.respondToDispute({
|
|
63
|
+
disputeId: params.dispute_id,
|
|
64
|
+
evidenceBlobId,
|
|
65
|
+
proposedSplitMist: parseMist(params.proposed_split_mist, 'proposed_split_mist'),
|
|
66
|
+
signer: context.keypair as never,
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
action: 'respond',
|
|
70
|
+
dispute_id: params.dispute_id,
|
|
71
|
+
evidence_blob_id: evidenceBlobId,
|
|
72
|
+
tx_digest: result.txDigest,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
case 'accept': {
|
|
76
|
+
if (!params.dispute_id || !params.task_id) {
|
|
77
|
+
throw new Error('dispute_id and task_id are required when action=accept');
|
|
78
|
+
}
|
|
79
|
+
const result = await client.acceptResolution({
|
|
80
|
+
disputeId: params.dispute_id,
|
|
81
|
+
taskId: params.task_id,
|
|
82
|
+
signer: context.keypair as never,
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
action: 'accept',
|
|
86
|
+
dispute_id: params.dispute_id,
|
|
87
|
+
task_id: params.task_id,
|
|
88
|
+
requester_amount_mist: result.requesterAmount.toString(),
|
|
89
|
+
provider_amount_mist: result.providerAmount.toString(),
|
|
90
|
+
tx_digest: result.txDigest,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
case 'status': {
|
|
94
|
+
if ((params.dispute_id ? 1 : 0) + (params.task_id ? 1 : 0) !== 1) {
|
|
95
|
+
throw new Error('Provide exactly one of dispute_id or task_id when action=status');
|
|
96
|
+
}
|
|
97
|
+
const dispute = params.dispute_id
|
|
98
|
+
? await client.getDispute(params.dispute_id)
|
|
99
|
+
: await client.getDisputeByTask(params.task_id as string);
|
|
100
|
+
if (!dispute) {
|
|
101
|
+
throw new Error(params.task_id ? `No dispute found for task ${params.task_id}.` : `Dispute ${params.dispute_id ?? ''} was not found.`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
action: 'status',
|
|
105
|
+
dispute,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
default:
|
|
109
|
+
throw new Error(`Unknown dispute action: ${String(params.action)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function resolveEvidenceBlobId(params: MeshDisputeParams, context: MeshToolContext): Promise<string> {
|
|
114
|
+
if ((params.evidence_blob_id ? 1 : 0) + (params.evidence ? 1 : 0) !== 1) {
|
|
115
|
+
throw new Error('Provide exactly one of evidence or evidence_blob_id for dispute open/respond actions');
|
|
116
|
+
}
|
|
117
|
+
if (params.evidence_blob_id) {
|
|
118
|
+
return params.evidence_blob_id;
|
|
119
|
+
}
|
|
120
|
+
if (!params.evidence) {
|
|
121
|
+
throw new Error('evidence or evidence_blob_id is required for dispute open/respond actions');
|
|
122
|
+
}
|
|
123
|
+
const stored = await context.blobStore.store(new TextEncoder().encode(params.evidence));
|
|
124
|
+
return stored.blobId;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseMist(value: string | number | undefined, field: string): bigint {
|
|
128
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
|
|
129
|
+
return BigInt(value);
|
|
130
|
+
}
|
|
131
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim())) {
|
|
132
|
+
return BigInt(value.trim());
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`${field} must be a non-negative integer string.`);
|
|
135
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MeshToolContext } from '../context.js';
|
|
2
|
+
import { prepareMeshExecution, type MeshExecuteParams } from './execute.js';
|
|
3
|
+
|
|
4
|
+
export interface MeshExecuteAsyncParams
|
|
5
|
+
extends Pick<MeshExecuteParams, 'capability' | 'provider_did' | 'input' | 'max_price_mist'> {}
|
|
6
|
+
|
|
7
|
+
export const meshExecuteAsyncTool = {
|
|
8
|
+
name: 'collective_execute_async',
|
|
9
|
+
description: 'Submit a mesh task and return immediately',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object' as const,
|
|
12
|
+
properties: {
|
|
13
|
+
capability: { type: 'string', description: 'Capability name to execute' },
|
|
14
|
+
provider_did: { type: 'string', description: 'Specific provider DID to use' },
|
|
15
|
+
input: { type: 'string', description: 'Task input payload' },
|
|
16
|
+
max_price_mist: { type: 'number', description: 'Maximum spend in MIST' },
|
|
17
|
+
},
|
|
18
|
+
required: ['capability', 'input'],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function runMeshExecuteAsync(
|
|
23
|
+
params: MeshExecuteAsyncParams,
|
|
24
|
+
context: MeshToolContext,
|
|
25
|
+
): Promise<{
|
|
26
|
+
task_id: string;
|
|
27
|
+
provider_did: string;
|
|
28
|
+
price_mist: string;
|
|
29
|
+
status: 'OPEN';
|
|
30
|
+
}> {
|
|
31
|
+
const prepared = await prepareMeshExecution(params, context);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
task_id: prepared.taskId,
|
|
35
|
+
provider_did: prepared.providerDid,
|
|
36
|
+
price_mist: prepared.priceMist.toString(),
|
|
37
|
+
status: 'OPEN',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { PaymentRail, TaskStatus, type AgentCard, type Capability, type Task } from '@hivemind-os/collective-types';
|
|
2
|
+
import { PaymentRailSelector, RelayConsumerClient, ReputationEventPublisher, type SelectedPaymentRail } from '@hivemind-os/collective-core';
|
|
3
|
+
|
|
4
|
+
import type { MeshToolContext } from '../context.js';
|
|
5
|
+
import { fetchMeshBlob, hexToBytes, supportsEncryptedBlobs } from '../encryption.js';
|
|
6
|
+
import { resolveProviderCapability } from './discover.js';
|
|
7
|
+
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
const DEFAULT_TIMEOUT_SECONDS = 120;
|
|
11
|
+
const DEFAULT_DISPUTE_WINDOW_MS = 5 * 60_000;
|
|
12
|
+
const DEFAULT_EXPIRY_HOURS = 24;
|
|
13
|
+
|
|
14
|
+
export interface MeshExecuteParams {
|
|
15
|
+
capability: string;
|
|
16
|
+
provider_did?: string;
|
|
17
|
+
input: string;
|
|
18
|
+
max_price_mist?: number;
|
|
19
|
+
timeout_seconds?: number;
|
|
20
|
+
mode?: 'auto' | 'sync' | 'async';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const meshExecuteTool = {
|
|
24
|
+
name: 'collective_execute',
|
|
25
|
+
description: 'Execute a mesh task. Returns a task handle (async) if the client supports MCP Tasks, otherwise blocks until completion.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object' as const,
|
|
28
|
+
properties: {
|
|
29
|
+
capability: { type: 'string', description: 'Capability name to execute' },
|
|
30
|
+
provider_did: { type: 'string', description: 'Specific provider DID to use' },
|
|
31
|
+
input: { type: 'string', description: 'Task input payload' },
|
|
32
|
+
max_price_mist: { type: 'number', description: 'Maximum spend in MIST' },
|
|
33
|
+
timeout_seconds: { type: 'number', description: 'Polling timeout in seconds (default 120)' },
|
|
34
|
+
mode: { type: 'string', enum: ['auto', 'sync', 'async'], description: 'Execution preference (default auto)' },
|
|
35
|
+
},
|
|
36
|
+
required: ['capability', 'input'],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export async function runMeshExecute(
|
|
41
|
+
params: MeshExecuteParams,
|
|
42
|
+
context: MeshToolContext,
|
|
43
|
+
): Promise<{
|
|
44
|
+
task_id: string;
|
|
45
|
+
result: string;
|
|
46
|
+
provider_did: string;
|
|
47
|
+
price_mist: string;
|
|
48
|
+
status: string;
|
|
49
|
+
execution_mode: 'sync' | 'async';
|
|
50
|
+
payment_rail: PaymentRail;
|
|
51
|
+
payment_receipt?: string;
|
|
52
|
+
latency_ms?: number;
|
|
53
|
+
}> {
|
|
54
|
+
const resolved = await resolveProviderCapability(params.capability, context, params.provider_did);
|
|
55
|
+
const priceMist = resolved.capability.pricing.amount;
|
|
56
|
+
const maxPrice = toOptionalBigInt(params.max_price_mist);
|
|
57
|
+
|
|
58
|
+
if (maxPrice !== undefined && priceMist > maxPrice) {
|
|
59
|
+
throw new Error(`Provider price ${priceMist.toString()} exceeds max_price_mist ${maxPrice.toString()}.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const mode = params.mode ?? 'auto';
|
|
63
|
+
if (mode !== 'async') {
|
|
64
|
+
const relayResult = await tryRelayExecution(params, context, resolved, priceMist);
|
|
65
|
+
if (relayResult) {
|
|
66
|
+
await publishSuccessfulReputationEvent(context, {
|
|
67
|
+
providerDid: relayResult.provider_did,
|
|
68
|
+
taskId: relayResult.task_id,
|
|
69
|
+
capability: resolved.capability.name,
|
|
70
|
+
priceMist,
|
|
71
|
+
latencyMs: relayResult.latency_ms,
|
|
72
|
+
});
|
|
73
|
+
return relayResult;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const prepared = await prepareAsyncExecution(params, context, resolved, priceMist);
|
|
78
|
+
const task = await waitForTaskCompletion(prepared.taskId, context, params.timeout_seconds);
|
|
79
|
+
|
|
80
|
+
if (!task.resultBlobId) {
|
|
81
|
+
throw new Error(`Task ${prepared.taskId} completed without a result blob.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resultBytes = await fetchMeshBlob(context.blobStore, task.resultBlobId);
|
|
85
|
+
if (!resultBytes) {
|
|
86
|
+
throw new Error(`Result blob ${task.resultBlobId} was not found.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await context.taskClient.releasePayment({
|
|
90
|
+
taskId: prepared.taskId,
|
|
91
|
+
keypair: context.keypair,
|
|
92
|
+
});
|
|
93
|
+
context.spendingPolicy.record({
|
|
94
|
+
amountMist: prepared.priceMist,
|
|
95
|
+
rail: prepared.rail,
|
|
96
|
+
taskId: prepared.taskId,
|
|
97
|
+
appId: prepared.providerDid,
|
|
98
|
+
originAppName: context.originAppName,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await publishSuccessfulReputationEvent(context, {
|
|
102
|
+
providerDid: prepared.providerDid,
|
|
103
|
+
taskId: prepared.taskId,
|
|
104
|
+
capability: resolved.capability.name,
|
|
105
|
+
priceMist: prepared.priceMist,
|
|
106
|
+
latencyMs: task.acceptedAt && task.completedAt ? task.completedAt - task.acceptedAt : undefined,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
task_id: prepared.taskId,
|
|
111
|
+
result: decoder.decode(resultBytes),
|
|
112
|
+
provider_did: prepared.providerDid,
|
|
113
|
+
price_mist: prepared.priceMist.toString(),
|
|
114
|
+
status: TaskStatus[TaskStatus.RELEASED],
|
|
115
|
+
execution_mode: 'async',
|
|
116
|
+
payment_rail: prepared.rail,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function prepareMeshExecution(
|
|
121
|
+
params: Pick<MeshExecuteParams, 'capability' | 'provider_did' | 'input' | 'max_price_mist'>,
|
|
122
|
+
context: MeshToolContext,
|
|
123
|
+
): Promise<{
|
|
124
|
+
taskId: string;
|
|
125
|
+
providerDid: string;
|
|
126
|
+
priceMist: bigint;
|
|
127
|
+
rail: PaymentRail;
|
|
128
|
+
}> {
|
|
129
|
+
const resolved = await resolveProviderCapability(params.capability, context, params.provider_did);
|
|
130
|
+
return prepareAsyncExecution(params, context, resolved, resolved.capability.pricing.amount);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function prepareAsyncExecution(
|
|
134
|
+
params: Pick<MeshExecuteParams, 'input' | 'max_price_mist'>,
|
|
135
|
+
context: MeshToolContext,
|
|
136
|
+
resolved: Awaited<ReturnType<typeof resolveProviderCapability>>,
|
|
137
|
+
priceMist: bigint,
|
|
138
|
+
): Promise<{
|
|
139
|
+
taskId: string;
|
|
140
|
+
providerDid: string;
|
|
141
|
+
priceMist: bigint;
|
|
142
|
+
rail: PaymentRail;
|
|
143
|
+
}> {
|
|
144
|
+
if (resolved.capability.pricing.rail !== PaymentRail.SUI_ESCROW) {
|
|
145
|
+
throw new Error(`Capability ${resolved.capability.name} does not support SUI escrow execution.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
approveSpend(context, priceMist, resolved.capability.pricing.rail, resolved.agent.did);
|
|
149
|
+
|
|
150
|
+
const { blobId } = await storeTaskInput(context, resolved.agent, encoder.encode(params.input));
|
|
151
|
+
const posted = await context.taskClient.postTask({
|
|
152
|
+
capability: resolved.capability.name,
|
|
153
|
+
category: 'general',
|
|
154
|
+
inputBlobId: blobId,
|
|
155
|
+
priceMist,
|
|
156
|
+
disputeWindowMs: DEFAULT_DISPUTE_WINDOW_MS,
|
|
157
|
+
expiryHours: DEFAULT_EXPIRY_HOURS,
|
|
158
|
+
keypair: context.keypair,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
taskId: posted.taskId,
|
|
163
|
+
providerDid: resolved.agent.did,
|
|
164
|
+
priceMist,
|
|
165
|
+
rail: resolved.capability.pricing.rail,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function tryRelayExecution(
|
|
170
|
+
params: MeshExecuteParams,
|
|
171
|
+
context: MeshToolContext,
|
|
172
|
+
resolved: Awaited<ReturnType<typeof resolveProviderCapability>>,
|
|
173
|
+
priceMist: bigint,
|
|
174
|
+
) {
|
|
175
|
+
const relayUrl = getRelayUrl(resolved.agent);
|
|
176
|
+
if (!relayUrl || !context.relayAuthProvider) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const selector = context.paymentRailSelector ?? new PaymentRailSelector();
|
|
182
|
+
const selectedRail = selector.selectRail({
|
|
183
|
+
executionMode: 'sync',
|
|
184
|
+
consumerHasSuiWallet: Boolean(context.relayAuthProvider),
|
|
185
|
+
consumerHasEvmWallet: Boolean(context.x402Client),
|
|
186
|
+
providerAcceptsSui: providerAcceptsSui(resolved.capability),
|
|
187
|
+
providerAcceptsX402: providerAcceptsX402(resolved.agent, resolved.capability),
|
|
188
|
+
amount: priceMist,
|
|
189
|
+
currency: resolved.capability.pricing.currency,
|
|
190
|
+
});
|
|
191
|
+
const rail = toPaymentRail(selectedRail);
|
|
192
|
+
|
|
193
|
+
approveSpend(context, priceMist, rail, resolved.agent.did);
|
|
194
|
+
|
|
195
|
+
const client = new RelayConsumerClient(context.x402Client ?? null, context.relayAuthProvider, { relayUrl });
|
|
196
|
+
const response = await client.executeSync({
|
|
197
|
+
providerDid: resolved.agent.did,
|
|
198
|
+
capability: resolved.capability.name,
|
|
199
|
+
input: params.input,
|
|
200
|
+
paymentRail: rail,
|
|
201
|
+
timeoutMs: normalizeTimeoutMs(params.timeout_seconds),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const taskId = response.taskId ?? `relay-${resolved.agent.did}-${Date.now()}`;
|
|
205
|
+
context.spendingPolicy.record({
|
|
206
|
+
amountMist: priceMist,
|
|
207
|
+
rail,
|
|
208
|
+
taskId,
|
|
209
|
+
appId: resolved.agent.did,
|
|
210
|
+
originAppName: context.originAppName,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
task_id: taskId,
|
|
215
|
+
result: stringifyRelayResult(response.result),
|
|
216
|
+
provider_did: response.providerDid ?? resolved.agent.did,
|
|
217
|
+
price_mist: priceMist.toString(),
|
|
218
|
+
status: 'COMPLETED',
|
|
219
|
+
execution_mode: 'sync' as const,
|
|
220
|
+
payment_rail: rail,
|
|
221
|
+
payment_receipt: response.paymentReceipt,
|
|
222
|
+
latency_ms: response.latencyMs,
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (!shouldFallbackToAsync(error)) {
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
context.logger?.warn?.(
|
|
230
|
+
{ err: error, providerDid: resolved.agent.did, capability: resolved.capability.name },
|
|
231
|
+
'Relay execution unavailable; falling back to async Sui flow.',
|
|
232
|
+
);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function waitForTaskCompletion(
|
|
238
|
+
taskId: string,
|
|
239
|
+
context: MeshToolContext,
|
|
240
|
+
timeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
|
|
241
|
+
): Promise<Task> {
|
|
242
|
+
const timeoutMs = normalizeTimeoutMs(timeoutSeconds);
|
|
243
|
+
const startedAt = Date.now();
|
|
244
|
+
|
|
245
|
+
while (true) {
|
|
246
|
+
const task = await context.taskClient.getTask(taskId);
|
|
247
|
+
if (!task) {
|
|
248
|
+
throw new Error(`Task ${taskId} was not found.`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (task.status === TaskStatus.COMPLETED || task.status === TaskStatus.RELEASED) {
|
|
252
|
+
return task;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (task.status === TaskStatus.CANCELLED || task.status === TaskStatus.DISPUTED) {
|
|
256
|
+
throw new Error(`Task ${taskId} ended with status ${taskStatusLabel(task.status)}.`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const elapsedMs = Date.now() - startedAt;
|
|
260
|
+
if (elapsedMs >= timeoutMs) {
|
|
261
|
+
throw new Error(`Timed out waiting for task ${taskId} to complete.`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const remainingMs = timeoutMs - elapsedMs;
|
|
265
|
+
await delay(Math.min(remainingMs, 1_000));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function approveSpend(context: MeshToolContext, amountMist: bigint, rail: PaymentRail, appId: string): void {
|
|
270
|
+
const decision = context.spendingPolicy.evaluate({
|
|
271
|
+
amountMist,
|
|
272
|
+
rail,
|
|
273
|
+
appId,
|
|
274
|
+
originAppName: context.originAppName,
|
|
275
|
+
});
|
|
276
|
+
if (!decision.approved) {
|
|
277
|
+
throw new Error(decision.reason ?? 'Spending policy rejected the request.');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function getRelayUrl(agent: AgentCard): string | undefined {
|
|
282
|
+
const relayEndpoint = agent.relayEndpoints?.find((endpoint) => !endpoint.modes || endpoint.modes.includes('sync'));
|
|
283
|
+
if (relayEndpoint?.endpoint) {
|
|
284
|
+
return relayEndpoint.endpoint;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (agent.endpoint && /^(https?|wss?):\/\//i.test(agent.endpoint)) {
|
|
288
|
+
return agent.endpoint;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function providerAcceptsSui(capability: Capability): boolean {
|
|
295
|
+
return capability.paymentRails?.includes(PaymentRail.SUI_TRANSFER) ?? capability.pricing.rail !== PaymentRail.X402_BASE;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function providerAcceptsX402(agent: AgentCard, capability: Capability): boolean {
|
|
299
|
+
return capability.paymentRails?.includes(PaymentRail.X402_BASE) ?? Boolean(getRelayUrl(agent));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function toPaymentRail(selectedRail: SelectedPaymentRail): PaymentRail {
|
|
303
|
+
switch (selectedRail) {
|
|
304
|
+
case 'sui-escrow':
|
|
305
|
+
return PaymentRail.SUI_ESCROW;
|
|
306
|
+
case 'sui-transfer':
|
|
307
|
+
return PaymentRail.SUI_TRANSFER;
|
|
308
|
+
case 'x402-base':
|
|
309
|
+
return PaymentRail.X402_BASE;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function shouldFallbackToAsync(error: unknown): boolean {
|
|
314
|
+
if (!(error instanceof Error)) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return /relay request timed out|fetch failed|provider .* is not connected to the relay|provider not found|payment challenge required|relay execution unavailable/i.test(
|
|
319
|
+
error.message,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function stringifyRelayResult(result: unknown): string {
|
|
324
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function normalizeTimeoutMs(timeoutSeconds?: number): number {
|
|
328
|
+
if (typeof timeoutSeconds !== 'number' || Number.isNaN(timeoutSeconds)) {
|
|
329
|
+
return DEFAULT_TIMEOUT_SECONDS * 1_000;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return Math.max(0, Math.floor(timeoutSeconds * 1_000));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function toOptionalBigInt(value?: number): bigint | undefined {
|
|
336
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return BigInt(Math.max(0, Math.floor(value)));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function storeTaskInput(
|
|
344
|
+
context: MeshToolContext,
|
|
345
|
+
provider: AgentCard,
|
|
346
|
+
input: Uint8Array,
|
|
347
|
+
): Promise<{ blobId: string }> {
|
|
348
|
+
const providerEncryptionKey = hexToBytes(provider.encryptionPublicKey);
|
|
349
|
+
const encryptionEnabled = context.encryption?.enabled ?? supportsEncryptedBlobs(context.blobStore);
|
|
350
|
+
const requireEncryption = context.encryption?.requireEncryption ?? false;
|
|
351
|
+
|
|
352
|
+
if (encryptionEnabled && providerEncryptionKey) {
|
|
353
|
+
if (!supportsEncryptedBlobs(context.blobStore)) {
|
|
354
|
+
throw new Error('Encryption is enabled, but the configured blobstore does not support encrypted payloads.');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return await context.blobStore.storeEncrypted(input, providerEncryptionKey);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (requireEncryption) {
|
|
361
|
+
throw new Error(`Provider ${provider.did} does not publish an encryption key.`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return await context.blobStore.store(input);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function taskStatusLabel(status: TaskStatus): string {
|
|
368
|
+
return TaskStatus[status] ?? 'UNKNOWN';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function publishSuccessfulReputationEvent(
|
|
372
|
+
context: MeshToolContext,
|
|
373
|
+
params: {
|
|
374
|
+
providerDid: string;
|
|
375
|
+
taskId: string;
|
|
376
|
+
capability: string;
|
|
377
|
+
priceMist: bigint;
|
|
378
|
+
latencyMs?: number;
|
|
379
|
+
},
|
|
380
|
+
): Promise<void> {
|
|
381
|
+
const publisher = context.reputationPublisher ?? (
|
|
382
|
+
hasSigningIdentity(context.relayAuthProvider)
|
|
383
|
+
? new ReputationEventPublisher(context.blobStore, context.relayAuthProvider)
|
|
384
|
+
: null
|
|
385
|
+
);
|
|
386
|
+
if (!publisher) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const event = await publisher.createEvent({
|
|
392
|
+
type: 'task_completion',
|
|
393
|
+
subject: params.providerDid,
|
|
394
|
+
taskId: params.taskId,
|
|
395
|
+
outcome: 'success',
|
|
396
|
+
capability: params.capability,
|
|
397
|
+
latencyMs: params.latencyMs,
|
|
398
|
+
paymentAmount: {
|
|
399
|
+
amount: params.priceMist.toString(),
|
|
400
|
+
currency: 'MIST',
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
await context.reputationStore?.addEvent(event);
|
|
404
|
+
await publisher.publishEvent(event);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
context.logger?.warn?.({ err: error, taskId: params.taskId }, 'Failed to publish reputation event.');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function hasSigningIdentity(identity: MeshToolContext['relayAuthProvider']): identity is NonNullable<MeshToolContext['relayAuthProvider']> {
|
|
411
|
+
return Boolean(identity && typeof identity.getDID === 'function' && typeof identity.signPersonalMessage === 'function');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function delay(ms: number): Promise<void> {
|
|
415
|
+
return new Promise((resolvePromise) => {
|
|
416
|
+
setTimeout(resolvePromise, ms);
|
|
417
|
+
});
|
|
418
|
+
}
|