@agentlayer.tech/wallet 0.1.34 → 0.1.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.
@@ -2,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
5
- "version": "0.1.34",
5
+ "version": "0.1.35",
6
6
  "contracts": {
7
7
  "tools": [
8
8
  "close_empty_token_accounts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayertech/agent-wallet-plugin",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN ../../../LICENSE",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.35 - 2026-06-03
6
+
7
+ - Hardened the Codex / Claude Code MCP wallet bridge:
8
+ - Run the blocking wallet CLI subprocess off the event loop
9
+ (`asyncio.to_thread`) so a slow or hung wallet op no longer freezes the MCP
10
+ server — `tools/list`, read-only calls, and cancellation stay responsive.
11
+ The approval-preview cache is now guarded by a lock for concurrent calls.
12
+ - Consume the approved preview after a successful execute, so a duplicate
13
+ execute call cannot silently re-run the operation from stale 15-min approval
14
+ context. A failed execute keeps the preview so retries still work.
15
+ - `set_wallet_backend` commits session network/backend selection only after
16
+ the validating call succeeds, preventing a stale backend + new network mix.
17
+ - Fixed an off-by-one in the Claude Code launcher's sibling-codex fallback
18
+ path so a local checkout resolves `codex/server.py`.
19
+ - Replaced the `py_compile` self-check with a non-writing `ast.parse` check so
20
+ a read-only install dir cannot trigger a false "runtime broken" error.
21
+ - Parse `AGENT_WALLET_CODEX_TIMEOUT` defensively and surface a clean timeout
22
+ error instead of the raw `TimeoutExpired` repr (which echoed argv, including
23
+ the approval token). CLI stdout parsing now tolerates a stray line ahead of
24
+ the JSON result.
25
+ - Codex skill doc parity (`set_wallet_backend` / `set_evm_network`).
26
+
5
27
  ## v0.1.34 - 2026-06-02
6
28
 
7
29
  - Added native ETH support to EVM (Velora) swaps in the wallet runtime.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.34
1
+ 0.1.35
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Keep in sync with package.json, pyproject.toml, and the npm installer version.
4
4
  # scripts/check_release_version.mjs enforces this on release.
5
- __version__ = "0.1.34"
5
+ __version__ = "0.1.35"
6
6
 
7
7
  __all__ = [
8
8
  "config",
@@ -2,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
5
- "version": "0.1.34",
5
+ "version": "0.1.35",
6
6
  "skills": ["skills/wallet-operator"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.34"
7
+ version = "0.1.35"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.34",
4
+ "version": "0.1.35",
5
5
  "description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
6
6
  "author": {
7
7
  "name": "AgentLayer"
@@ -10,13 +10,13 @@ PACKAGE_ROOT=${AGENT_WALLET_PACKAGE_ROOT:-${OPENCLAW_AGENT_WALLET_PACKAGE_ROOT:-
10
10
  # relative sibling paths below no longer resolve, so fall back to the codex
11
11
  # plugin copy inside the installed runtime package, which is always present.
12
12
  LOCAL_SERVER="$PLUGIN_ROOT/server.py"
13
- CODEX_SERVER="$PLUGIN_ROOT/../../codex/plugins/agent-wallet/server.py"
13
+ CODEX_SERVER="$PLUGIN_ROOT/../../../codex/plugins/agent-wallet/server.py"
14
14
  RUNTIME_CODEX_DIR="$OPENCLAW_HOME/agent-wallet-runtime/current/codex/plugins/agent-wallet"
15
15
 
16
16
  if [ -f "$LOCAL_SERVER" ]; then
17
17
  SERVER_PY="$LOCAL_SERVER"
18
18
  elif [ -f "$CODEX_SERVER" ]; then
19
- SERVER_PY=$(CDPATH= cd -- "$PLUGIN_ROOT/../../codex/plugins/agent-wallet" && pwd)/server.py
19
+ SERVER_PY=$(CDPATH= cd -- "$PLUGIN_ROOT/../../../codex/plugins/agent-wallet" && pwd)/server.py
20
20
  elif [ -f "$RUNTIME_CODEX_DIR/server.py" ]; then
21
21
  SERVER_PY=$(CDPATH= cd -- "$RUNTIME_CODEX_DIR" && pwd)/server.py
22
22
  else
@@ -37,7 +37,9 @@ else
37
37
  fi
38
38
 
39
39
  # Fail loudly (not -32000) if the resolved server cannot even be parsed.
40
- if ! "$PYTHON_BIN" -m py_compile "$SERVER_PY" 2>/dev/null; then
40
+ # Use ast.parse (no bytecode written) so a read-only install dir cannot trigger
41
+ # a false "runtime broken" error from py_compile failing to write __pycache__.
42
+ if ! "$PYTHON_BIN" -c 'import sys, ast; ast.parse(open(sys.argv[1], encoding="utf-8").read())' "$SERVER_PY" 2>/dev/null; then
41
43
  "$PYTHON_BIN" - "$SERVER_PY" >&2 <<'PY'
42
44
  import json, sys
43
45
  print(json.dumps({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "Codex plugin bridge for the AgentLayer wallet runtime.",
5
5
  "author": {
6
6
  "name": "AgentLayer"
@@ -24,7 +24,9 @@ if [ ! -f "$PLUGIN_ROOT/server.py" ]; then
24
24
  fi
25
25
 
26
26
  # Fail loudly (not -32000) if the resolved server cannot even be parsed.
27
- if ! "$PYTHON_BIN" -m py_compile "$PLUGIN_ROOT/server.py" 2>/dev/null; then
27
+ # Use ast.parse (no bytecode written) so a read-only install dir cannot trigger
28
+ # a false "runtime broken" error from py_compile failing to write __pycache__.
29
+ if ! "$PYTHON_BIN" -c 'import sys, ast; ast.parse(open(sys.argv[1], encoding="utf-8").read())' "$PLUGIN_ROOT/server.py" 2>/dev/null; then
28
30
  "$PYTHON_BIN" - "$PLUGIN_ROOT/server.py" >&2 <<'PY'
29
31
  import json, sys
30
32
  print(json.dumps({
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import copy
6
7
  import base64
7
8
  import hashlib
@@ -9,6 +10,7 @@ import json
9
10
  import os
10
11
  import subprocess
11
12
  import sys
13
+ import threading
12
14
  import time
13
15
  from functools import lru_cache
14
16
  from pathlib import Path
@@ -71,6 +73,9 @@ selected_solana_network: str | None = None
71
73
  selected_evm_network: str | None = None
72
74
  selected_btc_network: str | None = None
73
75
  approval_preview_cache: dict[str, dict[str, Any]] = {}
76
+ # Guards approval_preview_cache against races once wallet calls run concurrently
77
+ # via asyncio.to_thread. Reentrant so prune helpers can be nested under writers.
78
+ _approval_cache_lock = threading.RLock()
74
79
 
75
80
 
76
81
  class WalletCliError(RuntimeError):
@@ -94,6 +99,8 @@ def _openclaw_home() -> Path:
94
99
 
95
100
  @lru_cache(maxsize=1)
96
101
  def _openclaw_plugin_config() -> dict[str, Any]:
102
+ # Cached for the process lifetime: openclaw.json is read once per MCP server
103
+ # start. Edits to plugin config require restarting the bridge to take effect.
97
104
  config_path = _openclaw_home() / "openclaw.json"
98
105
  try:
99
106
  payload = json.loads(config_path.read_text(encoding="utf-8"))
@@ -212,9 +219,10 @@ def _approval_preview_tool_name(tool_name: str) -> str:
212
219
 
213
220
  def _prune_approval_preview_cache() -> None:
214
221
  now = time.time()
215
- for key in list(approval_preview_cache):
216
- if float(approval_preview_cache[key].get("expires_at") or 0) <= now:
217
- approval_preview_cache.pop(key, None)
222
+ with _approval_cache_lock:
223
+ for key in list(approval_preview_cache):
224
+ if float(approval_preview_cache[key].get("expires_at") or 0) <= now:
225
+ approval_preview_cache.pop(key, None)
218
226
 
219
227
 
220
228
  def _cache_preview_for_approval(user_id: str, tool_name: str, payload: dict[str, Any]) -> None:
@@ -231,18 +239,35 @@ def _cache_preview_for_approval(user_id: str, tool_name: str, payload: dict[str,
231
239
  summary = data.get("confirmation_summary")
232
240
  if not isinstance(summary, dict):
233
241
  return
234
- _prune_approval_preview_cache()
235
- approval_preview_cache[_approval_cache_key(user_id, cache_tool_name)] = {
236
- "digest": _preview_digest(data),
237
- "expires_at": time.time() + PREVIEW_CACHE_TTL_SECONDS,
238
- "preview": data,
239
- "summary": summary,
240
- }
242
+ with _approval_cache_lock:
243
+ _prune_approval_preview_cache()
244
+ approval_preview_cache[_approval_cache_key(user_id, cache_tool_name)] = {
245
+ "digest": _preview_digest(data),
246
+ "expires_at": time.time() + PREVIEW_CACHE_TTL_SECONDS,
247
+ "preview": data,
248
+ "summary": summary,
249
+ }
241
250
 
242
251
 
243
252
  def _latest_cached_preview(user_id: str, tool_name: str) -> dict[str, Any] | None:
244
- _prune_approval_preview_cache()
245
- return approval_preview_cache.get(_approval_cache_key(user_id, _approval_preview_tool_name(tool_name)))
253
+ with _approval_cache_lock:
254
+ _prune_approval_preview_cache()
255
+ return approval_preview_cache.get(_approval_cache_key(user_id, _approval_preview_tool_name(tool_name)))
256
+
257
+
258
+ def _consume_cached_preview(user_id: str, tool_name: str) -> None:
259
+ """Drop the cached preview once a successful execute has consumed it.
260
+
261
+ Without this, a preview lingers for the full TTL and a duplicate execute
262
+ call could re-run the operation from stale approval context — the runtime's
263
+ single-use nonce registry cannot stop it, because the bridge runs each
264
+ invoke in a fresh subprocess (empty registry) and mints a new token per
265
+ execute. Requiring a fresh preview before the next execute is the safe rule.
266
+ """
267
+ with _approval_cache_lock:
268
+ approval_preview_cache.pop(
269
+ _approval_cache_key(user_id, _approval_preview_tool_name(tool_name)), None
270
+ )
246
271
 
247
272
 
248
273
  def _approval_token_preview_digest(token: str) -> str:
@@ -509,28 +534,55 @@ def _parse_cli_error(text: str) -> WalletCliError:
509
534
  )
510
535
 
511
536
 
537
+ def _cli_timeout_seconds() -> float:
538
+ """Parse the CLI timeout from env, falling back to 180s on bad values."""
539
+ raw = os.getenv("AGENT_WALLET_CODEX_TIMEOUT", "180")
540
+ try:
541
+ value = float(raw)
542
+ except (TypeError, ValueError):
543
+ return 180.0
544
+ return value if value > 0 else 180.0
545
+
546
+
512
547
  def _call_wallet_cli(command: str, extra_args: list[str]) -> dict[str, Any]:
513
548
  package_root = _resolve_package_root()
514
- completed = subprocess.run(
515
- [
516
- _python_bin(package_root),
517
- "-m",
518
- "agent_wallet.openclaw_cli",
519
- command,
520
- *extra_args,
521
- ],
522
- cwd=str(package_root),
523
- env=_cli_env(package_root),
524
- text=True,
525
- capture_output=True,
526
- timeout=float(os.getenv("AGENT_WALLET_CODEX_TIMEOUT", "180")),
527
- check=False,
528
- )
549
+ timeout_seconds = _cli_timeout_seconds()
550
+ try:
551
+ completed = subprocess.run(
552
+ [
553
+ _python_bin(package_root),
554
+ "-m",
555
+ "agent_wallet.openclaw_cli",
556
+ command,
557
+ *extra_args,
558
+ ],
559
+ cwd=str(package_root),
560
+ env=_cli_env(package_root),
561
+ text=True,
562
+ capture_output=True,
563
+ timeout=timeout_seconds,
564
+ check=False,
565
+ )
566
+ except subprocess.TimeoutExpired as exc:
567
+ # Surface a clean, actionable message instead of the raw TimeoutExpired
568
+ # repr, which would echo the full argv (approval_token, config JSON).
569
+ raise WalletCliError(
570
+ f"agent-wallet CLI '{command}' timed out after {timeout_seconds:g}s. "
571
+ "Retry, or raise AGENT_WALLET_CODEX_TIMEOUT if the network is slow.",
572
+ code="timeout",
573
+ ) from exc
529
574
  if completed.returncode != 0:
530
575
  detail = completed.stderr.strip() or completed.stdout.strip()
531
576
  raise _parse_cli_error(detail)
577
+ # The CLI contract is a single JSON line on stdout. Parse the last non-empty
578
+ # line so a stray print/warning ahead of it from the runtime cannot break an
579
+ # otherwise-successful call.
580
+ stdout = completed.stdout.strip()
581
+ if not stdout:
582
+ return {}
583
+ last_line = stdout.splitlines()[-1].strip()
532
584
  try:
533
- return json.loads(completed.stdout.strip() or "{}")
585
+ return json.loads(last_line)
534
586
  except json.JSONDecodeError as exc:
535
587
  raise WalletCliError(f"agent-wallet CLI returned invalid JSON: {exc}") from exc
536
588
 
@@ -824,26 +876,38 @@ async def _handle_set_wallet_backend(params: dict[str, Any]) -> dict[str, Any]:
824
876
 
825
877
  requested = params.get("backend", params.get("wallet"))
826
878
  backend = _normalize_wallet_backend(requested)
879
+ # Resolve the target network into a local first; only commit to the session
880
+ # globals after the validating wallet call succeeds, so a failed switch never
881
+ # leaves a stale backend paired with a freshly mutated network selection.
827
882
  if backend == "wdk_evm_local":
828
883
  implied = params.get("network") or selected_evm_network or _default_evm_network() or "ethereum"
829
- selected_evm_network = _normalize_selectable_evm_network(implied)
884
+ resolved_network = _normalize_selectable_evm_network(implied)
830
885
  elif backend == "wdk_btc_local":
831
- selected_btc_network = _normalize_btc_network(
886
+ resolved_network = _normalize_btc_network(
832
887
  params.get("network") or selected_btc_network or _default_btc_network()
833
888
  )
834
889
  else:
835
- selected_solana_network = _normalize_solana_network(
890
+ resolved_network = _normalize_solana_network(
836
891
  params.get("network") or selected_solana_network or _default_solana_network()
837
892
  )
838
893
 
839
- config = _effective_config_for_backend(backend)
840
- payload = _invoke_tool(
894
+ config = _host_default_config()
895
+ config["backend"] = backend
896
+ config["network"] = resolved_network
897
+ payload = await asyncio.to_thread(
898
+ _invoke_tool,
841
899
  "get_evm_network" if backend == "wdk_evm_local" else "get_wallet_capabilities",
842
900
  {} if backend != "wdk_evm_local" else {"network": config["network"]},
843
901
  config,
844
902
  )
845
903
  if payload.get("ok") is False:
846
904
  raise RuntimeError(str(payload.get("error") or "set_wallet_backend failed"))
905
+ if backend == "wdk_evm_local":
906
+ selected_evm_network = resolved_network
907
+ elif backend == "wdk_btc_local":
908
+ selected_btc_network = resolved_network
909
+ else:
910
+ selected_solana_network = resolved_network
847
911
  selected_wallet_backend = backend
848
912
  return {
849
913
  "selected_backend": backend,
@@ -866,7 +930,7 @@ async def _handle_set_evm_network(params: dict[str, Any]) -> dict[str, Any]:
866
930
  network = _normalize_selectable_evm_network(params.get("network"))
867
931
  config = _effective_config_for_backend("wdk_evm_local")
868
932
  config["network"] = network
869
- payload = _invoke_tool("get_evm_network", {"network": network}, config)
933
+ payload = await asyncio.to_thread(_invoke_tool, "get_evm_network", {"network": network}, config)
870
934
  if payload.get("ok") is False:
871
935
  raise RuntimeError(str(payload.get("error") or "set_evm_network failed"))
872
936
  selected_wallet_backend = "wdk_evm_local"
@@ -885,6 +949,30 @@ async def _handle_set_evm_network(params: dict[str, Any]) -> dict[str, Any]:
885
949
  }
886
950
 
887
951
 
952
+ def _invoke_wallet_tool_blocking(
953
+ tool_name: str,
954
+ config: dict[str, Any],
955
+ effective_params: dict[str, Any],
956
+ ) -> dict[str, Any]:
957
+ """Synchronous wallet invocation: approval attach + CLI subprocess + cache.
958
+
959
+ Runs off the event loop via ``asyncio.to_thread`` so a slow or hung wallet
960
+ call never freezes the MCP server (tools/list, read-only calls, and
961
+ cancellation stay responsive).
962
+ """
963
+ used_cache = _attach_approval_for_execute(tool_name, config, effective_params)
964
+ try:
965
+ payload = _invoke_tool(tool_name, effective_params, config)
966
+ except Exception as exc:
967
+ raise _normalize_approval_context_error(exc) from exc
968
+ _cache_preview_for_approval(_user_id(), tool_name, payload)
969
+ # A bridge-managed preview that was just executed successfully is single-use:
970
+ # drop it so a duplicate execute cannot silently re-run the operation.
971
+ if used_cache is not None and payload.get("ok") is not False:
972
+ _consume_cached_preview(_user_id(), tool_name)
973
+ return payload
974
+
975
+
888
976
  async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
889
977
  config = _base_config(params, tool_name=tool_name)
890
978
  backend = _normalize_wallet_backend(config.get("backend"))
@@ -893,14 +981,10 @@ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[st
893
981
  config["network"] = selected_evm_network
894
982
 
895
983
  effective_params = dict(params)
896
- _attach_approval_for_execute(tool_name, config, effective_params)
897
-
898
- try:
899
- payload = _invoke_tool(tool_name, effective_params, config)
900
- except Exception as exc:
901
- raise _normalize_approval_context_error(exc) from exc
984
+ payload = await asyncio.to_thread(
985
+ _invoke_wallet_tool_blocking, tool_name, config, effective_params
986
+ )
902
987
 
903
- _cache_preview_for_approval(_user_id(), tool_name, payload)
904
988
  if payload.get("ok") is False:
905
989
  raise RuntimeError(str(payload.get("error") or f"{tool_name} failed"))
906
990
  return payload.get("data", {})
@@ -16,3 +16,4 @@ Rules:
16
16
  - On mainnet, restate the network, asset, amount, and destination before execute.
17
17
  - Do not ask the user for `approval_token`. The bridge manages approval binding internally.
18
18
  - If approval context is missing or stale, repeat preview instead of improvising.
19
+ - Use `set_wallet_backend` to switch between Solana, EVM, and Bitcoin wallets within a session, and `set_evm_network` to pick ethereum or base.
@@ -1,5 +1,5 @@
1
1
  name: agent-wallet
2
- version: 0.1.34
2
+ version: 0.1.35
3
3
  description: Thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend
4
4
  provides_tools:
5
5
  - agent_wallet_tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-btc-wallet",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate BTC-only wallet service built on Tether WDK.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-evm-wallet",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate EVM wallet service built on Tether WDK.",