@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.
|
|
43
|
+
## What's new in v0.2.83
|
|
44
44
|
|
|
45
|
-
- **`agent_wallet`
|
|
46
|
-
-
|
|
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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
785
|
-
|
|
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
|
|