@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.
- package/CHANGELOG.md +110 -0
- package/README.md +60 -18
- package/package.json +2 -2
- package/scripts/validate-cli-contract-schemas.mjs +5 -0
- package/src/cli/errors.mjs +58 -8
- package/src/cli/manifest.mjs +54 -5
- 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 +39 -4
- 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 +119 -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/core/args.mjs +10 -2
- package/src/core/chainRegistry.mjs +223 -1
- package/src/core/context.mjs +37 -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 +16 -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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static provider hints. Prediction markets: Polymarket and the venue-neutral `prediction` provider.
|
|
3
|
+
* Each entry builds the hint for one provider id; `dispatch.mjs` picks the entry.
|
|
4
|
+
*/
|
|
5
|
+
export const PREDICTION_PROVIDER_HINTS = Object.freeze({
|
|
6
|
+
polymarket: ({ provider }) => ({
|
|
7
|
+
provider,
|
|
8
|
+
category: 'prediction_market',
|
|
9
|
+
canonicalWriteAction: 'place_polymarket_order',
|
|
10
|
+
readActions: [
|
|
11
|
+
'get_polymarket_tags',
|
|
12
|
+
'search_polymarket',
|
|
13
|
+
'get_polymarket_events',
|
|
14
|
+
'get_polymarket_event',
|
|
15
|
+
'get_polymarket_markets',
|
|
16
|
+
'get_polymarket_market',
|
|
17
|
+
'get_polymarket_order_book',
|
|
18
|
+
'get_polymarket_prices',
|
|
19
|
+
'get_polymarket_price_history',
|
|
20
|
+
'get_polymarket_funding_status',
|
|
21
|
+
'get_polymarket_setup_status',
|
|
22
|
+
'get_polymarket_onboarding_plan',
|
|
23
|
+
'get_polymarket_bridge_supported_assets',
|
|
24
|
+
'get_polymarket_bridge_status',
|
|
25
|
+
'get_polymarket_order',
|
|
26
|
+
'get_polymarket_user_orders',
|
|
27
|
+
'get_polymarket_user_positions',
|
|
28
|
+
'get_polymarket_balance_allowance',
|
|
29
|
+
],
|
|
30
|
+
discoveryFlow: [
|
|
31
|
+
'get_polymarket_tags or search_polymarket',
|
|
32
|
+
'get_polymarket_events active=true closed=false',
|
|
33
|
+
'get_polymarket_market or get_polymarket_markets',
|
|
34
|
+
'get_polymarket_order_book',
|
|
35
|
+
'get_polymarket_prices or get_polymarket_price_history',
|
|
36
|
+
'get_polymarket_funding_status with agentKey (check deposit pUSD + nested setup status)',
|
|
37
|
+
'when Base USDC is on execution wallet: create_polymarket_bridge_deposit then execute_polymarket_bridge_deposit with confirm=true',
|
|
38
|
+
'get_polymarket_bridge_status until pUSD lands',
|
|
39
|
+
'get_polymarket_setup_status with agentKey (check liveOrderReady + apiExecutionGeoblockReady) before place_polymarket_order',
|
|
40
|
+
],
|
|
41
|
+
orderManagementActions: [
|
|
42
|
+
'place_polymarket_order',
|
|
43
|
+
'get_polymarket_order',
|
|
44
|
+
'get_polymarket_user_orders',
|
|
45
|
+
'get_polymarket_user_positions',
|
|
46
|
+
'get_polymarket_balance_allowance',
|
|
47
|
+
'cancel_polymarket_order',
|
|
48
|
+
],
|
|
49
|
+
setupActions: [
|
|
50
|
+
'get_polymarket_funding_status',
|
|
51
|
+
'get_polymarket_setup_status',
|
|
52
|
+
'get_polymarket_onboarding_plan',
|
|
53
|
+
'create_polymarket_bridge_deposit',
|
|
54
|
+
'execute_polymarket_bridge_deposit',
|
|
55
|
+
'prepare_polymarket_onboarding',
|
|
56
|
+
'agent_wallet_setup:ensure_agent_wallet_deposit_wallet',
|
|
57
|
+
'agent_wallet_setup:deploy_deposit_wallet_and_set_approvals',
|
|
58
|
+
'agent_wallet_setup:create_or_derive_clob_credentials (derive miss -> create via EU egress)',
|
|
59
|
+
'browser_setup:add_privy_signer',
|
|
60
|
+
'browser_setup:create_or_derive_clob_credentials',
|
|
61
|
+
'browser_setup:deploy_deposit_wallet_and_set_approvals',
|
|
62
|
+
'agent_wallet_setup:create_or_derive_clob_credentials',
|
|
63
|
+
],
|
|
64
|
+
statusActions: [
|
|
65
|
+
'get_polymarket_funding_status',
|
|
66
|
+
'get_polymarket_setup_status',
|
|
67
|
+
'get_polymarket_recovery_status',
|
|
68
|
+
],
|
|
69
|
+
recoveryActions: [
|
|
70
|
+
'get_polymarket_recovery_status',
|
|
71
|
+
'create_polymarket_bridge_withdraw',
|
|
72
|
+
'execute_polymarket_bridge_withdraw',
|
|
73
|
+
'get_polymarket_bridge_status',
|
|
74
|
+
'withdraw_polymarket_collateral',
|
|
75
|
+
],
|
|
76
|
+
decommissionExample: 'get_polymarket_recovery_status -> create_polymarket_bridge_withdraw -> execute_polymarket_bridge_withdraw -> get_polymarket_bridge_status -> withdraw-to-user (legacy: withdraw_polymarket_collateral + Li.Fi)',
|
|
77
|
+
credentialType: 'polymarket_clob',
|
|
78
|
+
credentialUx: 'Autonomous agents use an agent-owned Privy wallet and Polymarket deposit wallet first: Thirdfy creates the agent wallet, deploys/approves the deposit wallet, and stores encrypted CLOB credentials once Polymarket key creation is available. User-owned browser setup remains optional for interactive capital.',
|
|
79
|
+
funding: 'Fund the agent-owned Polymarket deposit wallet with Polygon pUSD after deposit-wallet deployment and approvals. If users start with Base USDC, use Polymarket official Bridge (create_polymarket_bridge_deposit / execute_polymarket_bridge_deposit) from the execution wallet. Li.Fi get_bridge_quote is legacy fallback only.',
|
|
80
|
+
laneCompatibility: 'Use agent_wallet as the preferred autonomous-agent path: Thirdfy ensures the agent owner signer and deposit wallet, while users fund pUSD into that deposit wallet. Use self/BYOW only for user-owned setup/funding signatures. CLOB order placement is an offchain venue write and should use thirdfy, hybrid, or agent_wallet after polymarket_clob credentials are stored. Production API execution geoblock uses Ireland (Vercel dub1) via internal EU service — check apiExecutionGeoblockReady on setup status, not only runtime/Fly egress.',
|
|
81
|
+
setupBlockers: [
|
|
82
|
+
'POLYMARKET_EU_SERVICE_UNAVAILABLE',
|
|
83
|
+
'POLYMARKET_EU_CONTRACT_MISMATCH',
|
|
84
|
+
'POLYMARKET_GEOBLOCKED',
|
|
85
|
+
'POLYMARKET_DEPOSIT_WALLET_REQUIRED',
|
|
86
|
+
'POLYMARKET_CLOB_CREDENTIALS_REQUIRED',
|
|
87
|
+
'POLYMARKET_DEPOSIT_WALLET_DEPLOY_REQUIRED',
|
|
88
|
+
'POLYMARKET_DEPOSIT_WALLET_APPROVALS_REQUIRED',
|
|
89
|
+
'POLYMARKET_DEPOSIT_WALLET_PUSD_REQUIRED',
|
|
90
|
+
'POLYMARKET_CREDITS_REQUIRED',
|
|
91
|
+
'POLYMARKET_RELAYER_UNAVAILABLE',
|
|
92
|
+
'POLYMARKET_CLOB_KEY_CREATION_BLOCKED',
|
|
93
|
+
'POLYMARKET_CLOB_KEY_CREATION_BLOCKED_UNTIL_POLYMARKET_FIX',
|
|
94
|
+
],
|
|
95
|
+
example: 'thirdfy-agent actions --provider polymarket && thirdfy-agent preflight --action place_polymarket_order --params \'{"tokenID":"...","price":0.42,"size":10,"side":"BUY"}\'',
|
|
96
|
+
readinessFields: [
|
|
97
|
+
'liveOrderReady',
|
|
98
|
+
'apiExecutionGeoblockReady',
|
|
99
|
+
'runtimeGeoblockReady',
|
|
100
|
+
'setupNextAction',
|
|
101
|
+
'blockingReasons',
|
|
102
|
+
],
|
|
103
|
+
monetization: 'CLOB V2 builder attribution uses server-side POLYMARKET_BUILDER_CODE. Polymarket relayer gas is separate from Thirdfy credits; credits gate Thirdfy orchestration/order execution, while user/agent deposit-wallet pUSD collateral funds trades.',
|
|
104
|
+
}),
|
|
105
|
+
prediction: ({ provider }) => ({
|
|
106
|
+
provider,
|
|
107
|
+
category: 'prediction',
|
|
108
|
+
canonicalWriteAction: 'place_prediction_order',
|
|
109
|
+
readActions: [
|
|
110
|
+
'get_prediction_tags',
|
|
111
|
+
'get_prediction_markets',
|
|
112
|
+
'get_prediction_events',
|
|
113
|
+
'get_prediction_event',
|
|
114
|
+
'get_prediction_market',
|
|
115
|
+
'get_prediction_order_book',
|
|
116
|
+
'search_prediction_markets',
|
|
117
|
+
],
|
|
118
|
+
discoveryFlow: [
|
|
119
|
+
'get_prediction_tags (humans resolve tag_id once)',
|
|
120
|
+
'get_prediction_markets with numeric tag_id + end_date_min/end_date_max + liquidity_num_min',
|
|
121
|
+
'get_prediction_order_book when outcomePrices/bestAsk are missing',
|
|
122
|
+
'place_prediction_order after setup/funding/preflight',
|
|
123
|
+
'search_prediction_markets only for human text/slug lookup (not agent discovery)',
|
|
124
|
+
],
|
|
125
|
+
orderManagementActions: ['place_prediction_order', 'cancel_prediction_order'],
|
|
126
|
+
compatibilityProvider: 'polymarket',
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static provider hints. Solana families (chain 101, agent_wallet only). Same facts as thirdfy-mcp `src/tools/providers/solanaProviderHints.ts`; the catalog stays the source of truth for action names, params and chains.
|
|
3
|
+
* Each entry builds the hint for one provider id; `dispatch.mjs` picks the entry.
|
|
4
|
+
*/
|
|
5
|
+
export const SOLANA_PROVIDER_HINTS = Object.freeze({
|
|
6
|
+
jupiter: ({ provider }) => ({
|
|
7
|
+
provider,
|
|
8
|
+
category: 'onchain_spot',
|
|
9
|
+
canonicalReadAction: 'get_jupiter_quote',
|
|
10
|
+
canonicalWriteAction: 'jupiter_swap',
|
|
11
|
+
readActions: ['get_jupiter_quote'],
|
|
12
|
+
orderManagementActions: ['jupiter_swap'],
|
|
13
|
+
supportedChains: [101],
|
|
14
|
+
laneCompatibility: 'Jupiter spot swaps. Params: inputMint, outputMint (base58 mints, Jupiter-verified), amount in human units of inputMint. Solana mainnet (chainId 101) only, runMode=agent_wallet on the agent\'s managed Privy Solana wallet. self/BYOW, hybrid and thirdfy lanes are EVM-only. Fund the base58 executionWalletAddress (`thirdfy-agent login email` lists it under executionWallets; `preflight --chain-id 101 --run-mode agent_wallet` returns it) with the token you spend, and give the user that base58 address, not the EVM one. Thirdfy sponsors the network fee (gasSponsored); keep a little SOL only for rent on new token accounts (about 0.002 SOL each) and any SOL the action spends. Jupiter swaps that Jupiter does not sponsor itself still pay their fee in SOL.',
|
|
15
|
+
example: 'thirdfy-agent run --action jupiter_swap --chain-id 101 --run-mode agent_wallet --params \'{"inputMint":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v","outputMint":"So11111111111111111111111111111111111111112","amount":"5"}\' --json',
|
|
16
|
+
}),
|
|
17
|
+
solana: ({ provider }) => ({
|
|
18
|
+
provider,
|
|
19
|
+
category: 'wallet',
|
|
20
|
+
canonicalReadAction: 'get_solana_balances',
|
|
21
|
+
canonicalWriteAction: 'solana_transfer',
|
|
22
|
+
readActions: ['get_solana_balances'],
|
|
23
|
+
orderManagementActions: ['solana_transfer'],
|
|
24
|
+
supportedChains: [101],
|
|
25
|
+
laneCompatibility: 'SOL and SPL balances and transfers. solana_transfer params: toAddress (base58), amount, optional mint (omit for native SOL). Solana mainnet (chainId 101) only, runMode=agent_wallet on the agent\'s managed Privy Solana wallet. self/BYOW, hybrid and thirdfy lanes are EVM-only. Fund the base58 executionWalletAddress (`thirdfy-agent login email` lists it under executionWallets; `preflight --chain-id 101 --run-mode agent_wallet` returns it) with the token you spend, and give the user that base58 address, not the EVM one. Thirdfy sponsors the network fee (gasSponsored); keep a little SOL only for rent on new token accounts (about 0.002 SOL each) and any SOL the action spends. Jupiter swaps that Jupiter does not sponsor itself still pay their fee in SOL.',
|
|
26
|
+
}),
|
|
27
|
+
'jupiter-prediction': ({ provider }) => ({
|
|
28
|
+
provider,
|
|
29
|
+
category: 'prediction_markets',
|
|
30
|
+
canonicalReadAction: 'get_jupiter_prediction_events',
|
|
31
|
+
canonicalWriteAction: 'place_jupiter_prediction_order',
|
|
32
|
+
readActions: [
|
|
33
|
+
'get_jupiter_prediction_events',
|
|
34
|
+
'get_jupiter_prediction_market',
|
|
35
|
+
'get_jupiter_prediction_positions',
|
|
36
|
+
],
|
|
37
|
+
orderManagementActions: [
|
|
38
|
+
'place_jupiter_prediction_order',
|
|
39
|
+
'close_jupiter_prediction_position',
|
|
40
|
+
'claim_jupiter_prediction_payout',
|
|
41
|
+
],
|
|
42
|
+
supportedChains: [101],
|
|
43
|
+
laneCompatibility: 'Jupiter prediction markets. Read the event and market first; the catalog paramsSchema names the order fields. Solana mainnet (chainId 101) only, runMode=agent_wallet on the agent\'s managed Privy Solana wallet. self/BYOW, hybrid and thirdfy lanes are EVM-only. Fund the base58 executionWalletAddress (`thirdfy-agent login email` lists it under executionWallets; `preflight --chain-id 101 --run-mode agent_wallet` returns it) with the token you spend, and give the user that base58 address, not the EVM one. Thirdfy sponsors the network fee (gasSponsored); keep a little SOL only for rent on new token accounts (about 0.002 SOL each) and any SOL the action spends. Jupiter swaps that Jupiter does not sponsor itself still pay their fee in SOL.',
|
|
44
|
+
}),
|
|
45
|
+
'jupiter-lend': ({ provider }) => ({
|
|
46
|
+
provider,
|
|
47
|
+
category: 'earn',
|
|
48
|
+
canonicalReadAction: 'get_jupiter_lend_vaults',
|
|
49
|
+
canonicalWriteAction: 'jupiter_lend_deposit',
|
|
50
|
+
readActions: ['get_jupiter_lend_vaults', 'get_jupiter_lend_positions'],
|
|
51
|
+
orderManagementActions: ['jupiter_lend_deposit', 'jupiter_lend_withdraw'],
|
|
52
|
+
supportedChains: [101],
|
|
53
|
+
laneCompatibility: 'Jupiter Lend earn vaults. Writes take asset and amount. Solana mainnet (chainId 101) only, runMode=agent_wallet on the agent\'s managed Privy Solana wallet. self/BYOW, hybrid and thirdfy lanes are EVM-only. Fund the base58 executionWalletAddress (`thirdfy-agent login email` lists it under executionWallets; `preflight --chain-id 101 --run-mode agent_wallet` returns it) with the token you spend, and give the user that base58 address, not the EVM one. Thirdfy sponsors the network fee (gasSponsored); keep a little SOL only for rent on new token accounts (about 0.002 SOL each) and any SOL the action spends. Jupiter swaps that Jupiter does not sponsor itself still pay their fee in SOL.',
|
|
54
|
+
}),
|
|
55
|
+
'solana-staking': ({ provider }) => ({
|
|
56
|
+
provider,
|
|
57
|
+
category: 'staking',
|
|
58
|
+
canonicalReadAction: 'get_solana_staking_info',
|
|
59
|
+
canonicalWriteAction: 'solana_stake',
|
|
60
|
+
readActions: ['get_solana_staking_info', 'get_solana_staking_positions'],
|
|
61
|
+
orderManagementActions: ['solana_stake', 'solana_unstake', 'solana_withdraw_stake'],
|
|
62
|
+
supportedChains: [101],
|
|
63
|
+
laneCompatibility: 'Native SOL staking. solana_unstake deactivates a stake account; solana_withdraw_stake moves SOL back after deactivation completes. Solana mainnet (chainId 101) only, runMode=agent_wallet on the agent\'s managed Privy Solana wallet. self/BYOW, hybrid and thirdfy lanes are EVM-only. Fund the base58 executionWalletAddress (`thirdfy-agent login email` lists it under executionWallets; `preflight --chain-id 101 --run-mode agent_wallet` returns it) with the token you spend, and give the user that base58 address, not the EVM one. Thirdfy sponsors the network fee (gasSponsored); keep a little SOL only for rent on new token accounts (about 0.002 SOL each) and any SOL the action spends. Jupiter swaps that Jupiter does not sponsor itself still pay their fee in SOL.',
|
|
64
|
+
}),
|
|
65
|
+
'kamino-vaults': ({ provider }) => ({
|
|
66
|
+
provider,
|
|
67
|
+
category: 'earn',
|
|
68
|
+
canonicalReadAction: 'get_kamino_vaults',
|
|
69
|
+
canonicalWriteAction: 'kamino_vault_deposit',
|
|
70
|
+
readActions: ['get_kamino_vaults', 'get_kamino_vault_positions'],
|
|
71
|
+
orderManagementActions: ['kamino_vault_deposit', 'kamino_vault_withdraw'],
|
|
72
|
+
supportedChains: [101],
|
|
73
|
+
laneCompatibility: 'Kamino earn vaults. Writes take vault (base58 address from get_kamino_vaults) and amount. Solana mainnet (chainId 101) only, runMode=agent_wallet on the agent\'s managed Privy Solana wallet. self/BYOW, hybrid and thirdfy lanes are EVM-only. Fund the base58 executionWalletAddress (`thirdfy-agent login email` lists it under executionWallets; `preflight --chain-id 101 --run-mode agent_wallet` returns it) with the token you spend, and give the user that base58 address, not the EVM one. Thirdfy sponsors the network fee (gasSponsored); keep a little SOL only for rent on new token accounts (about 0.002 SOL each) and any SOL the action spends. Jupiter swaps that Jupiter does not sponsor itself still pay their fee in SOL.',
|
|
74
|
+
}),
|
|
75
|
+
meteora: ({ provider }) => ({
|
|
76
|
+
provider,
|
|
77
|
+
category: 'liquidity_provision',
|
|
78
|
+
canonicalReadAction: 'get_meteora_pools',
|
|
79
|
+
canonicalWriteAction: 'open_meteora_position',
|
|
80
|
+
readActions: ['get_meteora_pools', 'get_meteora_positions'],
|
|
81
|
+
orderManagementActions: ['open_meteora_position', 'remove_meteora_liquidity', 'claim_meteora_fees'],
|
|
82
|
+
supportedChains: [101],
|
|
83
|
+
laneCompatibility: 'Meteora liquidity positions. open_meteora_position takes pool, amountX, amountY; remove_meteora_liquidity and claim_meteora_fees take position (base58). Solana mainnet (chainId 101) only, runMode=agent_wallet on the agent\'s managed Privy Solana wallet. self/BYOW, hybrid and thirdfy lanes are EVM-only. Fund the base58 executionWalletAddress (`thirdfy-agent login email` lists it under executionWallets; `preflight --chain-id 101 --run-mode agent_wallet` returns it) with the token you spend, and give the user that base58 address, not the EVM one. Thirdfy sponsors the network fee (gasSponsored); keep a little SOL only for rent on new token accounts (about 0.002 SOL each) and any SOL the action spends. Jupiter swaps that Jupiter does not sponsor itself still pay their fee in SOL.',
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static provider hints. On-chain spot trading, bridging and FX: the `trading` provider, `bridge`, `fx` and BasedBid launches.
|
|
3
|
+
* Each entry builds the hint for one provider id; `dispatch.mjs` picks the entry.
|
|
4
|
+
*/
|
|
5
|
+
export const TRADING_PROVIDER_HINTS = Object.freeze({
|
|
6
|
+
trading: ({ provider }) => ({
|
|
7
|
+
provider,
|
|
8
|
+
category: 'onchain_spot',
|
|
9
|
+
canonicalWriteAction: 'swap',
|
|
10
|
+
supportedChains: [1, 10, 130, 137, 8453, 84532, 42161, 4663],
|
|
11
|
+
laneCompatibility: 'Kyber quote/build is live on Ethereum, Optimism, Unichain, Polygon, Base, and Arbitrum; sponsored Kyber execution is validated on Base only. Robinhood Chain (4663) uses Li.Fi Intents for spot swaps with user-pays gas. Gnosis FX uses the fx provider via CoW. Pass private: true to swap from the shielded (Bermuda) balance on a fresh burner where a Bermuda pool exists (Base Sepolia 84532 today); it fails instead of falling back to a public swap.',
|
|
12
|
+
credentialUx: 'Use normal Thirdfy login/delegation; do not use this provider for Hyperliquid, Lighter, or Polymarket.',
|
|
13
|
+
readActions: ['get_best_trading_quote', 'search_tokens', 'get_token_metadata'],
|
|
14
|
+
tokenLookup: 'A symbol swap cannot resolve answers TOKEN_NOT_FOUND. Then: 1) search_tokens { query: <symbol>, chainId } (Base 8453, Ethereum 1, Arbitrum 42161, Optimism 10, Polygon 137, Unichain 130, Gnosis 100, World Chain 480, Robinhood 4663, Solana 101, DogeOS testnet 6281971; omit chainId to search all). Candidates and risk come from market-data providers (GeckoTerminal, Jupiter, CoinMarketCap, GMGN; Laika on DogeOS). 2) Show the user every candidate: name, symbol, chain, address, liquidityUsd, risk and warnings. 3) Ask the user which one they mean. 4) Swap with that address on its chain. Never guess or construct an address. A token with risk "high", "medium" or "unknown" may be a scam copy or unchecked: tell the user its warnings and get their confirmation before the swap. To check an address the user gave, use get_token_metadata { tokenIdentifier: "0x..." or Solana mint, chainId }.',
|
|
15
|
+
example: 'thirdfy-agent run --action swap --params \'{"tokenIn":"0x...","tokenOut":"0x...","amountInRaw":"1000000"}\'',
|
|
16
|
+
}),
|
|
17
|
+
bridge: ({ provider }) => ({
|
|
18
|
+
provider,
|
|
19
|
+
category: 'bridge',
|
|
20
|
+
canonicalReadAction: 'get_bridge_quote',
|
|
21
|
+
canonicalWriteAction: 'execute_bridge',
|
|
22
|
+
readActions: ['get_bridge_quote', 'get_supported_bridge_chains', 'check_bridge_status'],
|
|
23
|
+
orderManagementActions: ['execute_bridge'],
|
|
24
|
+
supportedChains: [
|
|
25
|
+
1,
|
|
26
|
+
8453,
|
|
27
|
+
42161,
|
|
28
|
+
10,
|
|
29
|
+
137,
|
|
30
|
+
56,
|
|
31
|
+
43114,
|
|
32
|
+
250,
|
|
33
|
+
100,
|
|
34
|
+
1284,
|
|
35
|
+
1285,
|
|
36
|
+
25,
|
|
37
|
+
288,
|
|
38
|
+
1088,
|
|
39
|
+
42220,
|
|
40
|
+
122,
|
|
41
|
+
324,
|
|
42
|
+
59144,
|
|
43
|
+
534352,
|
|
44
|
+
5000,
|
|
45
|
+
81457,
|
|
46
|
+
169,
|
|
47
|
+
4663,
|
|
48
|
+
],
|
|
49
|
+
laneCompatibility: 'Li.Fi bridge supports mainnet value movement only; testnets are not auto-mapped to mainnet.',
|
|
50
|
+
}),
|
|
51
|
+
fx: ({ provider }) => ({
|
|
52
|
+
provider,
|
|
53
|
+
category: 'fx',
|
|
54
|
+
canonicalReadAction: 'get_fx_supported_assets',
|
|
55
|
+
readActions: ['get_fx_supported_assets', 'get_monerium_profile_status', 'quote_fx_swap'],
|
|
56
|
+
supportedChains: [100],
|
|
57
|
+
laneCompatibility: 'Gnosis FX: quote_fx_swap is read-only; execute swaps via trading/cow on chain 100. Monerium payments require credentials.',
|
|
58
|
+
}),
|
|
59
|
+
basedbid: ({ provider }) => ({
|
|
60
|
+
provider,
|
|
61
|
+
category: 'spot',
|
|
62
|
+
canonicalReadAction: 'basedbid_get_tokens',
|
|
63
|
+
canonicalWriteAction: 'basedbid_create_lbp',
|
|
64
|
+
readActions: ['basedbid_get_tokens'],
|
|
65
|
+
orderManagementActions: [
|
|
66
|
+
'basedbid_create_lbp',
|
|
67
|
+
'basedbid_create_flash',
|
|
68
|
+
'basedbid_create_board',
|
|
69
|
+
'basedbid_buy',
|
|
70
|
+
'basedbid_sell',
|
|
71
|
+
'basedbid_claim_fees',
|
|
72
|
+
'basedbid_update_hook',
|
|
73
|
+
],
|
|
74
|
+
supportedChains: [8453, 1, 4663],
|
|
75
|
+
laneCompatibility: 'basedbid launchpad. Discovery is read-only. LBP and flash create, buy, and sell are write-enabled on 8453, 1, and 4663. Buy spends native ETH on the target chain. Board, fee claim, and hook writes stay fail-closed. Writes take the preview-then-run path (`thirdfy-agent preview`, then `run`).',
|
|
76
|
+
example: 'thirdfy-agent run --action basedbid_get_tokens --params \'{"chainId":8453,"limit":10}\' --provider basedbid --json',
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider notes for wallets under Thirdfy API 4.0 wallet control (`walletControl.owner` is `user` or
|
|
3
|
+
* `owner_quorum`), as thirdfy-mcp `src/tools/providers/walletControlHints.ts`. On those wallets the API
|
|
4
|
+
* pays exits only to owner destinations, and Privy refuses contract calls the signing policy cannot
|
|
5
|
+
* pin (Kyber, Li.Fi, multicall wrappers, Bermuda, CCTP, Lighter). The API refuses an uncovered call
|
|
6
|
+
* before signing, so these notes say what to expect, not what is enforced here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const EXIT_DESTINATION_OWNER_ONLY_NOTE =
|
|
10
|
+
"On a wallet under wallet control (walletControl.owner user or owner_quorum), withdraw-to-user and every other exit with a destination pay only the wallet itself, the owner payout address or the owner's wallet; any other destination is refused with WITHDRAW_DESTINATION_NOT_OWNER before signing.";
|
|
11
|
+
|
|
12
|
+
const UNCOVERED_ROUTERS_NOTE =
|
|
13
|
+
'On a wallet under wallet control, Kyber and Li.Fi routes are not covered by the signing policy and are refused before signing.';
|
|
14
|
+
|
|
15
|
+
const LIGHTER_NOTE =
|
|
16
|
+
'Lighter onboarding and funding (complete_lighter_onboarding, approve_lighter_integrator and the CCTP deposit intent) need the owner key and are not supported on a wallet under wallet control.';
|
|
17
|
+
|
|
18
|
+
const MULTICALL_NOTE =
|
|
19
|
+
'On a wallet under wallet control, multicall position calls (Uniswap v3/v4 LP, Aegis) are not covered by the signing policy and are refused before signing.';
|
|
20
|
+
|
|
21
|
+
const BERMUDA_NOTE = 'On a wallet under wallet control, Bermuda pool calls are not covered by the signing policy and are refused before signing.';
|
|
22
|
+
|
|
23
|
+
const PROVIDER_NOTES = Object.freeze({
|
|
24
|
+
trading: UNCOVERED_ROUTERS_NOTE,
|
|
25
|
+
bridge: UNCOVERED_ROUTERS_NOTE,
|
|
26
|
+
lifi: UNCOVERED_ROUTERS_NOTE,
|
|
27
|
+
lighter: LIGHTER_NOTE,
|
|
28
|
+
uniswap: MULTICALL_NOTE,
|
|
29
|
+
aegis: MULTICALL_NOTE,
|
|
30
|
+
bermuda: BERMUDA_NOTE,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** The wallet-control note for a provider's hint, or undefined when none applies. */
|
|
34
|
+
export function walletControlProviderNote(provider, hint) {
|
|
35
|
+
const notes = [];
|
|
36
|
+
if (Object.prototype.hasOwnProperty.call(PROVIDER_NOTES, provider)) notes.push(PROVIDER_NOTES[provider]);
|
|
37
|
+
if (/withdraw-to-user|withdraw_to_user/.test(String(hint?.decommissionExample || ''))) notes.push(EXIT_DESTINATION_OWNER_ONLY_NOTE);
|
|
38
|
+
return notes.length ? notes.join(' ') : undefined;
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createCliError } from '../../core/envelope.mjs';
|
|
2
|
+
import { isReadOnlyResolvedAction } from '../../core/readOnlyActions.mjs';
|
|
3
|
+
import { resolveExecuteEstimatedAmountUsd } from '../execution/payloads.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The USD notional of a write (`--estimated-amount-usd`), as thirdfy-mcp `walletExecute` checks it.
|
|
7
|
+
* Spend limits are checked against it; a write sent without it is checked against $0.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const NOTIONAL_WARNING =
|
|
11
|
+
'estimatedAmountUsd was not sent for this write. Send the USD notional it moves; a later release will refuse writes without it.';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True when a write needs its USD notional: the catalog row's `behavior.requiresEstimatedAmountUsd`
|
|
15
|
+
* when the API publishes it, else true (a cancel or approval row says false).
|
|
16
|
+
*/
|
|
17
|
+
export function writeNeedsNotional(behavior) {
|
|
18
|
+
return typeof behavior?.requiresEstimatedAmountUsd === 'boolean' ? behavior.requiresEstimatedAmountUsd : true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `THIRDFY_NOTIONAL_ESTIMATE_MODE`: `refuse`, or `warn` for anything else (unset included). */
|
|
22
|
+
export function notionalEstimateMode(env = process.env) {
|
|
23
|
+
return String(env.THIRDFY_NOTIONAL_ESTIMATE_MODE || '').trim().toLowerCase() === 'refuse' ? 'refuse' : 'warn';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* For a write about to be sent with no USD notional: refuses it with ESTIMATED_AMOUNT_USD_REQUIRED
|
|
28
|
+
* in `refuse` mode (nothing is sent), and otherwise returns the warning to put in `meta.warnings`.
|
|
29
|
+
* Returns null when the write has its notional or does not need one.
|
|
30
|
+
*/
|
|
31
|
+
export function checkWriteNotional({ action, estimatedAmountUsd, behavior, env = process.env }) {
|
|
32
|
+
if (estimatedAmountUsd != null || !writeNeedsNotional(behavior)) return null;
|
|
33
|
+
if (notionalEstimateMode(env) === 'refuse') {
|
|
34
|
+
throw createCliError(
|
|
35
|
+
'ESTIMATED_AMOUNT_USD_REQUIRED',
|
|
36
|
+
`${action} is a write: send --estimated-amount-usd, the USD notional it moves (THIRDFY_NOTIONAL_ESTIMATE_MODE=refuse).`,
|
|
37
|
+
{ action },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return NOTIONAL_WARNING;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Run modes whose write carries `estimatedAmountUsd` to the API (`/execute`, `/execute-intent`). */
|
|
44
|
+
const NOTIONAL_RUN_MODES = new Set(['agent_wallet', 'thirdfy']);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The notional check for `run`, `wallet execute` and `agent run`: `meta.warnings` for a write sent
|
|
48
|
+
* without --estimated-amount-usd (empty when none applies), or a throw in `refuse` mode.
|
|
49
|
+
*/
|
|
50
|
+
export function runNotionalWarnings(resolved, flags, runMode, env = process.env) {
|
|
51
|
+
if (!NOTIONAL_RUN_MODES.has(runMode) || isReadOnlyResolvedAction(resolved)) return [];
|
|
52
|
+
const warning = checkWriteNotional({
|
|
53
|
+
action: resolved.resolvedAction,
|
|
54
|
+
estimatedAmountUsd: resolveExecuteEstimatedAmountUsd(resolved.resolvedAction, flags),
|
|
55
|
+
behavior: resolved.resolvedActionMeta?.behavior,
|
|
56
|
+
env,
|
|
57
|
+
});
|
|
58
|
+
return warning ? [warning] : [];
|
|
59
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { createCliError } from '../../core/envelope.mjs';
|
|
3
|
+
import { chainGasNote } from '../../core/chainRegistry.mjs';
|
|
4
|
+
import { findRiskRefusal, riskRefusalMessage } from './riskRefusal.mjs';
|
|
5
|
+
import { writeNeedsNotional } from './notional.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `preview`: what Thirdfy would decide about a write before it is sent, from the action-aware
|
|
9
|
+
* execution-wallet preflight (`GET /agent/execution-wallet?action=¶ms=`), which runs the same
|
|
10
|
+
* policy, venue-readiness and risk-policy checks as `/agent/execute`. Same verdict as thirdfy-mcp
|
|
11
|
+
* `previewOrder` (`src/tools/risk/previewOrder.ts`). Nothing is signed, billed or reserved.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const NO_RISK_POLICY_NOTE = 'No risk policy set; only spend limits apply.';
|
|
15
|
+
export const NO_ORDER_PREVIEW_NOTE =
|
|
16
|
+
'No orderPreview for this write: it carries no USD amount (a cancel, approval or setting), so only the action and policy checks apply.';
|
|
17
|
+
/** The API's spend preview (previewKind "spend"): its rows are the enforced spend limits. */
|
|
18
|
+
export const SPEND_PREVIEW_NOTE =
|
|
19
|
+
'No risk policy set (leverage and loss-at-stop limits); the spend limits shown are enforced. Liquidation price and loss at the stop are not computed for this order.';
|
|
20
|
+
|
|
21
|
+
const isRecord = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The preflight answer as a short verdict. `preflight` is the CLI's managed-wallet preflight result
|
|
25
|
+
* for an action (allowed, or blocked by the API); a blocked order is a verdict, not a failure.
|
|
26
|
+
*/
|
|
27
|
+
export function shapeOrderPreview({ action, chainId, preflight }) {
|
|
28
|
+
const data = isRecord(preflight) ? preflight : {};
|
|
29
|
+
const actionPreflight = isRecord(data.actionPreflight) ? data.actionPreflight : {};
|
|
30
|
+
// A chain whose wallet pays gas (DogeOS): the preflight answers success at 0 balance (gasReady
|
|
31
|
+
// false, nativeGasWarning), but the send refuses it. Solana has a note too (sponsored fees), and a
|
|
32
|
+
// Solana wallet at 0 SOL is normal there, so it is never blocked on gas here.
|
|
33
|
+
const gas = chainGasNote(chainId, { gasSponsored: data.gasSponsored });
|
|
34
|
+
const gasWarning = isRecord(data.nativeGasWarning) ? data.nativeGasWarning : undefined;
|
|
35
|
+
const noGas = gas?.sponsored === false && (data.gasReady === false || gasWarning?.code === 'INSUFFICIENT_NATIVE_GAS');
|
|
36
|
+
const preflightBlocked = data.success === false;
|
|
37
|
+
const blocked = preflightBlocked || noGas;
|
|
38
|
+
const orderPreview = isRecord(data.orderPreview)
|
|
39
|
+
? data.orderPreview
|
|
40
|
+
: isRecord(actionPreflight.orderPreview)
|
|
41
|
+
? actionPreflight.orderPreview
|
|
42
|
+
: undefined;
|
|
43
|
+
const riskRefusal = findRiskRefusal(data);
|
|
44
|
+
const amountUsd = typeof actionPreflight.amountUsd === 'number' ? actionPreflight.amountUsd : undefined;
|
|
45
|
+
// Venue warnings that do not block (Hyperliquid: a stop past the estimated liquidation price).
|
|
46
|
+
const warnings = Array.isArray(actionPreflight.warnings)
|
|
47
|
+
? actionPreflight.warnings.filter((warning) => isRecord(warning) && typeof warning.message === 'string')
|
|
48
|
+
: [];
|
|
49
|
+
const reason =
|
|
50
|
+
typeof data.error === 'string' && data.error && data.error !== data.blockedReason ? data.error : undefined;
|
|
51
|
+
const note = !orderPreview
|
|
52
|
+
? NO_ORDER_PREVIEW_NOTE
|
|
53
|
+
: orderPreview.previewKind === 'spend'
|
|
54
|
+
? SPEND_PREVIEW_NOTE
|
|
55
|
+
: orderPreview.riskPolicyEnforced === false
|
|
56
|
+
? `${NO_RISK_POLICY_NOTE} The policy rows are informational.`
|
|
57
|
+
: undefined;
|
|
58
|
+
const gasMessage = noGas ? (typeof gasWarning?.message === 'string' && gasWarning.message) || gas?.note : undefined;
|
|
59
|
+
const noteWithGas = gas && !noGas ? [note, gas.note].filter(Boolean).join(' ') : note;
|
|
60
|
+
const venueSetup = isRecord(data.venueSetup) ? data.venueSetup : actionPreflight.venueSetup;
|
|
61
|
+
return {
|
|
62
|
+
action,
|
|
63
|
+
chainId,
|
|
64
|
+
success: !blocked,
|
|
65
|
+
wouldSign: typeof orderPreview?.wouldSign === 'boolean' ? orderPreview.wouldSign && !blocked : !blocked,
|
|
66
|
+
...(preflightBlocked
|
|
67
|
+
? {
|
|
68
|
+
blockedReason: data.blockedReason || 'PREFLIGHT_BLOCKED',
|
|
69
|
+
blockedStage: data.blockedStage || 'preflight',
|
|
70
|
+
...(reason ? { reason } : {}),
|
|
71
|
+
}
|
|
72
|
+
: noGas
|
|
73
|
+
? { blockedReason: 'INSUFFICIENT_NATIVE_GAS', blockedStage: 'gas', nextAction: 'fund_native_gas' }
|
|
74
|
+
: {}),
|
|
75
|
+
...(riskRefusal
|
|
76
|
+
? { riskRefusal, message: riskRefusalMessage(riskRefusal) }
|
|
77
|
+
: gasMessage
|
|
78
|
+
? { message: gasMessage }
|
|
79
|
+
: {}),
|
|
80
|
+
...(gas ? { gas } : {}),
|
|
81
|
+
...(amountUsd != null ? { amountUsd } : {}),
|
|
82
|
+
...(orderPreview ? { orderPreview } : {}),
|
|
83
|
+
...(warnings.length ? { warnings: warnings.map((warning) => ({ code: warning.code, message: warning.message })) } : {}),
|
|
84
|
+
...(noteWithGas ? { note: noteWithGas } : {}),
|
|
85
|
+
// The venue lacks setup only: Thirdfy runs it by itself with the first order (no funds move).
|
|
86
|
+
...(isRecord(venueSetup) && venueSetup.runsWithOrder === true ? { venueSetup } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A write that moves value which Thirdfy could not size: no --estimated-amount-usd, and neither the
|
|
92
|
+
* preflight (`amountUsd`) nor the preview (`notionalUsd`) derived one from the params. The limits
|
|
93
|
+
* were checked against $0, so "would sign" would be a verdict on nothing. Throws
|
|
94
|
+
* ESTIMATED_AMOUNT_USD_REQUIRED (a request to fix, not a verdict).
|
|
95
|
+
*/
|
|
96
|
+
export function assertPreviewSized({ shaped, estimatedAmountUsd, behavior }) {
|
|
97
|
+
const sizedUsd = Math.max(
|
|
98
|
+
typeof shaped.amountUsd === 'number' ? shaped.amountUsd : 0,
|
|
99
|
+
isRecord(shaped.orderPreview) && typeof shaped.orderPreview.notionalUsd === 'number' ? shaped.orderPreview.notionalUsd : 0,
|
|
100
|
+
);
|
|
101
|
+
if (shaped.success !== true || estimatedAmountUsd != null || sizedUsd > 0 || !writeNeedsNotional(behavior)) return;
|
|
102
|
+
throw createCliError(
|
|
103
|
+
'ESTIMATED_AMOUNT_USD_REQUIRED',
|
|
104
|
+
`${shaped.action} moves value: run preview again with --estimated-amount-usd, the USD value of the order (for a swap, the USD value of what is sold), so the limits are checked against it.`,
|
|
105
|
+
{ action: shaped.action },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const SCHEMA_CONFIRMATION_KEYS = ['confirmWrites', 'confirm'];
|
|
110
|
+
|
|
111
|
+
/** True when the action's catalog `paramsSchema` lists `confirmWrites` or `confirm`. */
|
|
112
|
+
export function schemaTakesConfirmation(schema) {
|
|
113
|
+
const properties = isRecord(schema?.properties) ? schema.properties : null;
|
|
114
|
+
return Boolean(properties && SCHEMA_CONFIRMATION_KEYS.some((key) => Object.prototype.hasOwnProperty.call(properties, key)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The order a would-sign preview describes: the `run` arguments, with one idempotency key for this
|
|
119
|
+
* preview so the same order is never placed twice, and `command`, the argv to place it. Without the
|
|
120
|
+
* caller's estimate, the size the API resolved (`amountUsd`, or the preview's `notionalUsd`) stands in,
|
|
121
|
+
* so the run is not refused for it. Undefined when Thirdfy would not sign it.
|
|
122
|
+
*/
|
|
123
|
+
export function buildPreviewOrder({ shaped, params, estimatedAmountUsd, paramsSchema }) {
|
|
124
|
+
if (shaped.wouldSign !== true) return undefined;
|
|
125
|
+
// The API's amountUsd, or the venue preview's notionalUsd when only the preview sized the order
|
|
126
|
+
// (the same size assertPreviewSized accepts; thirdfy-mcp #249).
|
|
127
|
+
const sizedUsd = Math.max(
|
|
128
|
+
typeof shaped.amountUsd === 'number' ? shaped.amountUsd : 0,
|
|
129
|
+
isRecord(shaped.orderPreview) && typeof shaped.orderPreview.notionalUsd === 'number' ? shaped.orderPreview.notionalUsd : 0,
|
|
130
|
+
);
|
|
131
|
+
const usd = typeof estimatedAmountUsd === 'number' ? estimatedAmountUsd : sizedUsd > 0 ? sizedUsd : undefined;
|
|
132
|
+
const confirmWrites = schemaTakesConfirmation(paramsSchema);
|
|
133
|
+
const executionIdempotencyKey = `preview-${randomUUID()}`;
|
|
134
|
+
const command = [
|
|
135
|
+
'thirdfy-agent', 'run', '--run-mode', 'agent_wallet',
|
|
136
|
+
'--action', shaped.action,
|
|
137
|
+
'--params', JSON.stringify(params || {}),
|
|
138
|
+
'--chain-id', String(shaped.chainId),
|
|
139
|
+
...(usd !== undefined ? ['--estimated-amount-usd', String(usd)] : []),
|
|
140
|
+
'--idempotency-key', executionIdempotencyKey,
|
|
141
|
+
...(confirmWrites ? ['--confirm-writes'] : []),
|
|
142
|
+
'--json',
|
|
143
|
+
];
|
|
144
|
+
return {
|
|
145
|
+
action: shaped.action,
|
|
146
|
+
params: params || {},
|
|
147
|
+
chainId: shaped.chainId,
|
|
148
|
+
runMode: 'agent_wallet',
|
|
149
|
+
...(usd !== undefined ? { estimatedAmountUsd: usd } : {}),
|
|
150
|
+
...(confirmWrites ? { confirmWrites: true } : {}),
|
|
151
|
+
executionIdempotencyKey,
|
|
152
|
+
command,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thirdfy agent risk policy refusals (thirdfy-api-v2 `policy/risk/`), as thirdfy-mcp
|
|
3
|
+
* `src/wallet/riskRefusal.ts` shows them to agents.
|
|
4
|
+
*
|
|
5
|
+
* With a risk policy set, the API checks an order before signing and refuses one that breaks a
|
|
6
|
+
* limit with `blockedReason`, `blockedStage: 'risk_policy'` and `riskRefusal { code, field, limit,
|
|
7
|
+
* value }`. On `/agent/execute` those sit under the error body's `data`; on the `/execution-wallet`
|
|
8
|
+
* preflight at the top level or under `data`. Agents read the top level, so the CLI lifts them there
|
|
9
|
+
* with a one-line message.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const isRecord = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
13
|
+
|
|
14
|
+
/** The payload and the bodies nested under its `data` (up to three deep), outermost first. */
|
|
15
|
+
function nestedBodies(payload) {
|
|
16
|
+
const bodies = [payload];
|
|
17
|
+
let current = payload.data;
|
|
18
|
+
for (let depth = 0; depth < 3 && isRecord(current); depth += 1) {
|
|
19
|
+
bodies.push(current);
|
|
20
|
+
current = current.data;
|
|
21
|
+
}
|
|
22
|
+
return bodies;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The first `riskRefusal` with a string `code`, at the top level or under `data`. */
|
|
26
|
+
export function findRiskRefusal(payload) {
|
|
27
|
+
if (!isRecord(payload)) return undefined;
|
|
28
|
+
for (const body of nestedBodies(payload)) {
|
|
29
|
+
const candidate = body.riskRefusal;
|
|
30
|
+
if (isRecord(candidate) && typeof candidate.code === 'string' && candidate.code) return candidate;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findString(payload, field) {
|
|
36
|
+
for (const body of nestedBodies(payload)) {
|
|
37
|
+
const value = body[field];
|
|
38
|
+
if (typeof value === 'string' && value) return value;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const formatValue = (value) =>
|
|
44
|
+
value === undefined || value === null ? 'unknown' : typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
45
|
+
|
|
46
|
+
/** One line for a person or a model: `refused by risk policy: maxLeverage 5, order 10`. */
|
|
47
|
+
export function riskRefusalMessage(refusal) {
|
|
48
|
+
if (refusal.code === 'POLICY_STATE_UNREAD') {
|
|
49
|
+
const reason = typeof refusal.reason === 'string' && refusal.reason ? ` (${refusal.reason})` : '';
|
|
50
|
+
return `refused by risk policy: a value it needs could not be read${reason}; nothing was signed, retry shortly`;
|
|
51
|
+
}
|
|
52
|
+
const field = refusal.field || refusal.code;
|
|
53
|
+
return `refused by risk policy: ${field} ${formatValue(refusal.limit)}, order ${formatValue(refusal.value)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The top-level fields for a payload that carries a `riskRefusal` anywhere under it: `riskRefusal`,
|
|
58
|
+
* `blockedReason` and `errorCode` (the refusal, not the HTTP status's generic code), `blockedStage`
|
|
59
|
+
* (the nested ones when present), `retryable` (true only for POLICY_STATE_UNREAD) and `message`.
|
|
60
|
+
* Undefined when there is none.
|
|
61
|
+
*/
|
|
62
|
+
export function riskRefusalFields(payload) {
|
|
63
|
+
const refusal = findRiskRefusal(payload);
|
|
64
|
+
if (!refusal) return undefined;
|
|
65
|
+
const blockedReason = findString(payload, 'blockedReason') || refusal.code;
|
|
66
|
+
return {
|
|
67
|
+
riskRefusal: refusal,
|
|
68
|
+
blockedReason,
|
|
69
|
+
errorCode: blockedReason,
|
|
70
|
+
blockedStage: findString(payload, 'blockedStage') || 'risk_policy',
|
|
71
|
+
retryable: refusal.code === 'POLICY_STATE_UNREAD',
|
|
72
|
+
message: riskRefusalMessage(refusal),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -105,6 +105,8 @@ export function enforceCatalogActionParams(resolved, flags, getPreparedParams) {
|
|
|
105
105
|
if (result.sanitizedParams && flags) {
|
|
106
106
|
flags.__preparedParams = result.sanitizedParams;
|
|
107
107
|
}
|
|
108
|
+
// Payload builders copy --confirm-writes only into the confirmation keys this schema lists.
|
|
109
|
+
if (flags) flags.__actionParamsSchema = resolveActionParamsSchema(resolved);
|
|
108
110
|
return;
|
|
109
111
|
}
|
|
110
112
|
throw createCliError('PARAMS_SCHEMA_INVALID', result.error || 'Action params failed catalog schema validation', {
|