@thirdfy/agent-cli 0.2.33 → 0.2.35
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,27 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.35] - 2026-07-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
- `--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.
|
|
13
|
+
- `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.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
19
|
+
## [0.2.34] - 2026-07-25
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Read-only actions run under `--run-mode agent_wallet` no longer go through the managed-wallet execute rail. `run`, `preflight`, and pack callers that request `get_*`, `list_*`, or `search_*` actions (or any catalog action with `supportsExecute: false`) are routed to the read rail up front, so the provider payload is returned as-is instead of being reshaped into an execution intent envelope (`intentId`, `blocked`, `executed`).
|
|
24
|
+
- Reads on `agent_wallet` are no longer gated on execution-wallet funding. The execution wallet address is still resolved and reported for response parity, but an unfunded wallet does not block a read. Every other preflight failure (`MISSING_USER_DID`, `EXECUTION_WALLET_MISMATCH`, and other routing checks) still fails closed with `PREFLIGHT_BLOCKED`, exactly as it does for a write.
|
|
25
|
+
|
|
26
|
+
Operator impact: `get_hyperliquid_perps_meta` and `get_hyperliquid_all_mids` previously returned an envelope with the market universe buried under `data.raw`, which consumers could not parse. Hyperliquid agents fell back to a stale cached universe and reported no trading edge. Upgrade to 0.2.34 on every runtime that executes with `--run-mode agent_wallet`.
|
|
27
|
+
|
|
7
28
|
## [0.2.33] - 2026-07-24
|
|
8
29
|
|
|
9
30
|
### Fixed
|
package/README.md
CHANGED
|
@@ -40,9 +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.35
|
|
44
44
|
|
|
45
|
-
-
|
|
45
|
+
- Read-only actions return the provider payload directly in every run mode. Market data reads such as `get_hyperliquid_perps_meta` no longer come back wrapped as an execution intent, and `--run-mode self` no longer refuses them.
|
|
46
|
+
- Read detection now matches the Thirdfy action catalog, so `fetch_*`, `show_*`, `dogeos_get_*`, and `*_info` actions are recognized as reads.
|
|
47
|
+
- Reads are not gated on execution-wallet funding. The execution address is still reported, but an unfunded wallet does not block a read. Routing and identity checks still apply.
|
|
46
48
|
|
|
47
49
|
Older versions: see [CHANGELOG.md](./CHANGELOG.md) and [GitHub Releases](https://github.com/thirdfy/agent-cli/releases).
|
|
48
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);
|
|
@@ -79,15 +80,21 @@ function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedActio
|
|
|
79
80
|
return next;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
function isReadOnlyAction(resolved) {
|
|
84
|
+
return isReadOnlyResolvedAction(resolved);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Reads carry no transaction, so wallet balance never blocks them. Every other preflight failure
|
|
88
|
+
// (identity, routing, wallet mismatch) still fails closed, same as a write.
|
|
89
|
+
const FUNDING_ONLY_PREFLIGHT_BLOCKS = new Set(['INSUFFICIENT_FUNDS', 'INVALID_FUNDING_BALANCE']);
|
|
90
|
+
|
|
91
|
+
function isFundingOnlyPreflightBlock(preflight) {
|
|
92
|
+
const reason = String(preflight?.blockedReason || '').trim().toUpperCase();
|
|
93
|
+
return FUNDING_ONLY_PREFLIGHT_BLOCKS.has(reason);
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
function shouldFallbackAgentWalletToExecuteIntent(resolved, response) {
|
|
83
|
-
|
|
84
|
-
const action = String(resolved?.resolvedAction || '')
|
|
85
|
-
.trim()
|
|
86
|
-
.toLowerCase()
|
|
87
|
-
.replace(/-/g, '_');
|
|
88
|
-
const readOnlyByName = action.startsWith('get_') || action.startsWith('search_');
|
|
89
|
-
const readOnlyByMeta = meta.supportsExecute === false && meta.supportsExecuteIntent !== false;
|
|
90
|
-
if (!readOnlyByName && !readOnlyByMeta) return false;
|
|
97
|
+
if (!isReadOnlyAction(resolved)) return false;
|
|
91
98
|
const err = String(response?.error || response?.message || '').toLowerCase();
|
|
92
99
|
return err.includes('does not support execute rail');
|
|
93
100
|
}
|
|
@@ -97,7 +104,8 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
97
104
|
if (runMode === 'agent_wallet') {
|
|
98
105
|
const preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
99
106
|
runMode,
|
|
100
|
-
|
|
107
|
+
// Reads carry no transaction; keep funding optional so preflight matches run.
|
|
108
|
+
requireFundingCheck: !isReadOnlyAction(resolved),
|
|
101
109
|
preflightSeed: options.managedPreflight,
|
|
102
110
|
});
|
|
103
111
|
const normalized = normalizeIntentResponse({
|
|
@@ -124,6 +132,34 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
124
132
|
};
|
|
125
133
|
}
|
|
126
134
|
if (runMode === 'self') {
|
|
135
|
+
// Reads have no unsigned tx to prepare. Keep preflight on validation-only execute-intent so
|
|
136
|
+
// operators dry-running self mode do not trigger a real catalog read (run still uses the read rail).
|
|
137
|
+
if (isReadOnlyAction(resolved)) {
|
|
138
|
+
const payload = buildIntentPayload(flags, {
|
|
139
|
+
validationOnly: true,
|
|
140
|
+
forceIdempotency: false,
|
|
141
|
+
resolvedAction,
|
|
142
|
+
runMode: 'self',
|
|
143
|
+
mirrorOnly: false,
|
|
144
|
+
});
|
|
145
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
146
|
+
const normalized = normalizeIntentResponse(response);
|
|
147
|
+
const blockedCount = normalized.blocked || 0;
|
|
148
|
+
if (!normalized.success || blockedCount > 0) {
|
|
149
|
+
normalized.success = false;
|
|
150
|
+
normalized.error = normalized.error || 'Preflight blocked self execution';
|
|
151
|
+
normalized.preflightBlocked = true;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
message: normalized.success
|
|
155
|
+
? 'Self preflight completed'
|
|
156
|
+
: normalized.preflightBlocked
|
|
157
|
+
? 'Self preflight blocked by governance'
|
|
158
|
+
: 'Self preflight failed',
|
|
159
|
+
route: 'execute_intent_validation',
|
|
160
|
+
normalized,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
127
163
|
const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
|
|
128
164
|
const normalized = await executeSelfRun(
|
|
129
165
|
ctx,
|
|
@@ -142,43 +178,74 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
142
178
|
normalized,
|
|
143
179
|
};
|
|
144
180
|
}
|
|
145
|
-
if (runMode === 'hybrid'
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
if (runMode === 'hybrid') {
|
|
182
|
+
// Reads have nothing to mirror and need no wallet. Keep preflight on validation-only
|
|
183
|
+
// execute-intent so dry-runs 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: 'hybrid',
|
|
190
|
+
mirrorOnly: false,
|
|
191
|
+
hybridWalletMode: options.hybridWalletMode,
|
|
192
|
+
});
|
|
193
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
194
|
+
const normalized = normalizeIntentResponse(response);
|
|
195
|
+
const blockedCount = normalized.blocked || 0;
|
|
196
|
+
if (!normalized.success || blockedCount > 0) {
|
|
197
|
+
normalized.success = false;
|
|
198
|
+
normalized.error = normalized.error || 'Preflight blocked hybrid execution';
|
|
199
|
+
normalized.preflightBlocked = true;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
message: normalized.success
|
|
203
|
+
? 'Hybrid preflight completed'
|
|
204
|
+
: normalized.preflightBlocked
|
|
205
|
+
? 'Hybrid preflight blocked by governance'
|
|
206
|
+
: 'Hybrid preflight failed',
|
|
207
|
+
route: 'execute_intent_validation',
|
|
208
|
+
normalized,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (options.hybridWalletMode === 'agent_wallet') {
|
|
212
|
+
const managedPayload = buildIntentPayload(flags, {
|
|
213
|
+
validationOnly: true,
|
|
214
|
+
forceIdempotency: false,
|
|
215
|
+
resolvedAction,
|
|
216
|
+
runMode: 'agent_wallet',
|
|
217
|
+
mirrorOnly: false,
|
|
176
218
|
hybridWalletMode: 'agent_wallet',
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
219
|
+
});
|
|
220
|
+
const managedResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', managedPayload);
|
|
221
|
+
const managedNormalized = normalizeIntentResponse(managedResponse);
|
|
222
|
+
const mirrorPayload = buildIntentPayload(flags, {
|
|
223
|
+
validationOnly: true,
|
|
224
|
+
forceIdempotency: false,
|
|
225
|
+
resolvedAction,
|
|
226
|
+
runMode: 'hybrid',
|
|
227
|
+
mirrorOnly: true,
|
|
228
|
+
hybridWalletMode: 'agent_wallet',
|
|
229
|
+
});
|
|
230
|
+
const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
231
|
+
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
232
|
+
return {
|
|
233
|
+
message:
|
|
234
|
+
managedNormalized.success && mirrorNormalized.success
|
|
235
|
+
? 'Hybrid preflight completed (agent_wallet + thirdfy mirror)'
|
|
236
|
+
: 'Hybrid preflight failed (agent_wallet + thirdfy mirror)',
|
|
237
|
+
route: 'execute_intent_validation_dual',
|
|
238
|
+
normalized: normalizeIntentResponse({
|
|
239
|
+
success: Boolean(managedNormalized.success && mirrorNormalized.success),
|
|
240
|
+
status: mirrorNormalized.status || managedNormalized.status || 'failed',
|
|
241
|
+
mode: 'hybrid',
|
|
242
|
+
hybridWalletMode: 'agent_wallet',
|
|
243
|
+
managed: managedNormalized,
|
|
244
|
+
mirror: mirrorNormalized,
|
|
245
|
+
error: managedNormalized.error || mirrorNormalized.error || null,
|
|
246
|
+
}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
182
249
|
}
|
|
183
250
|
const payload = buildIntentPayload(flags, {
|
|
184
251
|
validationOnly: true,
|
|
@@ -234,14 +301,20 @@ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
|
234
301
|
if (runMode === 'self') {
|
|
235
302
|
const normalized = await executeSelfRun(ctx, flags, resolved, options);
|
|
236
303
|
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
304
|
+
const readOnlySuccess =
|
|
305
|
+
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
237
306
|
return {
|
|
238
307
|
code: normalized.success
|
|
239
|
-
?
|
|
308
|
+
? readOnlySuccess
|
|
309
|
+
? 'SELF_READ_COMPLETED'
|
|
310
|
+
: 'SELF_UNSIGNED_TX_READY'
|
|
240
311
|
: preflightBlocked
|
|
241
312
|
? 'PREFLIGHT_BLOCKED'
|
|
242
313
|
: 'SELF_EXECUTION_FAILED',
|
|
243
314
|
message: normalized.success
|
|
244
|
-
?
|
|
315
|
+
? readOnlySuccess
|
|
316
|
+
? 'Read-only action completed via delegated execute-intent'
|
|
317
|
+
: 'Unsigned transaction prepared for self-custody execution'
|
|
245
318
|
: preflightBlocked
|
|
246
319
|
? 'Preflight blocked self execution'
|
|
247
320
|
: 'Self-custody execution failed',
|
|
@@ -254,12 +327,22 @@ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
|
254
327
|
const hybridWalletMode = normalizeHybridWalletMode(
|
|
255
328
|
normalized.hybridWalletMode || options?.hybridWalletMode || flags.hybridWalletMode || 'self',
|
|
256
329
|
);
|
|
330
|
+
const readOnlySuccess =
|
|
331
|
+
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
257
332
|
return {
|
|
258
|
-
code: normalized.success
|
|
333
|
+
code: normalized.success
|
|
334
|
+
? readOnlySuccess
|
|
335
|
+
? 'HYBRID_READ_COMPLETED'
|
|
336
|
+
: 'HYBRID_READY'
|
|
337
|
+
: preflightBlocked
|
|
338
|
+
? 'PREFLIGHT_BLOCKED'
|
|
339
|
+
: 'HYBRID_FAILED',
|
|
259
340
|
message: normalized.success
|
|
260
|
-
?
|
|
261
|
-
? '
|
|
262
|
-
:
|
|
341
|
+
? readOnlySuccess
|
|
342
|
+
? 'Read-only action completed via delegated execute-intent'
|
|
343
|
+
: hybridWalletMode === 'agent_wallet'
|
|
344
|
+
? 'Hybrid execution prepared (agent wallet + mirror preflight)'
|
|
345
|
+
: 'Hybrid execution prepared (self tx + mirror preflight)'
|
|
263
346
|
: preflightBlocked
|
|
264
347
|
? 'Preflight blocked hybrid execution'
|
|
265
348
|
: 'Hybrid execution failed',
|
|
@@ -304,7 +387,68 @@ async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'third
|
|
|
304
387
|
return normalized;
|
|
305
388
|
}
|
|
306
389
|
|
|
390
|
+
async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight, reportedMode = 'agent_wallet') {
|
|
391
|
+
const userDid = resolveEffectiveUserDid(flags, {
|
|
392
|
+
runMode: 'agent_wallet',
|
|
393
|
+
hybridWalletMode: options?.hybridWalletMode,
|
|
394
|
+
preferDelegationIdentity: options?.preferDelegationIdentity,
|
|
395
|
+
});
|
|
396
|
+
const intentResult = await executeThirdfyRun(
|
|
397
|
+
ctx,
|
|
398
|
+
{
|
|
399
|
+
...flags,
|
|
400
|
+
chainId: Number(flags.chainId || 8453),
|
|
401
|
+
runMode: 'thirdfy',
|
|
402
|
+
userDid: userDid || flags.userDid,
|
|
403
|
+
executionScope: flags.executionScope || 'solo_owner_mirror',
|
|
404
|
+
},
|
|
405
|
+
resolved,
|
|
406
|
+
{ ...options, skipPreflight: true },
|
|
407
|
+
'thirdfy'
|
|
408
|
+
);
|
|
409
|
+
intentResult.mode = reportedMode;
|
|
410
|
+
intentResult.routeFallback = 'execute_intent_read_only';
|
|
411
|
+
if (preflight) {
|
|
412
|
+
intentResult.executionWalletPreflight = preflight;
|
|
413
|
+
intentResult.executionWalletAddress = preflight.executionWalletAddress || null;
|
|
414
|
+
}
|
|
415
|
+
intentResult.signerMethod = 'managed_wallet_server';
|
|
416
|
+
return intentResult;
|
|
417
|
+
}
|
|
418
|
+
|
|
307
419
|
async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
420
|
+
// Read-only actions carry no transaction. Routing them through the managed-wallet execute rail
|
|
421
|
+
// returns an execution envelope instead of the queried data, which silently starves callers that
|
|
422
|
+
// asked for market state (see earnclaw-api HL universe probe regression, 2026-07-25).
|
|
423
|
+
// The wallet address is still resolved for response parity, but a read is never gated on funding.
|
|
424
|
+
if (isReadOnlyAction(resolved)) {
|
|
425
|
+
// Do not swallow lookup/routing errors: writes let them propagate, and reads must fail closed
|
|
426
|
+
// the same way (wallet mismatch, missing identity, failed /execution-wallet).
|
|
427
|
+
const readPreflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
428
|
+
runMode: 'agent_wallet',
|
|
429
|
+
requireFundingCheck: false,
|
|
430
|
+
preflightSeed: options?.managedPreflight,
|
|
431
|
+
});
|
|
432
|
+
// Funding is intentionally skipped for reads (`requireFundingCheck: false`), but routing
|
|
433
|
+
// failures (wallet mismatch, missing identity, failed address seed) must still fail closed.
|
|
434
|
+
if (readPreflight && readPreflight.success === false && !isFundingOnlyPreflightBlock(readPreflight)) {
|
|
435
|
+
const normalized = normalizeIntentResponse({
|
|
436
|
+
success: false,
|
|
437
|
+
status: 'failed',
|
|
438
|
+
mode: 'agent_wallet',
|
|
439
|
+
blockedReason: readPreflight.blockedReason || 'PRECHECK_FAILED',
|
|
440
|
+
blockedStage: readPreflight.blockedStage || 'routing',
|
|
441
|
+
error: readPreflight.error || 'Managed wallet preflight failed',
|
|
442
|
+
preflightBlocked: true,
|
|
443
|
+
executionWalletAddress: readPreflight.executionWalletAddress || null,
|
|
444
|
+
executionWalletPreflight: readPreflight,
|
|
445
|
+
});
|
|
446
|
+
normalized.executionWalletAddress = readPreflight.executionWalletAddress || null;
|
|
447
|
+
normalized.executionWalletPreflight = readPreflight;
|
|
448
|
+
return normalized;
|
|
449
|
+
}
|
|
450
|
+
return runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, readPreflight);
|
|
451
|
+
}
|
|
308
452
|
const skipPreflight = Boolean(options?.skipPreflight);
|
|
309
453
|
let preflight = null;
|
|
310
454
|
if (!skipPreflight) {
|
|
@@ -338,34 +482,12 @@ async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
|
338
482
|
const actionCtx = withActionTimeout(ctx, resolved.resolvedAction);
|
|
339
483
|
const response = await apiPost(actionCtx, '/api/v1/agent/execute', payload);
|
|
340
484
|
if (shouldFallbackAgentWalletToExecuteIntent(resolved, response)) {
|
|
341
|
-
const
|
|
342
|
-
runMode: 'agent_wallet',
|
|
343
|
-
hybridWalletMode: options.hybridWalletMode,
|
|
344
|
-
preferDelegationIdentity: options.preferDelegationIdentity,
|
|
345
|
-
});
|
|
346
|
-
const chainId = Number(flags.chainId || 8453);
|
|
347
|
-
const intentResult = await executeThirdfyRun(
|
|
348
|
-
ctx,
|
|
349
|
-
{
|
|
350
|
-
...flags,
|
|
351
|
-
chainId,
|
|
352
|
-
runMode: 'thirdfy',
|
|
353
|
-
userDid: userDid || flags.userDid,
|
|
354
|
-
executionScope: flags.executionScope || 'solo_owner_mirror',
|
|
355
|
-
},
|
|
356
|
-
resolved,
|
|
357
|
-
{ ...options, skipPreflight: true },
|
|
358
|
-
'thirdfy'
|
|
359
|
-
);
|
|
360
|
-
intentResult.mode = 'agent_wallet';
|
|
361
|
-
intentResult.routeFallback = 'execute_intent_read_only';
|
|
362
|
-
intentResult.executionWalletPreflight = preflight;
|
|
485
|
+
const intentResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight);
|
|
363
486
|
intentResult.executionWalletAddress =
|
|
364
487
|
preflight?.executionWalletAddress ||
|
|
365
488
|
response?.executionWalletAddress ||
|
|
366
489
|
response?.data?.executionWalletAddress ||
|
|
367
490
|
null;
|
|
368
|
-
intentResult.signerMethod = 'managed_wallet_server';
|
|
369
491
|
return intentResult;
|
|
370
492
|
}
|
|
371
493
|
const normalized = normalizeIntentResponse({
|
|
@@ -395,6 +517,12 @@ async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
|
395
517
|
async function executeSelfRun(ctx, flags, resolved, options) {
|
|
396
518
|
const skipPreflight = Boolean(options?.skipPreflight);
|
|
397
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
|
+
}
|
|
398
526
|
if (!skipPreflight) {
|
|
399
527
|
const preflightPayload = buildIntentPayload(flags, {
|
|
400
528
|
validationOnly: true,
|
|
@@ -437,6 +565,14 @@ async function executeSelfRun(ctx, flags, resolved, options) {
|
|
|
437
565
|
|
|
438
566
|
async function executeHybridRun(ctx, flags, resolved, options) {
|
|
439
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
|
+
}
|
|
440
576
|
if (hybridWalletMode === 'agent_wallet') {
|
|
441
577
|
const managedResult = await executeManagedWalletRun(ctx, { ...flags, runMode: 'agent_wallet' }, resolved, options);
|
|
442
578
|
if (!managedResult.success) {
|
|
@@ -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,
|