@thirdfy/agent-cli 0.2.82 → 0.2.83

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,12 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.2.83] - 2026-10-08
8
+
9
+ ### Fixed
10
+
11
+ - **`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`.
12
+
7
13
  ## [0.2.82] - 2026-10-01
8
14
 
9
15
  ### 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.83
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
+ - **`preflight --run-mode agent_wallet` checks the action, not just the wallet.** The CLI sends `action` and `params` to the API, which checks agent policy, limits and venue readiness (Thirdfy API 3.19.4). A venue that is not ready (for example `HYPERLIQUID_DEPOSIT_REQUIRED`) returns `PREFLIGHT_BLOCKED` with `blockingReasons` and `venueReadiness`, matching `venue readiness <venue>`.
46
+ - **`data.policyEvaluated`** is always present, and venue actions report `data.executionVenue` (where the order settles, next to the chain the wallet was looked up on). `run` is unchanged.
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.83",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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