@thirdfy/agent-cli 0.2.82 → 0.2.84

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 CHANGED
@@ -4,6 +4,22 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.2.84] - 2026-10-10
8
+
9
+ ### Added
10
+
11
+ - **Bermuda private actions and `private: true`.** Provider hints list `bermuda_stealth_exit`, `bermuda_stealth_swap` and `bermuda_sweep_burners`, the trading hint documents `private: true` on `swap`, and the earn hint documents `private: true` on `deposit_earn_position` / `withdraw_earn_position` (Thirdfy API: private swap on a fresh burner where a Bermuda pool exists; refused rather than run publicly elsewhere). `docs/providers/bermuda.md` covers the private writes.
12
+
13
+ ### Fixed
14
+
15
+ - **Private writes no longer fail the public balance check.** `run` checked the execution wallet's `tokenIn` balance before a write, so `swap` with `private: true` and `bermuda_stealth_swap` stopped with `INSUFFICIENT_FUNDS` although they spend the shielded balance. Funding checks now skip `private: true` and every Bermuda write except `bermuda_deposit`. Pin: `test/funding-token-in.test.cjs`.
16
+
17
+ ## [0.2.83] - 2026-10-08
18
+
19
+ ### Fixed
20
+
21
+ - **`preflight --run-mode agent_wallet` said `ready` for a venue order the venue could not take.** On production 2026-10-08 (API 3.19.3, CLI 0.2.82) `preflight --action place_hyperliquid_perps_order --run-mode agent_wallet` returned `PREFLIGHT_OK` with `policyEvaluated: false` and `chainId: 8453`, while `venue readiness hyperliquid` returned `HYPERLIQUID_DEPOSIT_REQUIRED`: the preflight sent `/execution-wallet` only `userDid`, `chainId`, `runMode` and `tokenIn`, so the API never saw the action. `preflight` now also sends `action`, `params` and `estimatedAmountUsd`. With Thirdfy API that supports it (capability `commandFeatures.preflightVenueReadiness`), the API checks agent policy, action limits and, for venue orders, venue readiness. A blocked answer (409 `VENUE_NOT_READY`, 403 policy, 429 limits) is `PREFLIGHT_BLOCKED` with `data.status: "blocked"`, `blockedReason`, `blockedStage`, `blockingReasons` and `venueReadiness`, instead of an HTTP error. `data.policyEvaluated` is now always present (false against an older API) and survives a later CLI-local failure (wallet mismatch, funding, native gas), whose `status` stays `failed`; only an API refusal is `blocked`. Venue actions also report `data.executionVenue` (`venue` as the API names it, `brand` for Veranta, `settlement`, `settlementChainId`, `walletLookupChainId`): `chainId` stays the chain the managed wallet was looked up on (Base by default), not where a Hyperliquid, Lighter or Polymarket order runs. `run` and the address-default lookups keep the wallet-only preflight. MCP `walletExecute` has no preflight-only mode and needs no change. Pin: `test/preflight-venue-readiness.test.cjs`.
22
+
7
23
  ## [0.2.82] - 2026-10-01
8
24
 
9
25
  ### Fixed
package/README.md CHANGED
@@ -40,10 +40,10 @@ Run without global install:
40
40
  npx @thirdfy/agent-cli --help
41
41
  ```
42
42
 
43
- ## What's new in v0.2.82
43
+ ## What's new in v0.2.84
44
44
 
45
- - **`agent_wallet` preflight shows native gas** on chains where the wallet pays gas (DogeOS 6281971): `gasSponsored`, `gasReady`, `nativeGasBalance`, from Thirdfy API 3.18.2. A write from a wallet with 0 native coin stops before `/execute` with `INSUFFICIENT_NATIVE_GAS` and the faucet link.
46
- - **DogeOS Laika buys pass preflight.** A buy spends native DOGE (`dogeAmountWei`); the CLI no longer checks the balance of the token being bought. Launch and claim are treated the same way, and a sell still checks the token it sells.
45
+ - **Private actions (Bermuda, Base Sepolia).** Hints cover `bermuda_stealth_exit`, `bermuda_stealth_swap` and `bermuda_sweep_burners`, plus `private: true` on `swap`, `deposit_earn_position` and `withdraw_earn_position` (Thirdfy API: the action runs from the shielded balance on a fresh burner where a Bermuda pool exists, or is refused; it never falls back to a public action). See [docs/providers/bermuda.md](./docs/providers/bermuda.md).
46
+ - **`run` no longer blocks private writes on the public balance check.** `private: true` and Bermuda writes (except `bermuda_deposit`) spend the shielded balance, so the execution wallet's `tokenIn` balance is not checked.
47
47
  - See [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases for older versions.
48
48
 
49
49
  ## Quick start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.2.82",
3
+ "version": "0.2.84",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,11 +24,21 @@ const POSITION_EXIT_ACTIONS = new Set([
24
24
  'dogeos_laika_claim',
25
25
  ]);
26
26
 
27
+ /**
28
+ * Shielded spends pay from the Bermuda pool, not the execution wallet: `private: true` on any action,
29
+ * and every Bermuda write except the public deposit into the pool.
30
+ */
31
+ function spendsShieldedFunds(params, actionId) {
32
+ if (params.private === true) return true;
33
+ return actionId.startsWith('bermuda_') && actionId !== 'bermuda_deposit';
34
+ }
35
+
27
36
  export function resolveTokenInForFunding(params, action) {
28
37
  if (!params || typeof params !== 'object') return '';
38
+ const actionId = normalizeActionKey(action);
39
+ if (spendsShieldedFunds(params, actionId)) return '';
29
40
  const explicit = String(params.tokenIn || '').trim();
30
41
  if (explicit) return explicit;
31
- const actionId = normalizeActionKey(action);
32
42
  if (NATIVE_ETH_SPEND_ACTIONS.has(actionId)) return '';
33
43
  if (POSITION_EXIT_ACTIONS.has(actionId)) return '';
34
44
  if (params.amountEth != null && String(params.amountEth).trim() !== '') return '';
@@ -0,0 +1,93 @@
1
+ import { toLegacyVenueActionKey } from '../../core/venueBrand.mjs';
2
+ import { resolveExecuteEstimatedAmountUsd } from './payloads.mjs';
3
+
4
+ /**
5
+ * Action-aware `agent_wallet` preflight (Thirdfy API: `GET /agent/execution-wallet?action=…`).
6
+ *
7
+ * Up to 0.2.82 the CLI sent only `userDid`, `chainId`, `runMode` and `tokenIn`, so the API never
8
+ * saw the action: `preflight --action place_hyperliquid_perps_order --run-mode agent_wallet` said
9
+ * `ready` with `policyEvaluated: false` while `venue readiness hyperliquid` said
10
+ * `HYPERLIQUID_DEPOSIT_REQUIRED` (production, 2026-10-08). Sending the action lets the API run
11
+ * agent policy, action limits and, for venue orders, venue readiness. An API without the feature
12
+ * ignores the extra query fields and answers `policyEvaluated` absent (reported as false).
13
+ */
14
+
15
+ /** Venue an action trades on, and where it settles. Null for actions outside the four venues. */
16
+ const VENUE_SETTLEMENT = Object.freeze({
17
+ hyperliquid: { venue: 'hyperliquid', settlement: 'hyperliquid_l1', settlementChainId: null },
18
+ lighter: { venue: 'lighter', settlement: 'lighter_l2', settlementChainId: null },
19
+ polymarket: { venue: 'polymarket', settlement: 'polygon', settlementChainId: 137 },
20
+ // `venue` matches the API's stored id; `brand` is the public name.
21
+ avantis: { venue: 'avantis', brand: 'veranta', settlement: 'base', settlementChainId: 8453 },
22
+ });
23
+
24
+ export function resolveActionVenue(action) {
25
+ const key = toLegacyVenueActionKey(action).replace(/-/g, '_');
26
+ for (const token of Object.keys(VENUE_SETTLEMENT)) {
27
+ if (new RegExp(`(^|_)${token}(_|$)`).test(key)) return VENUE_SETTLEMENT[token];
28
+ }
29
+ return null;
30
+ }
31
+
32
+ /**
33
+ * The venue an action executes on, next to the chain the managed wallet was looked up on. The
34
+ * managed wallet is one EVM address on every chain, so a Hyperliquid order still resolves it on the
35
+ * default chain; this says the order itself does not run there.
36
+ */
37
+ export function describeExecutionVenue(action, walletLookupChainId) {
38
+ const venue = resolveActionVenue(action);
39
+ if (!venue) return null;
40
+ return { ...venue, walletLookupChainId: Number(walletLookupChainId) || null };
41
+ }
42
+
43
+ /** Adds `action`, `params` and `estimatedAmountUsd` to an `/execution-wallet` query. */
44
+ export function appendActionPreflightQuery(query, { action, params, flags }) {
45
+ const actionId = String(action || '').trim();
46
+ if (!actionId) return query;
47
+ query.set('action', actionId);
48
+ if (params && typeof params === 'object' && !Array.isArray(params) && Object.keys(params).length) {
49
+ query.set('params', JSON.stringify(params));
50
+ }
51
+ const estimatedAmountUsd = resolveExecuteEstimatedAmountUsd(actionId, flags);
52
+ if (estimatedAmountUsd != null) query.set('estimatedAmountUsd', String(estimatedAmountUsd));
53
+ return query;
54
+ }
55
+
56
+ /** Venue fields an action preflight carries onto the CLI envelope. */
57
+ export const ACTION_PREFLIGHT_VENUE_FIELDS = Object.freeze(['venue', 'venueReadiness', 'blockingReasons']);
58
+
59
+ /** Stages at which the API refused the action itself (as opposed to a CLI-local check). */
60
+ const API_BLOCK_STAGES = new Set(['catalog', 'policy', 'venue_readiness']);
61
+
62
+ /** True when the API blocked the action (policy, catalog, venue readiness). */
63
+ export function isApiActionBlock(preflight) {
64
+ return preflight?.success === false && API_BLOCK_STAGES.has(String(preflight?.blockedStage || ''));
65
+ }
66
+
67
+ /** Policy and venue fields of an action-aware `/execution-wallet` answer (allowed or blocked). */
68
+ export function pickActionPreflightFields(response) {
69
+ const out = { policyEvaluated: response?.policyEvaluated === true };
70
+ for (const key of [...ACTION_PREFLIGHT_VENUE_FIELDS, 'actionPreflight']) {
71
+ if (response?.[key] != null) out[key] = response[key];
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /**
77
+ * A blocked action preflight arrives as a non-2xx answer (409 `VENUE_NOT_READY`, 403 policy, 429
78
+ * limits) with `preflightBlocked: true`. Turn it into a blocked preflight result; null otherwise.
79
+ */
80
+ export function blockedActionPreflightFromError(error) {
81
+ const payload = error?.payload;
82
+ if (!payload || typeof payload !== 'object' || payload.preflightBlocked !== true) return null;
83
+ return {
84
+ success: false,
85
+ blockedReason: String(payload.blockedReason || 'PREFLIGHT_BLOCKED'),
86
+ blockedStage: String(payload.blockedStage || 'policy'),
87
+ error: String(payload.error || payload.message || error?.message || 'Preflight blocked'),
88
+ statusCode: Number(error?.statusCode) || null,
89
+ signerMethod: payload.signerMethod || 'managed_wallet_server',
90
+ ...pickActionPreflightFields(payload),
91
+ ...(payload.data ? { data: payload.data } : {}),
92
+ };
93
+ }
@@ -1,5 +1,13 @@
1
1
  import { randomUUID } from 'crypto';
2
2
  import { pickNativeGasFields } from './nativeGas.mjs';
3
+ import {
4
+ ACTION_PREFLIGHT_VENUE_FIELDS,
5
+ appendActionPreflightQuery,
6
+ blockedActionPreflightFromError,
7
+ describeExecutionVenue,
8
+ isApiActionBlock,
9
+ pickActionPreflightFields,
10
+ } from './actionPreflight.mjs';
3
11
  import { requireFlag } from '../../core/args.mjs';
4
12
  import { createCliError } from '../../core/envelope.mjs';
5
13
  import {
@@ -204,6 +212,8 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
204
212
  // Reads carry no transaction; keep funding optional so preflight matches run.
205
213
  requireFundingCheck: !isReadOnlyAction(resolved),
206
214
  preflightSeed: options.managedPreflight,
215
+ // Ask the API to check policy and venue readiness for this action, not just the wallet.
216
+ evaluateAction: true,
207
217
  });
208
218
  const normalized = normalizeIntentResponse({
209
219
  success: preflight.success,
@@ -222,8 +232,21 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
222
232
  normalized.signerMethod = preflight.signerMethod || 'managed_wallet_server';
223
233
  normalized.executionWalletPreflight = preflight;
224
234
  normalized.fundingTokenBalance = preflight.fundingTokenBalance || null;
235
+ normalized.policyEvaluated = preflight.policyEvaluated === true;
236
+ for (const key of [...ACTION_PREFLIGHT_VENUE_FIELDS, 'executionVenue']) {
237
+ if (preflight[key] != null) normalized[key] = preflight[key];
238
+ }
239
+ // Only an API refusal of the action is `blocked`; CLI-local checks keep `failed`.
240
+ const apiBlocked = isApiActionBlock(preflight);
241
+ if (apiBlocked) normalized.status = 'blocked';
225
242
  return {
226
- message: preflight.success ? 'Managed wallet preflight completed' : 'Managed wallet preflight failed',
243
+ message: preflight.success
244
+ ? 'Managed wallet preflight completed'
245
+ : preflight.blockedStage === 'venue_readiness'
246
+ ? 'Managed wallet preflight blocked: venue not ready'
247
+ : apiBlocked
248
+ ? 'Managed wallet preflight blocked by policy'
249
+ : 'Managed wallet preflight failed',
227
250
  route: 'execution_wallet_preflight',
228
251
  normalized,
229
252
  };
@@ -768,22 +791,42 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
768
791
  runMode,
769
792
  });
770
793
  if (tokenIn) query.set('tokenIn', tokenIn);
794
+ const evaluateAction = options.evaluateAction === true;
795
+ if (evaluateAction) appendActionPreflightQuery(query, { action, params, flags });
796
+ const executionVenue = describeExecutionVenue(action, chainId);
771
797
  const preflightSeed = options.preflightSeed && typeof options.preflightSeed === 'object'
772
798
  ? options.preflightSeed
773
799
  : null;
774
800
  if (preflightSeed && preflightSeed.success === false) {
775
801
  return preflightSeed;
776
802
  }
777
- const response = preflightSeed
778
- ? {
803
+ // A seed (address default lookup) carries no policy answer; an action preflight asks the API again.
804
+ const useSeed = Boolean(preflightSeed) && !evaluateAction;
805
+ let response;
806
+ if (useSeed) {
807
+ response = {
779
808
  executionWalletAddress: preflightSeed.executionWalletAddress || null,
780
809
  signerMethod: preflightSeed.signerMethod || null,
781
810
  fundingTokenBalance: preflightSeed.fundingTokenBalance || null,
782
811
  ...pickNativeGasFields(preflightSeed),
812
+ };
813
+ } else {
814
+ try {
815
+ response = await apiGet(ctx, `/api/v1/agent/execution-wallet?${query.toString()}`, {
816
+ 'x-agent-api-key': agentApiKey,
817
+ });
818
+ } catch (error) {
819
+ const blocked = evaluateAction ? blockedActionPreflightFromError(error) : null;
820
+ if (!blocked) throw error;
821
+ return {
822
+ ...blocked,
823
+ userDid,
824
+ chainId,
825
+ ...(executionVenue ? { executionVenue } : {}),
826
+ };
783
827
  }
784
- : await apiGet(ctx, `/api/v1/agent/execution-wallet?${query.toString()}`, {
785
- 'x-agent-api-key': agentApiKey,
786
- });
828
+ }
829
+ const actionFields = evaluateAction ? pickActionPreflightFields(response) : {};
787
830
  const executionWalletAddress = String(response?.executionWalletAddress || '').trim().toLowerCase();
788
831
  const signerMethod = String(response?.signerMethod || 'managed_wallet_server').trim();
789
832
  const rawBalance = String(response?.fundingTokenBalance?.raw || '').trim();
@@ -803,6 +846,8 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
803
846
  executionWalletAddress,
804
847
  signerMethod,
805
848
  fundingTokenBalance: response?.fundingTokenBalance || null,
849
+ ...actionFields,
850
+ ...(executionVenue ? { executionVenue } : {}),
806
851
  };
807
852
  }
808
853
  }
@@ -819,6 +864,8 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
819
864
  executionWalletAddress,
820
865
  signerMethod,
821
866
  fundingTokenBalance: response?.fundingTokenBalance || null,
867
+ ...actionFields,
868
+ ...(executionVenue ? { executionVenue } : {}),
822
869
  };
823
870
  }
824
871
 
@@ -831,6 +878,8 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
831
878
  executionWalletAddress,
832
879
  signerMethod,
833
880
  fundingTokenBalance: response?.fundingTokenBalance || null,
881
+ ...actionFields,
882
+ ...(executionVenue ? { executionVenue } : {}),
834
883
  };
835
884
  }
836
885
 
@@ -847,6 +896,8 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
847
896
  executionWalletAddress,
848
897
  signerMethod,
849
898
  fundingTokenBalance: response?.fundingTokenBalance || null,
899
+ ...actionFields,
900
+ ...(executionVenue ? { executionVenue } : {}),
850
901
  ...nativeGas,
851
902
  };
852
903
  }
@@ -860,6 +911,8 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
860
911
  chainId,
861
912
  tokenIn: tokenIn || null,
862
913
  ...nativeGas,
914
+ ...actionFields,
915
+ ...(executionVenue ? { executionVenue } : {}),
863
916
  };
864
917
  }
865
918
 
@@ -127,7 +127,7 @@ export function createProviderHints({ getNegotiatedCapabilitiesCache }) {
127
127
  canonicalWriteAction: 'swap',
128
128
  supportedChains: [1, 10, 130, 137, 8453, 84532, 42161, 4663],
129
129
  laneCompatibility:
130
- '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.',
130
+ '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.',
131
131
  credentialUx: 'Use normal Thirdfy login/delegation; do not use this provider for Hyperliquid, Lighter, or Polymarket.',
132
132
  };
133
133
  }
@@ -512,7 +512,7 @@ export function createProviderHints({ getNegotiatedCapabilitiesCache }) {
512
512
  ],
513
513
  orderManagementActions: ['deposit_earn_position', 'withdraw_earn_position', 'claim_earn_rewards'],
514
514
  laneCompatibility:
515
- 'Generic earn writes use deposit_earn_position / withdraw_earn_position on normal Thirdfy policy/delegation rails; Yield.xyz incentive claims use claim_earn_rewards (no amount; optional passthrough from get_earn_position.claimableRewards). Routing selects Morpho, Aave, Curve, Vaults.fyi, or Yield.xyz adapters per provider/opportunity.',
515
+ 'Generic earn writes use deposit_earn_position / withdraw_earn_position on normal Thirdfy policy/delegation rails; Yield.xyz incentive claims use claim_earn_rewards (no amount; optional passthrough from get_earn_position.claimableRewards). Routing selects Morpho, Aave, Curve, Vaults.fyi, or Yield.xyz adapters per provider/opportunity. With private: true (deposit_earn_position / withdraw_earn_position) the position is entered or exited from the shielded (Bermuda) USDC balance on allowlisted vaults where a Bermuda pool exists (Base Sepolia MetaMorpho today); a private withdraw without amount exits everything; otherwise it is refused, never run publicly.',
516
516
  };
517
517
  }
518
518
  if (provider === 'capyfi') {
@@ -817,10 +817,13 @@ export function createProviderHints({ getNegotiatedCapabilitiesCache }) {
817
817
  'bermuda_transfer',
818
818
  'bermuda_withdraw',
819
819
  'bermuda_stealth_earn',
820
+ 'bermuda_stealth_exit',
821
+ 'bermuda_stealth_swap',
822
+ 'bermuda_sweep_burners',
820
823
  ],
821
824
  supportedChains: [84532],
822
825
  laneCompatibility:
823
- 'Base Sepolia shielded USDC lab rail. Catalog is flag-gated on the API host. Deposit is Privy-direct from the execution wallet. Transfer, withdraw, and stealth earn use the Bermuda relayer. Max write size is capped at 5 USD in catalog policy.',
826
+ 'Base Sepolia shielded accounts (USDC, WETH, MetaMorpho vault shares). Catalog is flag-gated on the API host. Deposit is sent from the execution wallet; every other write is proven and relayed, so the execution wallet never appears: transfer, withdraw, private vault entry/exit (stealth earn/exit), private swap (stealth swap, best of Thirdfy routes and Uniswap v3, fresh burner per trade) and burner sweep. The regular swap takes private: true for the same private swap. Writes are capped at 5 USD; the sweep has no cap. Private writes bill the write_private tier and private swaps carry a privacy fee. Responses include privacyNotes.',
824
827
  example:
825
828
  'thirdfy-agent run --action get_bermuda_setup_status --params \'{"chainId":84532}\' --provider bermuda --json',
826
829
  };