@thirdfy/agent-cli 0.2.32 → 0.2.34

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,21 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.34] - 2026-07-25
8
+
9
+ ### Fixed
10
+
11
+ - 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`).
12
+ - 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.
13
+
14
+ 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`.
15
+
16
+ ## [0.2.33] - 2026-07-24
17
+
18
+ ### Fixed
19
+
20
+ - `login email` always prints `executionWallets` / `executionWalletsError` and fails closed with `LOGIN_EMAIL_INCOMPLETE` when `runMode=agent_wallet` and execution wallets are missing or errored. Fund the managed execution wallet, not the owner embed.
21
+
7
22
  ## [0.2.32] - 2026-07-23
8
23
 
9
24
  ### Fixed
package/README.md CHANGED
@@ -40,9 +40,10 @@ Run without global install:
40
40
  npx @thirdfy/agent-cli --help
41
41
  ```
42
42
 
43
- ## What's new in v0.2.32
43
+ ## What's new in v0.2.34
44
44
 
45
- - Fixes `doctor certify execution` so live catalog schemas win when action names differ only by kebab vs snake case (Hyperliquid place/cancel/leverage and the same pattern on other venues).
45
+ - Read-only actions (`get_*`, `list_*`, `search_*`) requested with `--run-mode agent_wallet` now return the provider payload directly instead of an execution envelope. Market data reads such as `get_hyperliquid_perps_meta` no longer come back wrapped as an intent.
46
+ - Reads are no longer 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
47
 
47
48
  Older versions: see [CHANGELOG.md](./CHANGELOG.md) and [GitHub Releases](https://github.com/thirdfy/agent-cli/releases).
48
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -450,13 +450,18 @@ async function commandLoginEmail(ctx, flags) {
450
450
  const agentKey = extractAgentKey(data);
451
451
  const wallets = data.wallets && typeof data.wallets === 'object' ? data.wallets : {};
452
452
  const executionWallets = data.executionWallets && typeof data.executionWallets === 'object' ? data.executionWallets : {};
453
+ const executionWalletsError = data.executionWalletsError
454
+ ? String(data.executionWalletsError)
455
+ : data.execution_wallets_error
456
+ ? String(data.execution_wallets_error)
457
+ : null;
453
458
  const userDid = String(data.owner?.creatorDid || wallets.userDid || '').trim();
454
459
  const primaryEvmWallet = String(wallets.primaryEvmWallet || data.owner?.creatorWallet || '').trim();
455
460
  const persistedWallets = {
456
461
  ...wallets,
457
462
  ...(userDid ? { userDid } : {}),
458
463
  ...(primaryEvmWallet ? { primaryEvmWallet } : {}),
459
- ...(Object.keys(executionWallets).length ? { executionWallets } : {}),
464
+ executionWallets,
460
465
  };
461
466
  const runMode = normalizeRunMode(flags.runMode || current.runMode || process.env.THIRDFY_RUN_MODE || 'agent_wallet');
462
467
  const custodyMode = normalizeCustodyMode(flags.custodyMode || current.custodyMode || process.env.THIRDFY_CUSTODY_MODE, runMode);
@@ -491,7 +496,27 @@ async function commandLoginEmail(ctx, flags) {
491
496
  ...(agentKey ? { agentKey } : {}),
492
497
  };
493
498
  }
499
+ // Persist first so operators keep session tokens even when wallet provisioning failed.
494
500
  persistProfileConfig(next);
501
+
502
+ const executionWalletAddresses = Object.values(executionWallets)
503
+ .map((row) => (row && typeof row === 'object' ? String(row.walletAddress || '').trim() : ''))
504
+ .filter((addr) => /^0x[a-fA-F0-9]{40}$/.test(addr));
505
+ const hasExecutionWallet = executionWalletAddresses.length > 0;
506
+ if (runMode === 'agent_wallet' && (executionWalletsError || !hasExecutionWallet)) {
507
+ const fundHint = executionWalletAddresses[0] || 'the managed execution wallet address for each chain';
508
+ throw createCliError(
509
+ 'LOGIN_EMAIL_INCOMPLETE',
510
+ executionWalletsError
511
+ ? `Email login completed but execution wallets failed: ${executionWalletsError}. Fund the managed execution wallet (${fundHint}), not the owner embedded login wallet.`
512
+ : `Email login completed without execution wallets. Fund the managed execution wallet for agent_wallet trading (${fundHint}), not the owner embedded login wallet. Re-run login email or call /api/v1/agent/onboarding/cli/email/complete and confirm executionWallets is returned.`,
513
+ {
514
+ executionWallets,
515
+ executionWalletsError: executionWalletsError || null,
516
+ }
517
+ );
518
+ }
519
+
495
520
  printEnvelope({
496
521
  success: true,
497
522
  code: 'LOGIN_EMAIL_OK',
@@ -504,13 +529,17 @@ async function commandLoginEmail(ctx, flags) {
504
529
  evmWallets: Array.isArray(persistedWallets.evm) ? persistedWallets.evm : [],
505
530
  solanaWallets: Array.isArray(persistedWallets.solana) ? persistedWallets.solana : [],
506
531
  primaryEvmWallet: persistedWallets.primaryEvmWallet || null,
507
- executionWallets:
508
- persistedWallets.executionWallets && typeof persistedWallets.executionWallets === 'object'
509
- ? persistedWallets.executionWallets
510
- : {},
532
+ executionWallets,
533
+ executionWalletsError: executionWalletsError || null,
511
534
  ownerLinkedWallet: data.ownerLinkedWallet || null,
512
535
  configPath: getProfileConfigPath(),
513
536
  nextSteps: response?.nextSteps || [],
537
+ ...(runMode === 'agent_wallet'
538
+ ? {
539
+ fundingReminder:
540
+ 'Fund executionWallets addresses for agent_wallet. Do not fund the owner embedded wallet for Path A trading.',
541
+ }
542
+ : {}),
514
543
  },
515
544
  meta: { apiBase: ctx.apiBase },
516
545
  });
@@ -79,15 +79,29 @@ function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedActio
79
79
  return next;
80
80
  }
81
81
 
82
- function shouldFallbackAgentWalletToExecuteIntent(resolved, response) {
82
+ function isReadOnlyAction(resolved) {
83
83
  const meta = resolved?.resolvedActionMeta || {};
84
84
  const action = String(resolved?.resolvedAction || '')
85
85
  .trim()
86
86
  .toLowerCase()
87
87
  .replace(/-/g, '_');
88
- const readOnlyByName = action.startsWith('get_') || action.startsWith('search_');
88
+ const readOnlyByName =
89
+ action.startsWith('get_') || action.startsWith('search_') || action.startsWith('list_');
89
90
  const readOnlyByMeta = meta.supportsExecute === false && meta.supportsExecuteIntent !== false;
90
- if (!readOnlyByName && !readOnlyByMeta) return 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);
101
+ }
102
+
103
+ function shouldFallbackAgentWalletToExecuteIntent(resolved, response) {
104
+ if (!isReadOnlyAction(resolved)) return false;
91
105
  const err = String(response?.error || response?.message || '').toLowerCase();
92
106
  return err.includes('does not support execute rail');
93
107
  }
@@ -97,7 +111,8 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
97
111
  if (runMode === 'agent_wallet') {
98
112
  const preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
99
113
  runMode,
100
- requireFundingCheck: true,
114
+ // Reads carry no transaction; keep funding optional so preflight matches run.
115
+ requireFundingCheck: !isReadOnlyAction(resolved),
101
116
  preflightSeed: options.managedPreflight,
102
117
  });
103
118
  const normalized = normalizeIntentResponse({
@@ -304,7 +319,68 @@ async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'third
304
319
  return normalized;
305
320
  }
306
321
 
322
+ async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight) {
323
+ const userDid = resolveEffectiveUserDid(flags, {
324
+ runMode: 'agent_wallet',
325
+ hybridWalletMode: options?.hybridWalletMode,
326
+ preferDelegationIdentity: options?.preferDelegationIdentity,
327
+ });
328
+ const intentResult = await executeThirdfyRun(
329
+ ctx,
330
+ {
331
+ ...flags,
332
+ chainId: Number(flags.chainId || 8453),
333
+ runMode: 'thirdfy',
334
+ userDid: userDid || flags.userDid,
335
+ executionScope: flags.executionScope || 'solo_owner_mirror',
336
+ },
337
+ resolved,
338
+ { ...options, skipPreflight: true },
339
+ 'thirdfy'
340
+ );
341
+ intentResult.mode = 'agent_wallet';
342
+ intentResult.routeFallback = 'execute_intent_read_only';
343
+ if (preflight) {
344
+ intentResult.executionWalletPreflight = preflight;
345
+ intentResult.executionWalletAddress = preflight.executionWalletAddress || null;
346
+ }
347
+ intentResult.signerMethod = 'managed_wallet_server';
348
+ return intentResult;
349
+ }
350
+
307
351
  async function executeManagedWalletRun(ctx, flags, resolved, options) {
352
+ // Read-only actions carry no transaction. Routing them through the managed-wallet execute rail
353
+ // returns an execution envelope instead of the queried data, which silently starves callers that
354
+ // asked for market state (see earnclaw-api HL universe probe regression, 2026-07-25).
355
+ // The wallet address is still resolved for response parity, but a read is never gated on funding.
356
+ if (isReadOnlyAction(resolved)) {
357
+ // Do not swallow lookup/routing errors: writes let them propagate, and reads must fail closed
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
+ }
308
384
  const skipPreflight = Boolean(options?.skipPreflight);
309
385
  let preflight = null;
310
386
  if (!skipPreflight) {
@@ -338,34 +414,12 @@ async function executeManagedWalletRun(ctx, flags, resolved, options) {
338
414
  const actionCtx = withActionTimeout(ctx, resolved.resolvedAction);
339
415
  const response = await apiPost(actionCtx, '/api/v1/agent/execute', payload);
340
416
  if (shouldFallbackAgentWalletToExecuteIntent(resolved, response)) {
341
- const userDid = resolveEffectiveUserDid(flags, {
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;
417
+ const intentResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight);
363
418
  intentResult.executionWalletAddress =
364
419
  preflight?.executionWalletAddress ||
365
420
  response?.executionWalletAddress ||
366
421
  response?.data?.executionWalletAddress ||
367
422
  null;
368
- intentResult.signerMethod = 'managed_wallet_server';
369
423
  return intentResult;
370
424
  }
371
425
  const normalized = normalizeIntentResponse({