@thirdfy/agent-cli 0.2.34 → 0.2.36
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,29 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.36] - 2026-07-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `--run-mode agent_wallet` market-data reads use `/api/v1/agent/execute` again and keep the provider payload on the CLI envelope (`raw` plus coerced `data.result`). Version 0.2.34 routed every managed read through `/execute-intent` to avoid an execution-shaped reply, but solo Hermes `agent_wallet` intents come back as an empty queued envelope with no Hyperliquid universe. Live `/execute` still returns the meta payload (often as a JSON string under `data.result`); the CLI now parses that string so EarnClaw pack parsers can find `universe`. Execute-intent remains the fallback only when the execute rail rejects the action (`does not support execute rail`).
|
|
12
|
+
- **MCP parity:** the same rail fix belongs in `thirdfy-mcp` (`walletExecute`) as **0.0.79**. See workspace rule `cli-mcp-parity` and API docs-dev `cli-mcp-parity.md`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Reads still skip execution-wallet funding checks. Routing and identity preflight still fail closed.
|
|
17
|
+
|
|
18
|
+
## [0.2.35] - 2026-07-25
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Read-only detection now matches the Thirdfy API action manifest instead of a narrower client-side guess. `fetch_*`, `show_*`, `dogeos_get_*`, `*_info`, `dogeos_barkswap_get_quote`, `dogeos_laika_read_contract`, and `dogeos_laika_token_state` were previously classified as writes, so under `--run-mode agent_wallet` they went to the execute rail and came back as execution envelopes rather than data. A test now pins the two lists together so they cannot drift apart again.
|
|
23
|
+
- `--run-mode self` and `--run-mode hybrid` serve read-only actions from the read rail. Previously a read under `self` either failed with `SELF_MODE_REQUIRES_BUILD_TX` or, when catalog metadata was missing, asked `/api/v1/agent/build-tx` to build a transaction for a market-data query. Under `hybrid` the payload was nested beneath `managed`, where callers reading `results` could not find it, and a mirror failure could mark a successful read as failed.
|
|
24
|
+
- `normalizeIntentResponse` no longer drops top-level `data` and `raw`. It is a whitelist, and read responses carry their payload in `data`, so a caller could receive a well-formed envelope with nothing in it.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Bitfinex reads under `--run-mode hybrid` are served from the delegated rail instead of returning `HYBRID_MODE_REQUIRES_BUILD_TX` with a hint to rerun using `--run-mode thirdfy`. The guard still applies to Bitfinex writes.
|
|
29
|
+
|
|
7
30
|
## [0.2.34] - 2026-07-25
|
|
8
31
|
|
|
9
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -40,10 +40,11 @@ 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.36
|
|
44
44
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
45
|
+
- `--run-mode agent_wallet` market-data reads (`get_hyperliquid_perps_meta` and siblings) keep the live provider payload from `/api/v1/agent/execute`, including stringified `data.result` coercion, so pack callers can parse the Hyperliquid universe again.
|
|
46
|
+
- Execute-intent is only used for managed reads when the execute rail rejects the action. Solo agent_wallet intents no longer replace a successful market-data reply with an empty queue.
|
|
47
|
+
- Reads still skip funding checks; routing and identity preflight still fail closed.
|
|
47
48
|
|
|
48
49
|
Older versions: see [CHANGELOG.md](./CHANGELOG.md) and [GitHub Releases](https://github.com/thirdfy/agent-cli/releases).
|
|
49
50
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only action classification.
|
|
3
|
+
*
|
|
4
|
+
* This mirrors `isReadOnlyAction` in the Thirdfy API
|
|
5
|
+
* (`src/services/agents/execution/catalog/actionContractManifest.ts`). Keep the two in sync: when the
|
|
6
|
+
* client and the server disagree about what counts as a read, reads get sent to the managed-wallet
|
|
7
|
+
* execute rail and come back shaped as an execution envelope instead of provider data.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const READ_ONLY_PREFIXES = ['get_', 'list_', 'fetch_', 'search_', 'show_', 'dogeos_get_'];
|
|
11
|
+
|
|
12
|
+
const READ_ONLY_EXACT = new Set([
|
|
13
|
+
'dogeos_barkswap_get_quote',
|
|
14
|
+
'dogeos_laika_read_contract',
|
|
15
|
+
'dogeos_laika_token_state',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function normalizeActionKey(actionKey) {
|
|
19
|
+
return String(actionKey || '')
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/-/g, '_');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isReadOnlyActionName(actionKey) {
|
|
26
|
+
const key = normalizeActionKey(actionKey);
|
|
27
|
+
if (!key) return false;
|
|
28
|
+
if (READ_ONLY_EXACT.has(key)) return true;
|
|
29
|
+
if (key.endsWith('_info')) return true;
|
|
30
|
+
return READ_ONLY_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Catalog metadata can mark an action read-only even when the name does not match. Production
|
|
35
|
+
* defaults set `supportsExecute: true` on nearly everything, so this rarely fires on its own.
|
|
36
|
+
*/
|
|
37
|
+
export function isReadOnlyActionMeta(meta) {
|
|
38
|
+
if (!meta || typeof meta !== 'object') return false;
|
|
39
|
+
return meta.supportsExecute === false && meta.supportsExecuteIntent !== false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isReadOnlyResolvedAction(resolved, options = {}) {
|
|
43
|
+
const meta = resolved?.resolvedActionMeta;
|
|
44
|
+
if (options.requireMeta && (!meta || typeof meta !== 'object')) return false;
|
|
45
|
+
return isReadOnlyActionName(resolved?.resolvedAction) || isReadOnlyActionMeta(meta);
|
|
46
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../../core/runMode.mjs';
|
|
12
12
|
import { apiGet, apiPost } from '../../core/http.mjs';
|
|
13
13
|
import { withActionTimeout } from '../../core/actionTimeouts.mjs';
|
|
14
|
+
import { isReadOnlyResolvedAction } from '../../core/readOnlyActions.mjs';
|
|
14
15
|
import { createRequire } from 'module';
|
|
15
16
|
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
@@ -80,24 +81,7 @@ function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedActio
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function isReadOnlyAction(resolved) {
|
|
83
|
-
|
|
84
|
-
const action = String(resolved?.resolvedAction || '')
|
|
85
|
-
.trim()
|
|
86
|
-
.toLowerCase()
|
|
87
|
-
.replace(/-/g, '_');
|
|
88
|
-
const readOnlyByName =
|
|
89
|
-
action.startsWith('get_') || action.startsWith('search_') || action.startsWith('list_');
|
|
90
|
-
const readOnlyByMeta = meta.supportsExecute === false && meta.supportsExecuteIntent !== false;
|
|
91
|
-
return readOnlyByName || readOnlyByMeta;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Reads carry no transaction, so wallet balance never blocks them. Every other preflight failure
|
|
95
|
-
// (identity, routing, wallet mismatch) still fails closed, same as a write.
|
|
96
|
-
const FUNDING_ONLY_PREFLIGHT_BLOCKS = new Set(['INSUFFICIENT_FUNDS', 'INVALID_FUNDING_BALANCE']);
|
|
97
|
-
|
|
98
|
-
function isFundingOnlyPreflightBlock(preflight) {
|
|
99
|
-
const reason = String(preflight?.blockedReason || '').trim().toUpperCase();
|
|
100
|
-
return FUNDING_ONLY_PREFLIGHT_BLOCKS.has(reason);
|
|
84
|
+
return isReadOnlyResolvedAction(resolved);
|
|
101
85
|
}
|
|
102
86
|
|
|
103
87
|
function shouldFallbackAgentWalletToExecuteIntent(resolved, response) {
|
|
@@ -106,6 +90,62 @@ function shouldFallbackAgentWalletToExecuteIntent(resolved, response) {
|
|
|
106
90
|
return err.includes('does not support execute rail');
|
|
107
91
|
}
|
|
108
92
|
|
|
93
|
+
// Managed /execute often returns provider payloads as JSON strings under data.result. Pack parsers
|
|
94
|
+
// (and find_value_by_key) need a real object tree, so coerce strings that look like JSON.
|
|
95
|
+
function coerceJsonTreeValue(value) {
|
|
96
|
+
if (typeof value !== 'string') return value;
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) return value;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(trimmed);
|
|
101
|
+
} catch {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function shapeManagedExecuteReadResponse(response) {
|
|
107
|
+
if (!response || typeof response !== 'object') return response;
|
|
108
|
+
const data =
|
|
109
|
+
response.data && typeof response.data === 'object' ? { ...response.data } : response.data;
|
|
110
|
+
if (data && typeof data === 'object' && Object.prototype.hasOwnProperty.call(data, 'result')) {
|
|
111
|
+
data.result = coerceJsonTreeValue(data.result);
|
|
112
|
+
}
|
|
113
|
+
return { ...response, data };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeManagedExecuteResponse(response, { preflight, payload, isRead }) {
|
|
117
|
+
const shaped = isRead ? shapeManagedExecuteReadResponse(response) : response;
|
|
118
|
+
const normalized = normalizeIntentResponse({
|
|
119
|
+
success: Boolean(shaped?.success),
|
|
120
|
+
status: shaped?.success ? 'completed' : 'failed',
|
|
121
|
+
mode: 'agent_wallet',
|
|
122
|
+
txHash: shaped?.txHash || null,
|
|
123
|
+
blockedReason: shaped?.blockedReason || shaped?.data?.blockedReason || null,
|
|
124
|
+
blockedStage: shaped?.blockedStage || shaped?.data?.blockedStage || null,
|
|
125
|
+
error: shaped?.error || null,
|
|
126
|
+
executionWalletAddress:
|
|
127
|
+
shaped?.executionWalletAddress || shaped?.data?.executionWalletAddress || null,
|
|
128
|
+
signerMethod: 'managed_wallet_server',
|
|
129
|
+
idempotencyKey: payload.executionIdempotencyKey || null,
|
|
130
|
+
executionWalletPreflight: preflight,
|
|
131
|
+
amountNormalization:
|
|
132
|
+
shaped?.amountNormalization || shaped?.data?.amountNormalization || null,
|
|
133
|
+
// Reads must keep provider payload on the envelope. EarnClaw packs parse
|
|
134
|
+
// data.raw.data.result (see parse_thirdfy_action_result).
|
|
135
|
+
...(isRead && shaped?.data !== undefined ? { data: shaped.data } : {}),
|
|
136
|
+
raw: shaped,
|
|
137
|
+
});
|
|
138
|
+
normalized.executionWalletAddress =
|
|
139
|
+
shaped?.executionWalletAddress || shaped?.data?.executionWalletAddress || null;
|
|
140
|
+
normalized.signerMethod = 'managed_wallet_server';
|
|
141
|
+
normalized.executionWalletPreflight = preflight;
|
|
142
|
+
normalized.amountNormalization =
|
|
143
|
+
shaped?.amountNormalization || shaped?.data?.amountNormalization || null;
|
|
144
|
+
normalized.raw = shaped;
|
|
145
|
+
normalized.idempotencyKey = payload.executionIdempotencyKey || null;
|
|
146
|
+
return normalized;
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
110
150
|
const resolvedAction = resolved.resolvedAction;
|
|
111
151
|
if (runMode === 'agent_wallet') {
|
|
@@ -139,6 +179,34 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
139
179
|
};
|
|
140
180
|
}
|
|
141
181
|
if (runMode === 'self') {
|
|
182
|
+
// Reads have no unsigned tx to prepare. Keep preflight on validation-only execute-intent so
|
|
183
|
+
// operators dry-running self mode do not trigger a real catalog read (run still uses the read rail).
|
|
184
|
+
if (isReadOnlyAction(resolved)) {
|
|
185
|
+
const payload = buildIntentPayload(flags, {
|
|
186
|
+
validationOnly: true,
|
|
187
|
+
forceIdempotency: false,
|
|
188
|
+
resolvedAction,
|
|
189
|
+
runMode: 'self',
|
|
190
|
+
mirrorOnly: false,
|
|
191
|
+
});
|
|
192
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
193
|
+
const normalized = normalizeIntentResponse(response);
|
|
194
|
+
const blockedCount = normalized.blocked || 0;
|
|
195
|
+
if (!normalized.success || blockedCount > 0) {
|
|
196
|
+
normalized.success = false;
|
|
197
|
+
normalized.error = normalized.error || 'Preflight blocked self execution';
|
|
198
|
+
normalized.preflightBlocked = true;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
message: normalized.success
|
|
202
|
+
? 'Self preflight completed'
|
|
203
|
+
: normalized.preflightBlocked
|
|
204
|
+
? 'Self preflight blocked by governance'
|
|
205
|
+
: 'Self preflight failed',
|
|
206
|
+
route: 'execute_intent_validation',
|
|
207
|
+
normalized,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
142
210
|
const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
|
|
143
211
|
const normalized = await executeSelfRun(
|
|
144
212
|
ctx,
|
|
@@ -157,43 +225,74 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
157
225
|
normalized,
|
|
158
226
|
};
|
|
159
227
|
}
|
|
160
|
-
if (runMode === 'hybrid'
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
228
|
+
if (runMode === 'hybrid') {
|
|
229
|
+
// Reads have nothing to mirror and need no wallet. Keep preflight on validation-only
|
|
230
|
+
// execute-intent so dry-runs do not trigger a real catalog read (run still uses the read rail).
|
|
231
|
+
if (isReadOnlyAction(resolved)) {
|
|
232
|
+
const payload = buildIntentPayload(flags, {
|
|
233
|
+
validationOnly: true,
|
|
234
|
+
forceIdempotency: false,
|
|
235
|
+
resolvedAction,
|
|
236
|
+
runMode: 'hybrid',
|
|
237
|
+
mirrorOnly: false,
|
|
238
|
+
hybridWalletMode: options.hybridWalletMode,
|
|
239
|
+
});
|
|
240
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
241
|
+
const normalized = normalizeIntentResponse(response);
|
|
242
|
+
const blockedCount = normalized.blocked || 0;
|
|
243
|
+
if (!normalized.success || blockedCount > 0) {
|
|
244
|
+
normalized.success = false;
|
|
245
|
+
normalized.error = normalized.error || 'Preflight blocked hybrid execution';
|
|
246
|
+
normalized.preflightBlocked = true;
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
message: normalized.success
|
|
250
|
+
? 'Hybrid preflight completed'
|
|
251
|
+
: normalized.preflightBlocked
|
|
252
|
+
? 'Hybrid preflight blocked by governance'
|
|
253
|
+
: 'Hybrid preflight failed',
|
|
254
|
+
route: 'execute_intent_validation',
|
|
255
|
+
normalized,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (options.hybridWalletMode === 'agent_wallet') {
|
|
259
|
+
const managedPayload = buildIntentPayload(flags, {
|
|
260
|
+
validationOnly: true,
|
|
261
|
+
forceIdempotency: false,
|
|
262
|
+
resolvedAction,
|
|
263
|
+
runMode: 'agent_wallet',
|
|
264
|
+
mirrorOnly: false,
|
|
191
265
|
hybridWalletMode: 'agent_wallet',
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
266
|
+
});
|
|
267
|
+
const managedResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', managedPayload);
|
|
268
|
+
const managedNormalized = normalizeIntentResponse(managedResponse);
|
|
269
|
+
const mirrorPayload = buildIntentPayload(flags, {
|
|
270
|
+
validationOnly: true,
|
|
271
|
+
forceIdempotency: false,
|
|
272
|
+
resolvedAction,
|
|
273
|
+
runMode: 'hybrid',
|
|
274
|
+
mirrorOnly: true,
|
|
275
|
+
hybridWalletMode: 'agent_wallet',
|
|
276
|
+
});
|
|
277
|
+
const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
278
|
+
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
279
|
+
return {
|
|
280
|
+
message:
|
|
281
|
+
managedNormalized.success && mirrorNormalized.success
|
|
282
|
+
? 'Hybrid preflight completed (agent_wallet + thirdfy mirror)'
|
|
283
|
+
: 'Hybrid preflight failed (agent_wallet + thirdfy mirror)',
|
|
284
|
+
route: 'execute_intent_validation_dual',
|
|
285
|
+
normalized: normalizeIntentResponse({
|
|
286
|
+
success: Boolean(managedNormalized.success && mirrorNormalized.success),
|
|
287
|
+
status: mirrorNormalized.status || managedNormalized.status || 'failed',
|
|
288
|
+
mode: 'hybrid',
|
|
289
|
+
hybridWalletMode: 'agent_wallet',
|
|
290
|
+
managed: managedNormalized,
|
|
291
|
+
mirror: mirrorNormalized,
|
|
292
|
+
error: managedNormalized.error || mirrorNormalized.error || null,
|
|
293
|
+
}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
197
296
|
}
|
|
198
297
|
const payload = buildIntentPayload(flags, {
|
|
199
298
|
validationOnly: true,
|
|
@@ -249,14 +348,20 @@ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
|
249
348
|
if (runMode === 'self') {
|
|
250
349
|
const normalized = await executeSelfRun(ctx, flags, resolved, options);
|
|
251
350
|
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
351
|
+
const readOnlySuccess =
|
|
352
|
+
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
252
353
|
return {
|
|
253
354
|
code: normalized.success
|
|
254
|
-
?
|
|
355
|
+
? readOnlySuccess
|
|
356
|
+
? 'SELF_READ_COMPLETED'
|
|
357
|
+
: 'SELF_UNSIGNED_TX_READY'
|
|
255
358
|
: preflightBlocked
|
|
256
359
|
? 'PREFLIGHT_BLOCKED'
|
|
257
360
|
: 'SELF_EXECUTION_FAILED',
|
|
258
361
|
message: normalized.success
|
|
259
|
-
?
|
|
362
|
+
? readOnlySuccess
|
|
363
|
+
? 'Read-only action completed via delegated execute-intent'
|
|
364
|
+
: 'Unsigned transaction prepared for self-custody execution'
|
|
260
365
|
: preflightBlocked
|
|
261
366
|
? 'Preflight blocked self execution'
|
|
262
367
|
: 'Self-custody execution failed',
|
|
@@ -269,12 +374,22 @@ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
|
269
374
|
const hybridWalletMode = normalizeHybridWalletMode(
|
|
270
375
|
normalized.hybridWalletMode || options?.hybridWalletMode || flags.hybridWalletMode || 'self',
|
|
271
376
|
);
|
|
377
|
+
const readOnlySuccess =
|
|
378
|
+
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
272
379
|
return {
|
|
273
|
-
code: normalized.success
|
|
380
|
+
code: normalized.success
|
|
381
|
+
? readOnlySuccess
|
|
382
|
+
? 'HYBRID_READ_COMPLETED'
|
|
383
|
+
: 'HYBRID_READY'
|
|
384
|
+
: preflightBlocked
|
|
385
|
+
? 'PREFLIGHT_BLOCKED'
|
|
386
|
+
: 'HYBRID_FAILED',
|
|
274
387
|
message: normalized.success
|
|
275
|
-
?
|
|
276
|
-
? '
|
|
277
|
-
:
|
|
388
|
+
? readOnlySuccess
|
|
389
|
+
? 'Read-only action completed via delegated execute-intent'
|
|
390
|
+
: hybridWalletMode === 'agent_wallet'
|
|
391
|
+
? 'Hybrid execution prepared (agent wallet + mirror preflight)'
|
|
392
|
+
: 'Hybrid execution prepared (self tx + mirror preflight)'
|
|
278
393
|
: preflightBlocked
|
|
279
394
|
? 'Preflight blocked hybrid execution'
|
|
280
395
|
: 'Hybrid execution failed',
|
|
@@ -319,7 +434,7 @@ async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'third
|
|
|
319
434
|
return normalized;
|
|
320
435
|
}
|
|
321
436
|
|
|
322
|
-
async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight) {
|
|
437
|
+
async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight, reportedMode = 'agent_wallet') {
|
|
323
438
|
const userDid = resolveEffectiveUserDid(flags, {
|
|
324
439
|
runMode: 'agent_wallet',
|
|
325
440
|
hybridWalletMode: options?.hybridWalletMode,
|
|
@@ -338,7 +453,7 @@ async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, pre
|
|
|
338
453
|
{ ...options, skipPreflight: true },
|
|
339
454
|
'thirdfy'
|
|
340
455
|
);
|
|
341
|
-
intentResult.mode =
|
|
456
|
+
intentResult.mode = reportedMode;
|
|
342
457
|
intentResult.routeFallback = 'execute_intent_read_only';
|
|
343
458
|
if (preflight) {
|
|
344
459
|
intentResult.executionWalletPreflight = preflight;
|
|
@@ -349,44 +464,18 @@ async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, pre
|
|
|
349
464
|
}
|
|
350
465
|
|
|
351
466
|
async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
// the same way (wallet mismatch, missing identity, failed /execution-wallet).
|
|
359
|
-
const readPreflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
360
|
-
runMode: 'agent_wallet',
|
|
361
|
-
requireFundingCheck: false,
|
|
362
|
-
preflightSeed: options?.managedPreflight,
|
|
363
|
-
});
|
|
364
|
-
// Funding is intentionally skipped for reads (`requireFundingCheck: false`), but routing
|
|
365
|
-
// failures (wallet mismatch, missing identity, failed address seed) must still fail closed.
|
|
366
|
-
if (readPreflight && readPreflight.success === false && !isFundingOnlyPreflightBlock(readPreflight)) {
|
|
367
|
-
const normalized = normalizeIntentResponse({
|
|
368
|
-
success: false,
|
|
369
|
-
status: 'failed',
|
|
370
|
-
mode: 'agent_wallet',
|
|
371
|
-
blockedReason: readPreflight.blockedReason || 'PRECHECK_FAILED',
|
|
372
|
-
blockedStage: readPreflight.blockedStage || 'routing',
|
|
373
|
-
error: readPreflight.error || 'Managed wallet preflight failed',
|
|
374
|
-
preflightBlocked: true,
|
|
375
|
-
executionWalletAddress: readPreflight.executionWalletAddress || null,
|
|
376
|
-
executionWalletPreflight: readPreflight,
|
|
377
|
-
});
|
|
378
|
-
normalized.executionWalletAddress = readPreflight.executionWalletAddress || null;
|
|
379
|
-
normalized.executionWalletPreflight = readPreflight;
|
|
380
|
-
return normalized;
|
|
381
|
-
}
|
|
382
|
-
return runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, readPreflight);
|
|
383
|
-
}
|
|
467
|
+
// Market-data reads under agent_wallet must use /execute and keep the provider payload on the
|
|
468
|
+
// envelope (raw + parsed data.result). Routing them only through /execute-intent was the 0.2.34
|
|
469
|
+
// attempt to avoid a burial bug, but solo Hermes agent_wallet intents come back as an empty
|
|
470
|
+
// queued envelope with no universe. Live /execute still returns the Hyperliquid meta payload
|
|
471
|
+
// (often as a JSON string under data.result). Reads skip funding gates; routing still fails closed.
|
|
472
|
+
const isRead = isReadOnlyAction(resolved);
|
|
384
473
|
const skipPreflight = Boolean(options?.skipPreflight);
|
|
385
474
|
let preflight = null;
|
|
386
475
|
if (!skipPreflight) {
|
|
387
476
|
preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
388
477
|
runMode: 'agent_wallet',
|
|
389
|
-
requireFundingCheck:
|
|
478
|
+
requireFundingCheck: !isRead,
|
|
390
479
|
preflightSeed: options?.managedPreflight,
|
|
391
480
|
});
|
|
392
481
|
if (!preflight.success) {
|
|
@@ -422,33 +511,18 @@ async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
|
422
511
|
null;
|
|
423
512
|
return intentResult;
|
|
424
513
|
}
|
|
425
|
-
|
|
426
|
-
success: Boolean(response?.success),
|
|
427
|
-
status: response?.success ? 'completed' : 'failed',
|
|
428
|
-
mode: 'agent_wallet',
|
|
429
|
-
txHash: response?.txHash || null,
|
|
430
|
-
blockedReason: response?.blockedReason || response?.data?.blockedReason || null,
|
|
431
|
-
blockedStage: response?.blockedStage || response?.data?.blockedStage || null,
|
|
432
|
-
error: response?.error || null,
|
|
433
|
-
executionWalletAddress: response?.executionWalletAddress || response?.data?.executionWalletAddress || null,
|
|
434
|
-
signerMethod: 'managed_wallet_server',
|
|
435
|
-
idempotencyKey: payload.executionIdempotencyKey || null,
|
|
436
|
-
executionWalletPreflight: preflight,
|
|
437
|
-
amountNormalization: response?.amountNormalization || response?.data?.amountNormalization || null,
|
|
438
|
-
raw: response,
|
|
439
|
-
});
|
|
440
|
-
normalized.executionWalletAddress = response?.executionWalletAddress || response?.data?.executionWalletAddress || null;
|
|
441
|
-
normalized.signerMethod = 'managed_wallet_server';
|
|
442
|
-
normalized.executionWalletPreflight = preflight;
|
|
443
|
-
normalized.amountNormalization = response?.amountNormalization || response?.data?.amountNormalization || null;
|
|
444
|
-
normalized.raw = response;
|
|
445
|
-
normalized.idempotencyKey = payload.executionIdempotencyKey || null;
|
|
446
|
-
return normalized;
|
|
514
|
+
return normalizeManagedExecuteResponse(response, { preflight, payload, isRead });
|
|
447
515
|
}
|
|
448
516
|
|
|
449
517
|
async function executeSelfRun(ctx, flags, resolved, options) {
|
|
450
518
|
const skipPreflight = Boolean(options?.skipPreflight);
|
|
451
519
|
const blockedReasonCode = String(options?.blockedReasonCode || 'SELF_MODE_REQUIRES_BUILD_TX');
|
|
520
|
+
// A read has no transaction to build or sign, so the self lane has nothing to do with it. Without
|
|
521
|
+
// this it either fails with SELF_MODE_REQUIRES_BUILD_TX or, when catalog metadata is missing, asks
|
|
522
|
+
// /build-tx to produce a transaction for a market-data query.
|
|
523
|
+
if (isReadOnlyAction(resolved)) {
|
|
524
|
+
return runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, null, options?.runMode || 'self');
|
|
525
|
+
}
|
|
452
526
|
if (!skipPreflight) {
|
|
453
527
|
const preflightPayload = buildIntentPayload(flags, {
|
|
454
528
|
validationOnly: true,
|
|
@@ -491,6 +565,14 @@ async function executeSelfRun(ctx, flags, resolved, options) {
|
|
|
491
565
|
|
|
492
566
|
async function executeHybridRun(ctx, flags, resolved, options) {
|
|
493
567
|
const hybridWalletMode = normalizeHybridWalletMode(options?.hybridWalletMode || flags.hybridWalletMode || 'self');
|
|
568
|
+
// Reads have nothing to mirror and need no wallet, whichever hybrid wallet mode is selected.
|
|
569
|
+
// Answering from the read rail also keeps the payload at the top level, instead of nesting it under
|
|
570
|
+
// `managed` where callers that read `results` cannot find it.
|
|
571
|
+
if (isReadOnlyAction(resolved)) {
|
|
572
|
+
const readResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, null, 'hybrid');
|
|
573
|
+
readResult.hybridWalletMode = hybridWalletMode;
|
|
574
|
+
return readResult;
|
|
575
|
+
}
|
|
494
576
|
if (hybridWalletMode === 'agent_wallet') {
|
|
495
577
|
const managedResult = await executeManagedWalletRun(ctx, { ...flags, runMode: 'agent_wallet' }, resolved, options);
|
|
496
578
|
if (!managedResult.success) {
|
|
@@ -598,15 +680,19 @@ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options =
|
|
|
598
680
|
try {
|
|
599
681
|
parsedRawBalance = BigInt(rawBalance);
|
|
600
682
|
} catch {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
683
|
+
// Reads pass requireFundingCheck: false; a malformed balance must not block them.
|
|
684
|
+
// Writes still fail closed on unparseable fundingTokenBalance.raw.
|
|
685
|
+
if (options.requireFundingCheck) {
|
|
686
|
+
return {
|
|
687
|
+
success: false,
|
|
688
|
+
blockedReason: 'INVALID_FUNDING_BALANCE',
|
|
689
|
+
blockedStage: 'sizing',
|
|
690
|
+
error: `Execution wallet ${executionWalletAddress || 'unknown'} returned invalid funding token balance.`,
|
|
691
|
+
executionWalletAddress,
|
|
692
|
+
signerMethod,
|
|
693
|
+
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
610
696
|
}
|
|
611
697
|
}
|
|
612
698
|
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim().toLowerCase();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCliError } from '../../core/envelope.mjs';
|
|
2
|
+
import { isReadOnlyResolvedAction } from '../../core/readOnlyActions.mjs';
|
|
2
3
|
import {
|
|
3
4
|
sanitizeParamsForSchemaValidation,
|
|
4
5
|
validateParamsSchema,
|
|
@@ -12,15 +13,7 @@ export function resolveActionParamsSchema(resolved) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function isReadOnlyCatalogAction(resolved) {
|
|
15
|
-
|
|
16
|
-
if (!meta || typeof meta !== 'object') return false;
|
|
17
|
-
const action = String(resolved?.resolvedAction || '')
|
|
18
|
-
.trim()
|
|
19
|
-
.toLowerCase()
|
|
20
|
-
.replace(/-/g, '_');
|
|
21
|
-
const readOnlyByName = action.startsWith('get_') || action.startsWith('list_') || action.startsWith('search_');
|
|
22
|
-
const readOnlyByMeta = meta.supportsExecute === false && meta.supportsExecuteIntent !== false;
|
|
23
|
-
return readOnlyByName || readOnlyByMeta;
|
|
16
|
+
return isReadOnlyResolvedAction(resolved, { requireMeta: true });
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
export function validateResolvedActionParams(resolved, params) {
|
|
@@ -34,7 +34,14 @@ function normalizeIntentResponse(response) {
|
|
|
34
34
|
const topLevelDelegationSignal = String(response?.delegationSignal || '').trim() || null;
|
|
35
35
|
const policyEvaluated = response?.policyEvaluated === true;
|
|
36
36
|
const preflightBlocked = response?.preflightBlocked === true;
|
|
37
|
+
// This function is a whitelist, so any provider payload outside the known keys is dropped. Read
|
|
38
|
+
// actions return their data in `data` (and sometimes `raw`) rather than in `results`, so those two
|
|
39
|
+
// must survive or the caller gets a well-formed envelope with nothing in it.
|
|
40
|
+
const passthrough = {};
|
|
41
|
+
if (response?.data !== undefined) passthrough.data = response.data;
|
|
42
|
+
if (response?.raw !== undefined) passthrough.raw = response.raw;
|
|
37
43
|
return {
|
|
44
|
+
...passthrough,
|
|
38
45
|
success,
|
|
39
46
|
status: response?.status || (success ? 'queued' : 'failed'),
|
|
40
47
|
intentId: response?.intentId || null,
|