@thirdfy/agent-cli 0.3.0 → 0.4.1
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/CHANGELOG.md +125 -0
- package/README.md +59 -19
- package/package.json +2 -2
- package/scripts/validate-cli-contract-schemas.mjs +6 -0
- package/src/cli/errors.mjs +58 -8
- package/src/cli/help.mjs +1 -1
- package/src/cli/manifest.mjs +62 -6
- package/src/cli/options/bootstrap.mjs +15 -3
- package/src/cli/options/chains.mjs +5 -0
- package/src/cli/options/credits.mjs +9 -0
- package/src/cli/options/data.mjs +13 -1
- package/src/cli/options/index.mjs +10 -4
- package/src/cli/options/walletControl.mjs +8 -0
- package/src/commands/bootstrap.mjs +44 -5
- package/src/commands/chat.mjs +3 -0
- package/src/commands/credits.mjs +109 -16
- package/src/commands/delegation.mjs +3 -1
- package/src/commands/discovery.mjs +156 -13
- package/src/commands/doctor.mjs +5 -2
- package/src/commands/execute.mjs +96 -3
- package/src/commands/prompt.mjs +5 -3
- package/src/commands/wallet.mjs +238 -3
- package/src/commands/walletControl.mjs +66 -0
- package/src/contracts/cli/schemas/actions.response.schema.json +45 -0
- package/src/contracts/cli/schemas/chains-non-evm.response.schema.json +76 -0
- package/src/contracts/cli/schemas/chains.response.schema.json +151 -0
- package/src/contracts/cli/schemas/error.response.schema.json +61 -0
- package/src/contracts/cli/schemas/preview.response.schema.json +179 -0
- package/src/contracts/cli/schemas/readiness.response.schema.json +94 -0
- package/src/contracts/cli/schemas/wallet-profile.response.schema.json +127 -0
- package/src/core/args.mjs +10 -2
- package/src/core/chainRegistry.mjs +223 -1
- package/src/core/context.mjs +57 -14
- package/src/core/errorEnvelope.mjs +209 -0
- package/src/core/fundingTokenIn.mjs +24 -1
- package/src/core/http.mjs +6 -1
- package/src/core/readOnlyActions.mjs +10 -0
- package/src/core/walletExecuteChainId.mjs +25 -0
- package/src/lib/creditsUsdSummary.mjs +48 -4
- package/src/runtime/actions/actionList.mjs +87 -0
- package/src/runtime/actions/selection.mjs +17 -22
- package/src/runtime/execution/actionPreflight.mjs +50 -14
- package/src/runtime/execution/payloads.mjs +36 -28
- package/src/runtime/execution/runners.mjs +75 -21
- package/src/runtime/execution/svmLane.mjs +106 -0
- package/src/runtime/execution/walletControl.mjs +104 -0
- package/src/runtime/handlers.mjs +18 -2
- package/src/runtime/index.mjs +1 -1
- package/src/runtime/main.mjs +21 -5
- package/src/runtime/providerHints/apps.mjs +36 -0
- package/src/runtime/providerHints/catalogHint.mjs +55 -0
- package/src/runtime/providerHints/cex.mjs +130 -0
- package/src/runtime/providerHints/data.mjs +144 -0
- package/src/runtime/providerHints/dispatch.mjs +177 -0
- package/src/runtime/providerHints/dogeos.mjs +38 -0
- package/src/runtime/providerHints/earn.mjs +201 -0
- package/src/runtime/providerHints/perps.mjs +346 -0
- package/src/runtime/providerHints/prediction.mjs +128 -0
- package/src/runtime/providerHints/solana.mjs +85 -0
- package/src/runtime/providerHints/trading.mjs +78 -0
- package/src/runtime/providerHints/walletControl.mjs +39 -0
- package/src/runtime/risk/notional.mjs +59 -0
- package/src/runtime/risk/orderPreview.mjs +154 -0
- package/src/runtime/risk/riskRefusal.mjs +74 -0
- package/src/runtime/validation/actionParamsGate.mjs +2 -0
- package/src/runtime/providerHints.mjs +0 -1017
|
@@ -3,6 +3,7 @@ import { pickNativeGasFields } from './nativeGas.mjs';
|
|
|
3
3
|
import {
|
|
4
4
|
ACTION_PREFLIGHT_VENUE_FIELDS,
|
|
5
5
|
appendActionPreflightQuery,
|
|
6
|
+
blockedActionPreflight,
|
|
6
7
|
blockedActionPreflightFromError,
|
|
7
8
|
describeExecutionVenue,
|
|
8
9
|
isApiActionBlock,
|
|
@@ -23,7 +24,11 @@ import { idempotentReplayFields } from '../../core/hostedRuntimeIdempotency.mjs'
|
|
|
23
24
|
import { withActionTimeout } from '../../core/actionTimeouts.mjs';
|
|
24
25
|
import { isReadOnlyResolvedAction } from '../../core/readOnlyActions.mjs';
|
|
25
26
|
import { resolveWalletExecuteChainId } from '../../core/walletExecuteChainId.mjs';
|
|
26
|
-
import { resolveChainRpcUrl } from '../../core/chainRegistry.mjs';
|
|
27
|
+
import { describeInsufficientNativeGas, isSvmChainId, resolveChainRpcUrl, sameWalletAddress } from '../../core/chainRegistry.mjs';
|
|
28
|
+
import { readWalletControl } from './walletControl.mjs';
|
|
29
|
+
import { actionNameForApi } from '../actions/selection.mjs';
|
|
30
|
+
import { blockedResultFields } from '../../core/errorEnvelope.mjs';
|
|
31
|
+
import { riskRefusalFields } from '../risk/riskRefusal.mjs';
|
|
27
32
|
import { isEmptyQueuedExecuteIntent, shouldFallbackManagedReadToExecuteIntent } from './managedReadExecuteRail.mjs';
|
|
28
33
|
import { createRequire } from 'module';
|
|
29
34
|
|
|
@@ -158,6 +163,9 @@ function normalizeManagedExecuteResponse(response, { preflight, payload, isRead
|
|
|
158
163
|
shaped?.amountNormalization || shaped?.data?.amountNormalization || null;
|
|
159
164
|
normalized.raw = shaped;
|
|
160
165
|
normalized.idempotencyKey = payload.executionIdempotencyKey || null;
|
|
166
|
+
// A risk-policy refusal nests riskRefusal under data; lift it as thirdfy-mcp walletExecute does.
|
|
167
|
+
const refusal = shaped?.success ? undefined : riskRefusalFields(shaped || {});
|
|
168
|
+
if (refusal) Object.assign(normalized, refusal);
|
|
161
169
|
// A replay means nothing new ran; say so, or the caller reads it as a fresh fill.
|
|
162
170
|
if (!isRead) {
|
|
163
171
|
Object.assign(
|
|
@@ -185,11 +193,9 @@ function shouldUseManagedReadFastPath(flags, resolved, options = {}) {
|
|
|
185
193
|
if (options.requireFundingCheck === true) return false;
|
|
186
194
|
const runMode = String(flags.runMode || 'agent_wallet').trim().toLowerCase();
|
|
187
195
|
if (runMode !== 'agent_wallet') return false;
|
|
188
|
-
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim()
|
|
189
|
-
const agentKey = String(typeof resolveAgentKey === 'function' ? resolveAgentKey(flags) : flags.agentKey || '')
|
|
190
|
-
|
|
191
|
-
.toLowerCase();
|
|
192
|
-
if (expectedWallet && expectedWallet !== agentKey) return false;
|
|
196
|
+
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
|
|
197
|
+
const agentKey = String(typeof resolveAgentKey === 'function' ? resolveAgentKey(flags) : flags.agentKey || '').trim();
|
|
198
|
+
if (expectedWallet && !sameWalletAddress(expectedWallet, agentKey)) return false;
|
|
193
199
|
let chainId;
|
|
194
200
|
try {
|
|
195
201
|
chainId = resolveWalletExecuteChainId({
|
|
@@ -259,6 +265,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
259
265
|
validationOnly: true,
|
|
260
266
|
forceIdempotency: false,
|
|
261
267
|
resolvedAction,
|
|
268
|
+
apiAction: actionNameForApi(resolved),
|
|
262
269
|
runMode: 'self',
|
|
263
270
|
mirrorOnly: false,
|
|
264
271
|
});
|
|
@@ -306,6 +313,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
306
313
|
validationOnly: true,
|
|
307
314
|
forceIdempotency: false,
|
|
308
315
|
resolvedAction,
|
|
316
|
+
apiAction: actionNameForApi(resolved),
|
|
309
317
|
runMode: 'hybrid',
|
|
310
318
|
mirrorOnly: false,
|
|
311
319
|
hybridWalletMode: options.hybridWalletMode,
|
|
@@ -333,6 +341,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
333
341
|
validationOnly: true,
|
|
334
342
|
forceIdempotency: false,
|
|
335
343
|
resolvedAction,
|
|
344
|
+
apiAction: actionNameForApi(resolved),
|
|
336
345
|
runMode: 'agent_wallet',
|
|
337
346
|
mirrorOnly: false,
|
|
338
347
|
hybridWalletMode: 'agent_wallet',
|
|
@@ -343,6 +352,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
343
352
|
validationOnly: true,
|
|
344
353
|
forceIdempotency: false,
|
|
345
354
|
resolvedAction,
|
|
355
|
+
apiAction: actionNameForApi(resolved),
|
|
346
356
|
runMode: 'hybrid',
|
|
347
357
|
mirrorOnly: true,
|
|
348
358
|
hybridWalletMode: 'agent_wallet',
|
|
@@ -371,6 +381,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
371
381
|
validationOnly: true,
|
|
372
382
|
forceIdempotency: false,
|
|
373
383
|
resolvedAction,
|
|
384
|
+
apiAction: actionNameForApi(resolved),
|
|
374
385
|
runMode,
|
|
375
386
|
mirrorOnly: runMode === 'hybrid',
|
|
376
387
|
});
|
|
@@ -479,6 +490,7 @@ async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'third
|
|
|
479
490
|
validationOnly: true,
|
|
480
491
|
forceIdempotency: false,
|
|
481
492
|
resolvedAction: resolved.resolvedAction,
|
|
493
|
+
apiAction: actionNameForApi(resolved),
|
|
482
494
|
runMode,
|
|
483
495
|
mirrorOnly: false,
|
|
484
496
|
hybridWalletMode: options?.hybridWalletMode,
|
|
@@ -502,6 +514,7 @@ async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'third
|
|
|
502
514
|
validationOnly: false,
|
|
503
515
|
forceIdempotency: true,
|
|
504
516
|
resolvedAction: resolved.resolvedAction,
|
|
517
|
+
apiAction: actionNameForApi(resolved),
|
|
505
518
|
runMode,
|
|
506
519
|
mirrorOnly: false,
|
|
507
520
|
hybridWalletMode: options?.hybridWalletMode,
|
|
@@ -581,6 +594,7 @@ async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
|
581
594
|
}
|
|
582
595
|
const payload = buildManagedExecutePayload(flags, {
|
|
583
596
|
resolvedAction: resolved.resolvedAction,
|
|
597
|
+
apiAction: actionNameForApi(resolved),
|
|
584
598
|
runMode: 'agent_wallet',
|
|
585
599
|
forceIdempotency: true,
|
|
586
600
|
});
|
|
@@ -600,9 +614,20 @@ async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
|
600
614
|
: preflight;
|
|
601
615
|
const intentResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, fallbackPreflight);
|
|
602
616
|
if (isEmptyQueuedExecuteIntent(intentResult.raw || intentResult)) {
|
|
617
|
+
// A read that did not answer in time (often a cold provider cache): nothing changed, and the
|
|
618
|
+
// same read usually answers on the next try. Same fields as thirdfy-mcp walletExecute.
|
|
603
619
|
intentResult.success = false;
|
|
604
620
|
intentResult.status = 'failed';
|
|
605
621
|
intentResult.error = intentResult.error || 'MANAGED_READ_EMPTY_EXECUTE_INTENT';
|
|
622
|
+
Object.assign(intentResult, {
|
|
623
|
+
errorCode: 'MANAGED_READ_EMPTY_EXECUTE_INTENT',
|
|
624
|
+
retryable: true,
|
|
625
|
+
retryAfterSeconds: 3,
|
|
626
|
+
nextAction: 'retry_later',
|
|
627
|
+
hint:
|
|
628
|
+
intentResult.hint ||
|
|
629
|
+
'The read did not answer in time; nothing was changed. Repeat the same read in a few seconds. Do not treat the empty queued result as data.',
|
|
630
|
+
});
|
|
606
631
|
}
|
|
607
632
|
intentResult.executionWalletAddress =
|
|
608
633
|
preflight?.executionWalletAddress ||
|
|
@@ -620,12 +645,11 @@ async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
|
620
645
|
* the API reports for the read.
|
|
621
646
|
*/
|
|
622
647
|
function fastPathWalletMismatch(flags, response) {
|
|
623
|
-
|
|
648
|
+
// 0x addresses compare case-insensitively; Solana base58 is case-sensitive and kept as sent.
|
|
649
|
+
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
|
|
624
650
|
if (!expectedWallet || parseBooleanFlag(flags.allowWalletMismatch, false)) return null;
|
|
625
|
-
const apiWallet = String(response?.executionWalletAddress || response?.data?.executionWalletAddress || '')
|
|
626
|
-
|
|
627
|
-
.toLowerCase();
|
|
628
|
-
if (!apiWallet || apiWallet === expectedWallet) return null;
|
|
651
|
+
const apiWallet = String(response?.executionWalletAddress || response?.data?.executionWalletAddress || '').trim();
|
|
652
|
+
if (!apiWallet || sameWalletAddress(apiWallet, expectedWallet)) return null;
|
|
629
653
|
const normalized = normalizeIntentResponse({
|
|
630
654
|
success: false,
|
|
631
655
|
status: 'failed',
|
|
@@ -655,6 +679,7 @@ async function executeSelfRun(ctx, flags, resolved, options) {
|
|
|
655
679
|
validationOnly: true,
|
|
656
680
|
forceIdempotency: false,
|
|
657
681
|
resolvedAction: resolved.resolvedAction,
|
|
682
|
+
apiAction: actionNameForApi(resolved),
|
|
658
683
|
runMode: 'self',
|
|
659
684
|
mirrorOnly: false,
|
|
660
685
|
});
|
|
@@ -679,7 +704,7 @@ async function executeSelfRun(ctx, flags, resolved, options) {
|
|
|
679
704
|
normalized.preflightBlocked = true;
|
|
680
705
|
return normalized;
|
|
681
706
|
}
|
|
682
|
-
const buildTxPayload = buildBuildTxPayload(flags, resolved.resolvedAction);
|
|
707
|
+
const buildTxPayload = buildBuildTxPayload(flags, resolved.resolvedAction, actionNameForApi(resolved));
|
|
683
708
|
// build-tx returns an unsigned transaction; nothing is broadcast, so a timeout may be resent.
|
|
684
709
|
const buildTx = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/build-tx', buildTxPayload);
|
|
685
710
|
return normalizeIntentResponse({
|
|
@@ -714,6 +739,7 @@ async function executeHybridRun(ctx, flags, resolved, options) {
|
|
|
714
739
|
validationOnly: true,
|
|
715
740
|
forceIdempotency: true,
|
|
716
741
|
resolvedAction: resolved.resolvedAction,
|
|
742
|
+
apiAction: actionNameForApi(resolved),
|
|
717
743
|
runMode: 'hybrid',
|
|
718
744
|
mirrorOnly: true,
|
|
719
745
|
hybridWalletMode,
|
|
@@ -741,6 +767,7 @@ async function executeHybridRun(ctx, flags, resolved, options) {
|
|
|
741
767
|
validationOnly: true,
|
|
742
768
|
forceIdempotency: true,
|
|
743
769
|
resolvedAction: resolved.resolvedAction,
|
|
770
|
+
apiAction: actionNameForApi(resolved),
|
|
744
771
|
runMode: 'hybrid',
|
|
745
772
|
mirrorOnly: true,
|
|
746
773
|
});
|
|
@@ -784,7 +811,7 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
|
|
|
784
811
|
signerMethod: 'managed_wallet_server',
|
|
785
812
|
};
|
|
786
813
|
}
|
|
787
|
-
const tokenIn = resolveTokenInForFunding(params, action);
|
|
814
|
+
const tokenIn = resolveTokenInForFunding(params, action, resolved?.resolvedActionMeta?.behavior?.spend);
|
|
788
815
|
const query = new URLSearchParams({
|
|
789
816
|
userDid,
|
|
790
817
|
chainId: String(chainId),
|
|
@@ -792,7 +819,7 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
|
|
|
792
819
|
});
|
|
793
820
|
if (tokenIn) query.set('tokenIn', tokenIn);
|
|
794
821
|
const evaluateAction = options.evaluateAction === true;
|
|
795
|
-
if (evaluateAction) appendActionPreflightQuery(query, { action, params, flags });
|
|
822
|
+
if (evaluateAction) appendActionPreflightQuery(query, { action, apiAction: actionNameForApi(resolved), params, flags });
|
|
796
823
|
const executionVenue = describeExecutionVenue(action, chainId);
|
|
797
824
|
const preflightSeed = options.preflightSeed && typeof options.preflightSeed === 'object'
|
|
798
825
|
? options.preflightSeed
|
|
@@ -826,8 +853,23 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
|
|
|
826
853
|
};
|
|
827
854
|
}
|
|
828
855
|
}
|
|
856
|
+
// An action preflight the API answered 2xx but refused is a block too (thirdfy-mcp previewOrder).
|
|
857
|
+
if (evaluateAction && response?.success === false) {
|
|
858
|
+
return {
|
|
859
|
+
...blockedActionPreflight(response, {
|
|
860
|
+
errorFields: blockedResultFields(String(response.blockedReason || 'PREFLIGHT_BLOCKED')),
|
|
861
|
+
}),
|
|
862
|
+
userDid,
|
|
863
|
+
chainId,
|
|
864
|
+
...(executionVenue ? { executionVenue } : {}),
|
|
865
|
+
};
|
|
866
|
+
}
|
|
829
867
|
const actionFields = evaluateAction ? pickActionPreflightFields(response) : {};
|
|
830
|
-
|
|
868
|
+
// API 4.0 wallet control, as thirdfy-mcp reports it: the API refuses writes itself (before
|
|
869
|
+
// signing), so preflight only reports it. Solana is exempt from the API's gate.
|
|
870
|
+
const walletControlFields = readWalletControl(response, { svm: isSvmChainId(chainId) });
|
|
871
|
+
// Keep the API's case: Solana base58 addresses are case-sensitive (lowercasing corrupts them).
|
|
872
|
+
const executionWalletAddress = String(response?.executionWalletAddress || '').trim();
|
|
831
873
|
const signerMethod = String(response?.signerMethod || 'managed_wallet_server').trim();
|
|
832
874
|
const rawBalance = String(response?.fundingTokenBalance?.raw || '').trim();
|
|
833
875
|
let parsedRawBalance = null;
|
|
@@ -848,17 +890,24 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
|
|
|
848
890
|
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
849
891
|
...actionFields,
|
|
850
892
|
...(executionVenue ? { executionVenue } : {}),
|
|
893
|
+
...walletControlFields,
|
|
851
894
|
};
|
|
852
895
|
}
|
|
853
896
|
}
|
|
854
897
|
}
|
|
855
|
-
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim()
|
|
898
|
+
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
|
|
856
899
|
const allowWalletMismatch = parseBooleanFlag(flags.allowWalletMismatch, false);
|
|
857
900
|
|
|
858
|
-
if (
|
|
901
|
+
if (
|
|
902
|
+
expectedWallet
|
|
903
|
+
&& executionWalletAddress
|
|
904
|
+
&& !sameWalletAddress(expectedWallet, executionWalletAddress)
|
|
905
|
+
&& !allowWalletMismatch
|
|
906
|
+
) {
|
|
859
907
|
return {
|
|
860
908
|
success: false,
|
|
861
909
|
blockedReason: 'EXECUTION_WALLET_MISMATCH',
|
|
910
|
+
...blockedResultFields('EXECUTION_WALLET_MISMATCH'),
|
|
862
911
|
blockedStage: 'routing',
|
|
863
912
|
error: `Funded wallet (${expectedWallet}) does not match execution wallet (${executionWalletAddress}). Use --allow-wallet-mismatch to override.`,
|
|
864
913
|
executionWalletAddress,
|
|
@@ -866,6 +915,7 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
|
|
|
866
915
|
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
867
916
|
...actionFields,
|
|
868
917
|
...(executionVenue ? { executionVenue } : {}),
|
|
918
|
+
...walletControlFields,
|
|
869
919
|
};
|
|
870
920
|
}
|
|
871
921
|
|
|
@@ -873,6 +923,7 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
|
|
|
873
923
|
return {
|
|
874
924
|
success: false,
|
|
875
925
|
blockedReason: 'INSUFFICIENT_FUNDS',
|
|
926
|
+
...blockedResultFields('INSUFFICIENT_FUNDS'),
|
|
876
927
|
blockedStage: 'sizing',
|
|
877
928
|
error: `Execution wallet ${executionWalletAddress || 'unknown'} has insufficient tokenIn balance.`,
|
|
878
929
|
executionWalletAddress,
|
|
@@ -880,24 +931,26 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
|
|
|
880
931
|
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
881
932
|
...actionFields,
|
|
882
933
|
...(executionVenue ? { executionVenue } : {}),
|
|
934
|
+
...walletControlFields,
|
|
883
935
|
};
|
|
884
936
|
}
|
|
885
937
|
|
|
886
|
-
// User-pays-gas chains (DogeOS): the API reports native gas readiness (API 3.18.2+). Keep
|
|
887
|
-
// stop a write before /execute when the wallet
|
|
938
|
+
// User-pays-gas chains (DogeOS, Solana): the API reports native gas readiness (API 3.18.2+). Keep
|
|
939
|
+
// it, and stop a write before /execute when the API says the wallet cannot pay gas (gasReady false).
|
|
888
940
|
const nativeGas = pickNativeGasFields(response);
|
|
889
941
|
if (options.requireFundingCheck && nativeGas.gasReady === false) {
|
|
890
942
|
return {
|
|
891
943
|
success: false,
|
|
892
944
|
blockedReason: 'INSUFFICIENT_NATIVE_GAS',
|
|
945
|
+
...blockedResultFields('INSUFFICIENT_NATIVE_GAS'),
|
|
893
946
|
blockedStage: 'sizing',
|
|
894
|
-
error: nativeGas.nativeGasWarning?.message
|
|
895
|
-
|| `Execution wallet ${executionWalletAddress || 'unknown'} has 0 native gas on chain ${chainId}; gas on this chain is paid by the wallet.`,
|
|
947
|
+
error: nativeGas.nativeGasWarning?.message || describeInsufficientNativeGas(executionWalletAddress, chainId),
|
|
896
948
|
executionWalletAddress,
|
|
897
949
|
signerMethod,
|
|
898
950
|
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
899
951
|
...actionFields,
|
|
900
952
|
...(executionVenue ? { executionVenue } : {}),
|
|
953
|
+
...walletControlFields,
|
|
901
954
|
...nativeGas,
|
|
902
955
|
};
|
|
903
956
|
}
|
|
@@ -913,6 +966,7 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
|
|
|
913
966
|
...nativeGas,
|
|
914
967
|
...actionFields,
|
|
915
968
|
...(executionVenue ? { executionVenue } : {}),
|
|
969
|
+
...walletControlFields,
|
|
916
970
|
};
|
|
917
971
|
}
|
|
918
972
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { parseJsonFlag } from '../../core/args.mjs';
|
|
2
|
+
import { createCliError } from '../../core/envelope.mjs';
|
|
3
|
+
import { chainRegistrySource, configuredSvmChainIds, getChainInfo, isSvmChainId, svmChainRefusal } from '../../core/chainRegistry.mjs';
|
|
4
|
+
import {
|
|
5
|
+
inferChainIdFromCatalogRow,
|
|
6
|
+
resolveWalletExecuteChainId,
|
|
7
|
+
} from '../../core/walletExecuteChainId.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Solana (svm) lane rules. Solana actions run only with run mode `agent_wallet` on the managed
|
|
11
|
+
* Privy Solana agent wallet. The self and hybrid lanes sign EVM transactions (ethers / OWS), and
|
|
12
|
+
* `thirdfy` fan-out does not run on Solana, so a Solana run never goes to them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function preparedParams(flags) {
|
|
16
|
+
if (flags.__preparedParams && typeof flags.__preparedParams === 'object') return flags.__preparedParams;
|
|
17
|
+
try {
|
|
18
|
+
return parseJsonFlag(flags, 'params') || {};
|
|
19
|
+
} catch {
|
|
20
|
+
// The params gate reports invalid JSON; this lookup only needs a chain hint.
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasValue(value) {
|
|
26
|
+
return value !== undefined && value !== null && String(value).trim() !== '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* With no `--chain-id` and no `params.chainId`, a catalog row that runs only on Solana
|
|
31
|
+
* (`supportedChains: [101]`) sets `flags.chainId` to that chain. Returns the chain the managed
|
|
32
|
+
* wallet is resolved on, or undefined when it cannot be resolved here.
|
|
33
|
+
*/
|
|
34
|
+
export function applySvmChainDefault(flags, resolved) {
|
|
35
|
+
const params = preparedParams(flags);
|
|
36
|
+
if (!hasValue(flags.chainId) && !hasValue(params?.chainId)) {
|
|
37
|
+
const inferred = inferChainIdFromCatalogRow(resolved?.resolvedActionMeta);
|
|
38
|
+
if (inferred) {
|
|
39
|
+
flags.chainId = String(inferred);
|
|
40
|
+
flags.__chainIdInferred = inferred;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return resolveWalletExecuteChainId({
|
|
45
|
+
action: String(resolved?.resolvedAction || flags.action || '').trim(),
|
|
46
|
+
chainId: flags.chainId,
|
|
47
|
+
params,
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run mode for a resolved chain. On a Solana chain:
|
|
56
|
+
* - `agent_wallet` passes;
|
|
57
|
+
* - a run mode that only came from the profile, env or config default becomes `agent_wallet`;
|
|
58
|
+
* - an explicit `--run-mode` other than `agent_wallet` (or a command fixed to self signing) fails
|
|
59
|
+
* with SVM_REQUIRES_AGENT_WALLET before any API call.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveSvmRunMode(flags, runMode, chainId, { locked = false } = {}) {
|
|
62
|
+
if (!isSvmChainId(chainId) || runMode === 'agent_wallet') return runMode;
|
|
63
|
+
const explicit = locked || hasValue(flags.runMode);
|
|
64
|
+
if (explicit) {
|
|
65
|
+
const info = getChainInfo(chainId);
|
|
66
|
+
throw createCliError(
|
|
67
|
+
'SVM_REQUIRES_AGENT_WALLET',
|
|
68
|
+
`${info?.displayName || 'Solana'} (chain ${chainId}) actions run only with --run-mode agent_wallet on the managed Solana agent wallet. run-mode=${runMode} is not supported on Solana: self and hybrid signing are EVM-only.`,
|
|
69
|
+
{
|
|
70
|
+
chainId: Number(chainId),
|
|
71
|
+
chainFamily: 'svm',
|
|
72
|
+
requestedRunMode: runMode,
|
|
73
|
+
recommendedRunModes: ['agent_wallet'],
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
flags.runMode = 'agent_wallet';
|
|
78
|
+
flags.__runModeDefaultedForSvm = true;
|
|
79
|
+
return 'agent_wallet';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Refuses a Solana chain the API does not run, before any wallet call (thirdfy-mcp
|
|
84
|
+
* `unsupportedChainFields`): SOLANA_NOT_ENABLED (`use_evm_chain`) on an API with no Solana,
|
|
85
|
+
* CHAIN_NOT_SUPPORTED (`fix_request`) for a Solana chain id it does not list.
|
|
86
|
+
*/
|
|
87
|
+
export function assertSvmChainRunByApi(chainId) {
|
|
88
|
+
const refusal = svmChainRefusal(chainId);
|
|
89
|
+
if (!refusal) return;
|
|
90
|
+
const fields = { chainId: Number(chainId), chainFamily: 'svm', chainRegistrySource: chainRegistrySource() };
|
|
91
|
+
if (refusal === 'SOLANA_NOT_ENABLED') {
|
|
92
|
+
throw createCliError('SOLANA_NOT_ENABLED', `This Thirdfy API does not run Solana yet (its capabilities list no svm chain); chain ${chainId} is refused.`, fields);
|
|
93
|
+
}
|
|
94
|
+
const supported = configuredSvmChainIds();
|
|
95
|
+
throw createCliError('CHAIN_NOT_SUPPORTED', `This Thirdfy API does not run Solana chain ${chainId}; it runs ${supported.join(', ')}.`, {
|
|
96
|
+
...fields,
|
|
97
|
+
supportedSvmChainIds: supported,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Chain default plus run mode check, in that order. Returns the run mode to use. */
|
|
102
|
+
export function applySvmLane(flags, resolved, runMode, options = {}) {
|
|
103
|
+
const chainId = applySvmChainDefault(flags, resolved);
|
|
104
|
+
assertSvmChainRunByApi(chainId);
|
|
105
|
+
return resolveSvmRunMode(flags, runMode, chainId, options);
|
|
106
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thirdfy API 4.0 wallet control (Privy owner/signer split, `PRIVY_WALLET_CONTROL_MODE` off /
|
|
3
|
+
* enforced / user_owned / per_template), shown the way thirdfy-mcp 0.1.19 shows it
|
|
4
|
+
* (`src/wallet/walletControl.ts`), so CLI and MCP callers read the same fields.
|
|
5
|
+
*
|
|
6
|
+
* The API reports a managed agent wallet's control as `walletControl { owner, policy,
|
|
7
|
+
* signerActive, reason?, blockers? }` on `/agent/execution-wallet`, and refuses writes before
|
|
8
|
+
* signing with the codes below, nesting `nextAction`, `userMessage` and `walletControl` under the
|
|
9
|
+
* error body's `data`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Wallet-control refusals, and whether repeating the same call can succeed. */
|
|
13
|
+
export const WALLET_CONTROL_RETRYABLE = Object.freeze({
|
|
14
|
+
AGENT_SIGNER_REMOVED_BY_OWNER: false,
|
|
15
|
+
AGENT_WALLET_CONTROL_CONTRADICTORY: false,
|
|
16
|
+
AGENT_WALLET_CONTROL_UNAVAILABLE: true,
|
|
17
|
+
WITHDRAW_DESTINATION_NOT_OWNER: false,
|
|
18
|
+
AGENT_POLICY_UPDATE_REQUIRED: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const WALLET_CONTROL_HINTS = Object.freeze({
|
|
22
|
+
AGENT_SIGNER_REMOVED_BY_OWNER:
|
|
23
|
+
"Do not retry. The wallet owner removed Thirdfy's signer from this agent wallet; the owner must re-enable the agent in their Thirdfy account before it can trade.",
|
|
24
|
+
AGENT_WALLET_CONTROL_CONTRADICTORY:
|
|
25
|
+
"Do not retry. The agent wallet's control does not match Thirdfy's records; nothing was signed. Contact Thirdfy support.",
|
|
26
|
+
AGENT_WALLET_CONTROL_UNAVAILABLE: "The agent wallet's control could not be verified; nothing was signed. Retry shortly.",
|
|
27
|
+
WITHDRAW_DESTINATION_NOT_OWNER:
|
|
28
|
+
"Exits from a wallet under wallet control pay only the wallet itself, the owner payout address or the owner's wallet. Send the exit there, or ask the owner to change the payout.",
|
|
29
|
+
AGENT_POLICY_UPDATE_REQUIRED:
|
|
30
|
+
"Do not retry. The owner must sign a policy update (update_policy) for this action's venue before the agent can use it: have the owner open ownerAction.requestsPath in their Thirdfy account, or run `thirdfy-agent wallet-control request --action update_policy`, sign it, and run `wallet-control submit`. Nothing was signed.",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** What an agent should do for each readiness blocker, as the API's own gate says it. */
|
|
34
|
+
const BLOCKER_NEXT_ACTION = Object.freeze({
|
|
35
|
+
AGENT_SIGNER_REMOVED_BY_OWNER: 'owner_reenable_agent_signer',
|
|
36
|
+
AGENT_WALLET_CONTROL_UNAVAILABLE: 'retry_later',
|
|
37
|
+
AGENT_WALLET_CONTROL_CONTRADICTORY: 'contact_support',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const LIFTED_FIELDS = ['nextAction', 'userMessage', 'walletControl', 'ownerAction'];
|
|
41
|
+
|
|
42
|
+
const isRecord = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
43
|
+
|
|
44
|
+
/** The body and the bodies nested under its `data`, outermost first. */
|
|
45
|
+
function nestedBodies(body) {
|
|
46
|
+
const bodies = [];
|
|
47
|
+
let current = body;
|
|
48
|
+
for (let depth = 0; depth < 3 && isRecord(current); depth += 1) {
|
|
49
|
+
bodies.push(current);
|
|
50
|
+
current = current.data;
|
|
51
|
+
}
|
|
52
|
+
return bodies;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The wallet-control code an API error body carries (`error` or `blockedReason`), if any. */
|
|
56
|
+
export function walletControlCode(body) {
|
|
57
|
+
for (const entry of nestedBodies(body)) {
|
|
58
|
+
for (const value of [entry.error, entry.blockedReason]) {
|
|
59
|
+
if (typeof value === 'string' && Object.prototype.hasOwnProperty.call(WALLET_CONTROL_RETRYABLE, value)) return value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The fields a wallet-control refusal adds to the CLI's error envelope `data`: `walletControl`,
|
|
67
|
+
* `nextAction`, `userMessage` from the API body (at any depth), plus `retryable` and `hint` for the
|
|
68
|
+
* known codes. Empty for any other error, so other envelopes are unchanged.
|
|
69
|
+
*/
|
|
70
|
+
export function walletControlErrorFields(body) {
|
|
71
|
+
const code = walletControlCode(body);
|
|
72
|
+
if (!code) return {};
|
|
73
|
+
const fields = { walletControlCode: code, retryable: WALLET_CONTROL_RETRYABLE[code] };
|
|
74
|
+
const bodies = nestedBodies(body);
|
|
75
|
+
for (const field of LIFTED_FIELDS) {
|
|
76
|
+
const source = bodies.find((entry) => entry[field] !== undefined && entry[field] !== null);
|
|
77
|
+
if (source) fields[field] = source[field];
|
|
78
|
+
}
|
|
79
|
+
if (WALLET_CONTROL_HINTS[code]) fields.hint = WALLET_CONTROL_HINTS[code];
|
|
80
|
+
return fields;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `walletControl` from an `/agent/execution-wallet` body, for preflight output. When Thirdfy's
|
|
85
|
+
* signer is off (`signerActive: false`) it adds the blocker (`walletControl.blockers`) and the next
|
|
86
|
+
* step. The API reads control from the EVM wallet and its gate exempts Solana, so a Solana check
|
|
87
|
+
* (`svm: true`) reports it without blocking.
|
|
88
|
+
*/
|
|
89
|
+
export function readWalletControl(body, { svm = false } = {}) {
|
|
90
|
+
const walletControl = isRecord(body) && isRecord(body.walletControl) ? body.walletControl : undefined;
|
|
91
|
+
if (!walletControl) return {};
|
|
92
|
+
if (walletControl.signerActive !== false || svm) return { walletControl };
|
|
93
|
+
const blockers = Array.isArray(walletControl.blockers) ? walletControl.blockers : [];
|
|
94
|
+
const blocker =
|
|
95
|
+
blockers.find((entry) => typeof entry === 'string' && Object.prototype.hasOwnProperty.call(BLOCKER_NEXT_ACTION, entry)) ||
|
|
96
|
+
'AGENT_SIGNER_REMOVED_BY_OWNER';
|
|
97
|
+
return {
|
|
98
|
+
walletControl,
|
|
99
|
+
walletControlBlocked: true,
|
|
100
|
+
walletControlBlocker: blocker,
|
|
101
|
+
nextAction: BLOCKER_NEXT_ACTION[blocker],
|
|
102
|
+
userMessage: WALLET_CONTROL_HINTS[blocker],
|
|
103
|
+
};
|
|
104
|
+
}
|
package/src/runtime/handlers.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { createBrainCommands } from '../commands/brain.mjs';
|
|
|
18
18
|
import { createTelemetryCommands } from '../commands/telemetry.mjs';
|
|
19
19
|
import { createCredentialsCommands } from '../commands/credentials.mjs';
|
|
20
20
|
import { createAgentCommands } from '../commands/agent.mjs';
|
|
21
|
+
import { createWalletControlCommands } from '../commands/walletControl.mjs';
|
|
21
22
|
import { createDoctorCommands } from '../commands/doctor.mjs';
|
|
22
23
|
import { createPromptCommands } from '../commands/prompt.mjs';
|
|
23
24
|
import { createVenueCommands } from '../commands/venue.mjs';
|
|
@@ -36,7 +37,7 @@ export function createRuntimeHandlers(deps) {
|
|
|
36
37
|
buildOwnerAuthHeaders,
|
|
37
38
|
probeBitfinexPermissions,
|
|
38
39
|
applyActionFilters,
|
|
39
|
-
|
|
40
|
+
getProviderHintForActions,
|
|
40
41
|
resolveChainSupport,
|
|
41
42
|
resolveAgentApiKey,
|
|
42
43
|
getPreparedParams,
|
|
@@ -54,6 +55,7 @@ export function createRuntimeHandlers(deps) {
|
|
|
54
55
|
performSelfBroadcast,
|
|
55
56
|
resolveManagedExecutionPreflight,
|
|
56
57
|
resolveEffectiveUserDid,
|
|
58
|
+
resolveAgentKey,
|
|
57
59
|
loadEthers,
|
|
58
60
|
resolveOwsBinary,
|
|
59
61
|
CLI_VERSION,
|
|
@@ -62,9 +64,10 @@ export function createRuntimeHandlers(deps) {
|
|
|
62
64
|
const __discovery = createDiscoveryCommands({
|
|
63
65
|
printEnvelope,
|
|
64
66
|
applyActionFilters,
|
|
65
|
-
|
|
67
|
+
getProviderHintForActions,
|
|
66
68
|
resolveChainSupport,
|
|
67
69
|
getAuthToken,
|
|
70
|
+
buildOwnerAuthHeaders,
|
|
68
71
|
resolveAgentApiKey,
|
|
69
72
|
});
|
|
70
73
|
|
|
@@ -73,6 +76,9 @@ export function createRuntimeHandlers(deps) {
|
|
|
73
76
|
const __credits = createCreditsCommands({
|
|
74
77
|
printEnvelope,
|
|
75
78
|
getAuthToken,
|
|
79
|
+
buildOwnerAuthHeaders,
|
|
80
|
+
// __wallet is created further down; resolve it when the command runs.
|
|
81
|
+
commandWalletExecute: (...args) => __wallet.commandWalletExecute(...args),
|
|
76
82
|
});
|
|
77
83
|
|
|
78
84
|
const __chat = createChatCommands({
|
|
@@ -95,6 +101,8 @@ export function createRuntimeHandlers(deps) {
|
|
|
95
101
|
probeBitfinexPermissions,
|
|
96
102
|
});
|
|
97
103
|
|
|
104
|
+
const __walletControl = createWalletControlCommands({ printEnvelope, buildOwnerAuthHeaders });
|
|
105
|
+
|
|
98
106
|
const __agent = createAgentCommands({
|
|
99
107
|
printEnvelope,
|
|
100
108
|
extractAgentApiKey,
|
|
@@ -149,6 +157,9 @@ export function createRuntimeHandlers(deps) {
|
|
|
149
157
|
executeSelfRun,
|
|
150
158
|
submitSignedTx,
|
|
151
159
|
printEnvelope,
|
|
160
|
+
resolveEffectiveUserDid,
|
|
161
|
+
resolveAgentKey,
|
|
162
|
+
buildOwnerAuthHeaders,
|
|
152
163
|
});
|
|
153
164
|
|
|
154
165
|
const __execute = createExecuteCommands({
|
|
@@ -162,6 +173,7 @@ export function createRuntimeHandlers(deps) {
|
|
|
162
173
|
applyAvantisAgentWalletAddressDefault,
|
|
163
174
|
runPreflightByMode,
|
|
164
175
|
runExecutionByMode,
|
|
176
|
+
resolveManagedExecutionPreflight,
|
|
165
177
|
applyExecutionFallbackHints,
|
|
166
178
|
performSelfBroadcast,
|
|
167
179
|
executeManagedWalletRun,
|
|
@@ -237,6 +249,7 @@ export function createRuntimeHandlers(deps) {
|
|
|
237
249
|
commandActions: __discovery.commandActions,
|
|
238
250
|
commandSignals: __signals.commandSignals,
|
|
239
251
|
commandCreditsBalance: __discovery.commandCreditsBalance,
|
|
252
|
+
commandCreditsBuy: __credits.commandCreditsBuy,
|
|
240
253
|
commandCreditsTransactions: __credits.commandCreditsTransactions,
|
|
241
254
|
commandCreditsUsage: __credits.commandCreditsUsage,
|
|
242
255
|
commandModelsList: __credits.commandModelsList,
|
|
@@ -257,6 +270,9 @@ export function createRuntimeHandlers(deps) {
|
|
|
257
270
|
commandAgentRegister: __agent.commandAgentRegister,
|
|
258
271
|
commandAgentKeyRotate: __agent.commandAgentKeyRotate,
|
|
259
272
|
commandAgentKeyRevoke: __agent.commandAgentKeyRevoke,
|
|
273
|
+
commandWalletControlState: __walletControl.commandWalletControlState,
|
|
274
|
+
commandWalletControlRequest: __walletControl.commandWalletControlRequest,
|
|
275
|
+
commandWalletControlSubmit: __walletControl.commandWalletControlSubmit,
|
|
260
276
|
commandAgentAuthChallenge: __agent.commandAgentAuthChallenge,
|
|
261
277
|
commandAgentAuthVerify: __agent.commandAgentAuthVerify,
|
|
262
278
|
commandAnalyticsPortfolio: __telemetry.commandAnalyticsPortfolio,
|
package/src/runtime/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createThirdfyAgentRuntime } from './main.mjs';
|
|
2
|
-
export { createProviderHints } from './providerHints.mjs';
|
|
2
|
+
export { createProviderHints } from './providerHints/dispatch.mjs';
|
|
3
3
|
export { createRuntimeHandlers } from './handlers.mjs';
|
|
4
4
|
export { createInitialRuntimeContext, applyFlagsToRuntimeContext } from './context.mjs';
|