@thirdfy/agent-cli 0.3.0 → 0.4.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 (64) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +60 -18
  3. package/package.json +2 -2
  4. package/scripts/validate-cli-contract-schemas.mjs +5 -0
  5. package/src/cli/errors.mjs +58 -8
  6. package/src/cli/manifest.mjs +54 -5
  7. package/src/cli/options/bootstrap.mjs +15 -3
  8. package/src/cli/options/chains.mjs +5 -0
  9. package/src/cli/options/credits.mjs +9 -0
  10. package/src/cli/options/data.mjs +13 -1
  11. package/src/cli/options/index.mjs +10 -4
  12. package/src/cli/options/walletControl.mjs +8 -0
  13. package/src/commands/bootstrap.mjs +44 -5
  14. package/src/commands/chat.mjs +3 -0
  15. package/src/commands/credits.mjs +39 -4
  16. package/src/commands/delegation.mjs +3 -1
  17. package/src/commands/discovery.mjs +156 -13
  18. package/src/commands/doctor.mjs +5 -2
  19. package/src/commands/execute.mjs +96 -3
  20. package/src/commands/prompt.mjs +5 -3
  21. package/src/commands/wallet.mjs +119 -3
  22. package/src/commands/walletControl.mjs +66 -0
  23. package/src/contracts/cli/schemas/actions.response.schema.json +45 -0
  24. package/src/contracts/cli/schemas/chains-non-evm.response.schema.json +76 -0
  25. package/src/contracts/cli/schemas/chains.response.schema.json +151 -0
  26. package/src/contracts/cli/schemas/error.response.schema.json +61 -0
  27. package/src/contracts/cli/schemas/preview.response.schema.json +179 -0
  28. package/src/contracts/cli/schemas/readiness.response.schema.json +94 -0
  29. package/src/core/args.mjs +10 -2
  30. package/src/core/chainRegistry.mjs +223 -1
  31. package/src/core/context.mjs +37 -14
  32. package/src/core/errorEnvelope.mjs +209 -0
  33. package/src/core/fundingTokenIn.mjs +24 -1
  34. package/src/core/http.mjs +6 -1
  35. package/src/core/readOnlyActions.mjs +10 -0
  36. package/src/core/walletExecuteChainId.mjs +25 -0
  37. package/src/lib/creditsUsdSummary.mjs +48 -4
  38. package/src/runtime/actions/actionList.mjs +87 -0
  39. package/src/runtime/actions/selection.mjs +17 -22
  40. package/src/runtime/execution/actionPreflight.mjs +50 -14
  41. package/src/runtime/execution/payloads.mjs +36 -28
  42. package/src/runtime/execution/runners.mjs +75 -21
  43. package/src/runtime/execution/svmLane.mjs +106 -0
  44. package/src/runtime/execution/walletControl.mjs +104 -0
  45. package/src/runtime/handlers.mjs +16 -2
  46. package/src/runtime/index.mjs +1 -1
  47. package/src/runtime/main.mjs +21 -5
  48. package/src/runtime/providerHints/apps.mjs +36 -0
  49. package/src/runtime/providerHints/catalogHint.mjs +55 -0
  50. package/src/runtime/providerHints/cex.mjs +130 -0
  51. package/src/runtime/providerHints/data.mjs +144 -0
  52. package/src/runtime/providerHints/dispatch.mjs +177 -0
  53. package/src/runtime/providerHints/dogeos.mjs +38 -0
  54. package/src/runtime/providerHints/earn.mjs +201 -0
  55. package/src/runtime/providerHints/perps.mjs +346 -0
  56. package/src/runtime/providerHints/prediction.mjs +128 -0
  57. package/src/runtime/providerHints/solana.mjs +85 -0
  58. package/src/runtime/providerHints/trading.mjs +78 -0
  59. package/src/runtime/providerHints/walletControl.mjs +39 -0
  60. package/src/runtime/risk/notional.mjs +59 -0
  61. package/src/runtime/risk/orderPreview.mjs +154 -0
  62. package/src/runtime/risk/riskRefusal.mjs +74 -0
  63. package/src/runtime/validation/actionParamsGate.mjs +2 -0
  64. 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().toLowerCase();
189
- const agentKey = String(typeof resolveAgentKey === 'function' ? resolveAgentKey(flags) : flags.agentKey || '')
190
- .trim()
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
- const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim().toLowerCase();
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
- .trim()
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
- const executionWalletAddress = String(response?.executionWalletAddress || '').trim().toLowerCase();
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().toLowerCase();
898
+ const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
856
899
  const allowWalletMismatch = parseBooleanFlag(flags.allowWalletMismatch, false);
857
900
 
858
- if (expectedWallet && executionWalletAddress && expectedWallet !== executionWalletAddress && !allowWalletMismatch) {
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 it, and
887
- // stop a write before /execute when the wallet holds 0 native coin.
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
+ }
@@ -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
- getTradingProviderHint,
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,7 +64,7 @@ export function createRuntimeHandlers(deps) {
62
64
  const __discovery = createDiscoveryCommands({
63
65
  printEnvelope,
64
66
  applyActionFilters,
65
- getTradingProviderHint,
67
+ getProviderHintForActions,
66
68
  resolveChainSupport,
67
69
  getAuthToken,
68
70
  resolveAgentApiKey,
@@ -73,6 +75,8 @@ export function createRuntimeHandlers(deps) {
73
75
  const __credits = createCreditsCommands({
74
76
  printEnvelope,
75
77
  getAuthToken,
78
+ // __wallet is created further down; resolve it when the command runs.
79
+ commandWalletExecute: (...args) => __wallet.commandWalletExecute(...args),
76
80
  });
77
81
 
78
82
  const __chat = createChatCommands({
@@ -95,6 +99,8 @@ export function createRuntimeHandlers(deps) {
95
99
  probeBitfinexPermissions,
96
100
  });
97
101
 
102
+ const __walletControl = createWalletControlCommands({ printEnvelope, buildOwnerAuthHeaders });
103
+
98
104
  const __agent = createAgentCommands({
99
105
  printEnvelope,
100
106
  extractAgentApiKey,
@@ -149,6 +155,9 @@ export function createRuntimeHandlers(deps) {
149
155
  executeSelfRun,
150
156
  submitSignedTx,
151
157
  printEnvelope,
158
+ resolveEffectiveUserDid,
159
+ resolveAgentKey,
160
+ buildOwnerAuthHeaders,
152
161
  });
153
162
 
154
163
  const __execute = createExecuteCommands({
@@ -162,6 +171,7 @@ export function createRuntimeHandlers(deps) {
162
171
  applyAvantisAgentWalletAddressDefault,
163
172
  runPreflightByMode,
164
173
  runExecutionByMode,
174
+ resolveManagedExecutionPreflight,
165
175
  applyExecutionFallbackHints,
166
176
  performSelfBroadcast,
167
177
  executeManagedWalletRun,
@@ -237,6 +247,7 @@ export function createRuntimeHandlers(deps) {
237
247
  commandActions: __discovery.commandActions,
238
248
  commandSignals: __signals.commandSignals,
239
249
  commandCreditsBalance: __discovery.commandCreditsBalance,
250
+ commandCreditsBuy: __credits.commandCreditsBuy,
240
251
  commandCreditsTransactions: __credits.commandCreditsTransactions,
241
252
  commandCreditsUsage: __credits.commandCreditsUsage,
242
253
  commandModelsList: __credits.commandModelsList,
@@ -257,6 +268,9 @@ export function createRuntimeHandlers(deps) {
257
268
  commandAgentRegister: __agent.commandAgentRegister,
258
269
  commandAgentKeyRotate: __agent.commandAgentKeyRotate,
259
270
  commandAgentKeyRevoke: __agent.commandAgentKeyRevoke,
271
+ commandWalletControlState: __walletControl.commandWalletControlState,
272
+ commandWalletControlRequest: __walletControl.commandWalletControlRequest,
273
+ commandWalletControlSubmit: __walletControl.commandWalletControlSubmit,
260
274
  commandAgentAuthChallenge: __agent.commandAgentAuthChallenge,
261
275
  commandAgentAuthVerify: __agent.commandAgentAuthVerify,
262
276
  commandAnalyticsPortfolio: __telemetry.commandAnalyticsPortfolio,
@@ -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';