@agentlayer.tech/wallet 0.1.28 → 0.1.30

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.
Files changed (56) hide show
  1. package/.openclaw/extensions/agent-wallet/README.md +4 -5
  2. package/.openclaw/extensions/agent-wallet/dist/index.js +29 -20
  3. package/.openclaw/extensions/agent-wallet/index.ts +29 -20
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +2 -2
  5. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  6. package/CHANGELOG.md +38 -0
  7. package/README.md +9 -0
  8. package/agent-wallet/README.md +18 -22
  9. package/agent-wallet/agent_wallet/bootstrap.py +28 -12
  10. package/agent-wallet/agent_wallet/btc_user_wallets.py +2 -7
  11. package/agent-wallet/agent_wallet/config.py +99 -22
  12. package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -14
  13. package/agent-wallet/agent_wallet/openclaw_adapter.py +28 -32
  14. package/agent-wallet/agent_wallet/openclaw_runtime.py +3 -12
  15. package/agent-wallet/agent_wallet/providers/kamino.py +21 -4
  16. package/agent-wallet/agent_wallet/providers/solana_rpc.py +0 -23
  17. package/agent-wallet/agent_wallet/providers/x402.py +4 -9
  18. package/agent-wallet/agent_wallet/user_wallets.py +4 -3
  19. package/agent-wallet/agent_wallet/wallet_layer/base.py +3 -3
  20. package/agent-wallet/agent_wallet/wallet_layer/factory.py +8 -5
  21. package/agent-wallet/agent_wallet/wallet_layer/solana.py +437 -44
  22. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +2 -8
  23. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +2 -12
  24. package/agent-wallet/examples/openclaw_runtime_onboarding.py +1 -1
  25. package/agent-wallet/examples/openclaw_user_wallet_example.py +1 -1
  26. package/agent-wallet/openclaw.plugin.json +1 -1
  27. package/agent-wallet/pyproject.toml +2 -1
  28. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +3 -5
  29. package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -12
  30. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  31. package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1 -4
  32. package/agent-wallet/scripts/install_agent_wallet.py +1 -0
  33. package/agent-wallet/scripts/install_openclaw_local_config.py +4 -6
  34. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +2 -4
  35. package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +2 -15
  36. package/agent-wallet/scripts/reveal_btc_seed.sh +7 -16
  37. package/agent-wallet/scripts/setup_btc_wallet.sh +7 -16
  38. package/agent-wallet/scripts/setup_evm_wallet.sh +1 -11
  39. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +4 -1
  40. package/agent-wallet/skills/wallet-operator/SKILL.md +0 -1
  41. package/bin/openclaw-agent-wallet.mjs +289 -0
  42. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +20 -0
  43. package/claude-code/plugins/agent-wallet/.mcp.json +14 -0
  44. package/claude-code/plugins/agent-wallet/README.md +65 -0
  45. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +34 -0
  46. package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  47. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +38 -0
  48. package/codex/plugins/agent-wallet/.mcp.json +15 -0
  49. package/codex/plugins/agent-wallet/README.md +39 -0
  50. package/codex/plugins/agent-wallet/scripts/run_mcp.sh +21 -0
  51. package/codex/plugins/agent-wallet/server.py +1077 -0
  52. package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  53. package/hermes/plugins/agent_wallet/schemas.py +2 -2
  54. package/hermes/plugins/agent_wallet/tools.py +17 -3
  55. package/package.json +6 -1
  56. package/setup.sh +2 -0
@@ -0,0 +1,1077 @@
1
+ """Codex MCP bridge for the existing AgentLayer wallet runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import base64
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ from functools import lru_cache
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ SECRET_CONFIG_KEYS = {"privateKey", "masterKey", "approvalSecret"}
19
+ HOST_DEFAULT_CONFIG_KEYS = {
20
+ "backend",
21
+ "signOnly",
22
+ "network",
23
+ "rpcUrl",
24
+ "rpcUrls",
25
+ "rpcProviderMode",
26
+ "providerGatewayUrl",
27
+ "providerGatewayRpcProvider",
28
+ "wdkBtcServiceUrl",
29
+ "wdkBtcWalletId",
30
+ "wdkBtcAccountIndex",
31
+ "wdkEvmServiceUrl",
32
+ "wdkEvmWalletId",
33
+ "wdkEvmAccountIndex",
34
+ "swapProvider",
35
+ "heliusApiKey",
36
+ "alchemyApiKey",
37
+ "publicKey",
38
+ "keypairPath",
39
+ "autoCreateWallet",
40
+ "encryptUserWallets",
41
+ "migratePlaintextUserWallets",
42
+ "refuseMainnetWalletRecreation",
43
+ "openclawHome",
44
+ "jupiterBaseUrl",
45
+ "jupiterSwapV2BaseUrl",
46
+ "jupiterUltraBaseUrl",
47
+ "jupiterPriceBaseUrl",
48
+ "jupiterPortfolioBaseUrl",
49
+ "jupiterLendBaseUrl",
50
+ "jupiterApiKey",
51
+ "houdiniBaseUrl",
52
+ "houdiniApiKey",
53
+ "houdiniApiSecret",
54
+ "houdiniUserIp",
55
+ "houdiniUserAgent",
56
+ "houdiniUserTimezone",
57
+ "kaminoBaseUrl",
58
+ "kaminoProgramId",
59
+ }
60
+ BACKENDS = ("solana_local", "wdk_btc_local", "wdk_evm_local")
61
+ PREVIEW_CACHE_TTL_SECONDS = 15 * 60
62
+ PRIVATE_SWAP_CACHE_TTL_SECONDS = 35 * 60
63
+ PREVIEW_BOUND_SWAP_TOOLS = {
64
+ "swap_solana_tokens",
65
+ "swap_solana_privately",
66
+ "flash_trade_open_position",
67
+ "flash_trade_close_position",
68
+ }
69
+ PRIVATE_SWAP_APPROVAL_TOOL_NAME = "swap_solana_privately"
70
+ APPROVAL_PREVIEW_TOOL_ALIASES = {
71
+ "x402_pay_request": "x402_preview_request",
72
+ }
73
+ APPROVAL_CONTEXT_MISSING_MESSAGE = (
74
+ "Confirmation context is not ready or has expired. Preview or intent_preview the wallet "
75
+ "operation again, wait for explicit user confirmation, then retry execute. Do not ask the "
76
+ "user for a manual approval token."
77
+ )
78
+
79
+ selected_wallet_backend: str | None = None
80
+ selected_solana_network: str | None = None
81
+ selected_evm_network: str | None = None
82
+ selected_btc_network: str | None = None
83
+ approval_preview_cache: dict[str, dict[str, Any]] = {}
84
+ private_swap_order_cache: dict[str, dict[str, Any]] = {}
85
+
86
+
87
+ class WalletCliError(RuntimeError):
88
+ def __init__(self, message: str, *, code: str = "", details: dict[str, Any] | None = None):
89
+ super().__init__(message)
90
+ self.code = code
91
+ self.details = details or {}
92
+
93
+
94
+ def _plugin_root() -> Path:
95
+ return Path(__file__).resolve().parent
96
+
97
+
98
+ def _repo_relative_package_root() -> Path:
99
+ return Path(__file__).resolve().parents[3] / "agent-wallet"
100
+
101
+
102
+ def _openclaw_home() -> Path:
103
+ return Path(os.getenv("OPENCLAW_HOME", "~/.openclaw")).expanduser().resolve()
104
+
105
+
106
+ @lru_cache(maxsize=1)
107
+ def _openclaw_plugin_config() -> dict[str, Any]:
108
+ config_path = _openclaw_home() / "openclaw.json"
109
+ try:
110
+ payload = json.loads(config_path.read_text(encoding="utf-8"))
111
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
112
+ return {}
113
+ if not isinstance(payload, dict):
114
+ return {}
115
+ plugins = payload.get("plugins")
116
+ if not isinstance(plugins, dict):
117
+ return {}
118
+ entries = plugins.get("entries")
119
+ if not isinstance(entries, dict):
120
+ return {}
121
+ agent_wallet = entries.get("agent-wallet")
122
+ if not isinstance(agent_wallet, dict):
123
+ return {}
124
+ config = agent_wallet.get("config")
125
+ return config if isinstance(config, dict) else {}
126
+
127
+
128
+ def _host_default_config() -> dict[str, Any]:
129
+ plugin_config = _openclaw_plugin_config()
130
+ defaults: dict[str, Any] = {}
131
+ for key in HOST_DEFAULT_CONFIG_KEYS:
132
+ value = plugin_config.get(key)
133
+ if value is not None:
134
+ defaults[key] = copy.deepcopy(value)
135
+ return defaults
136
+
137
+
138
+ def _configured_backend() -> str | None:
139
+ value = _openclaw_plugin_config().get("backend")
140
+ if value is None:
141
+ return None
142
+ try:
143
+ return _normalize_wallet_backend(value)
144
+ except RuntimeError:
145
+ return None
146
+
147
+
148
+ def _configured_network_for_backend(backend: str) -> str | None:
149
+ value = _openclaw_plugin_config().get("network")
150
+ if value in (None, ""):
151
+ return None
152
+ try:
153
+ if backend == "wdk_evm_local":
154
+ return _normalize_selectable_evm_network(value)
155
+ if backend == "wdk_btc_local":
156
+ return _normalize_btc_network(value)
157
+ return _normalize_solana_network(value)
158
+ except RuntimeError:
159
+ return None
160
+
161
+
162
+ def _resolve_package_root() -> Path:
163
+ candidates = [
164
+ os.getenv("AGENT_WALLET_PACKAGE_ROOT"),
165
+ os.getenv("OPENCLAW_AGENT_WALLET_PACKAGE_ROOT"),
166
+ _openclaw_plugin_config().get("packageRoot"),
167
+ str(_openclaw_home() / "agent-wallet-runtime" / "current" / "agent-wallet"),
168
+ str(_repo_relative_package_root()),
169
+ str(Path.cwd() / "agent-wallet"),
170
+ ]
171
+ for candidate in candidates:
172
+ if not candidate:
173
+ continue
174
+ root = Path(candidate).expanduser().resolve()
175
+ if (root / "agent_wallet" / "__init__.py").exists():
176
+ return root
177
+ raise RuntimeError(
178
+ "Could not resolve the agent-wallet package root. Set AGENT_WALLET_PACKAGE_ROOT or "
179
+ "OPENCLAW_AGENT_WALLET_PACKAGE_ROOT."
180
+ )
181
+
182
+
183
+ def _python_bin(package_root: Path) -> str:
184
+ for candidate in (
185
+ os.getenv("AGENT_WALLET_PYTHON"),
186
+ os.getenv("OPENCLAW_AGENT_WALLET_PYTHON"),
187
+ _openclaw_plugin_config().get("pythonBin"),
188
+ str(package_root / ".venv" / "bin" / "python"),
189
+ str(package_root / ".runtime-venv" / "bin" / "python"),
190
+ "python3",
191
+ ):
192
+ if not candidate:
193
+ continue
194
+ resolved = Path(candidate).expanduser()
195
+ if resolved.is_absolute() and not resolved.exists():
196
+ continue
197
+ return str(resolved)
198
+ return "python3"
199
+
200
+
201
+ def _cli_env(package_root: Path) -> dict[str, str]:
202
+ env = dict(os.environ)
203
+ current = str(env.get("PYTHONPATH", "")).strip()
204
+ env["PYTHONPATH"] = f"{package_root}{os.pathsep}{current}" if current else str(package_root)
205
+ return env
206
+
207
+
208
+ def _canonical_json_text(payload: dict[str, Any]) -> str:
209
+ return json.dumps(payload, sort_keys=True, separators=(",", ":"))
210
+
211
+
212
+ def _preview_digest(preview: dict[str, Any]) -> str:
213
+ return hashlib.sha256(_canonical_json_text(preview).encode("utf-8")).hexdigest()
214
+
215
+
216
+ def _approval_cache_key(user_id: str, tool_name: str) -> str:
217
+ return f"{user_id}::{tool_name}"
218
+
219
+
220
+ def _approval_preview_tool_name(tool_name: str) -> str:
221
+ return APPROVAL_PREVIEW_TOOL_ALIASES.get(tool_name.strip(), tool_name.strip())
222
+
223
+
224
+ def _prune_approval_preview_cache() -> None:
225
+ now = time.time()
226
+ for key in list(approval_preview_cache):
227
+ if float(approval_preview_cache[key].get("expires_at") or 0) <= now:
228
+ approval_preview_cache.pop(key, None)
229
+
230
+
231
+ def _cache_preview_for_approval(user_id: str, tool_name: str, payload: dict[str, Any]) -> None:
232
+ cache_tool_name = _approval_preview_tool_name(tool_name)
233
+ if not isinstance(payload, dict):
234
+ return
235
+ if payload.get("ok") is False:
236
+ return
237
+ data = payload.get("data")
238
+ if not isinstance(data, dict):
239
+ return
240
+ if str(data.get("mode") or "") not in {"preview", "prepare", "intent_preview"}:
241
+ return
242
+ summary = data.get("confirmation_summary")
243
+ if not isinstance(summary, dict):
244
+ return
245
+ _prune_approval_preview_cache()
246
+ approval_preview_cache[_approval_cache_key(user_id, cache_tool_name)] = {
247
+ "digest": _preview_digest(data),
248
+ "expires_at": time.time()
249
+ + (
250
+ PRIVATE_SWAP_CACHE_TTL_SECONDS
251
+ if cache_tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME
252
+ else PREVIEW_CACHE_TTL_SECONDS
253
+ ),
254
+ "preview": data,
255
+ "summary": summary,
256
+ }
257
+ if cache_tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME:
258
+ private_swap_order_cache.pop(_approval_cache_key(user_id, cache_tool_name), None)
259
+
260
+
261
+ def _latest_cached_preview(user_id: str, tool_name: str) -> dict[str, Any] | None:
262
+ _prune_approval_preview_cache()
263
+ return approval_preview_cache.get(_approval_cache_key(user_id, _approval_preview_tool_name(tool_name)))
264
+
265
+
266
+ def _approval_token_preview_digest(token: str) -> str:
267
+ if not isinstance(token, str) or "." not in token:
268
+ return ""
269
+ encoded = token.split(".", 1)[0]
270
+ try:
271
+ padding = "=" * (-len(encoded) % 4)
272
+ payload = json.loads(base64.urlsafe_b64decode(encoded + padding).decode("utf-8"))
273
+ except Exception:
274
+ return ""
275
+ summary = payload.get("binding", {}).get("summary") if isinstance(payload, dict) else None
276
+ if not isinstance(summary, dict):
277
+ return ""
278
+ digest = summary.get("_preview_digest")
279
+ return digest.strip() if isinstance(digest, str) else ""
280
+
281
+
282
+ def _cached_preview_for_token(user_id: str, tool_name: str, token: str) -> dict[str, Any] | None:
283
+ digest = _approval_token_preview_digest(token)
284
+ if not digest:
285
+ return None
286
+ cached = _latest_cached_preview(user_id, tool_name)
287
+ if not cached or cached.get("digest") != digest:
288
+ return None
289
+ preview = cached.get("preview")
290
+ return preview if isinstance(preview, dict) else None
291
+
292
+
293
+ def _cache_pending_private_swap_order(
294
+ user_id: str,
295
+ tool_name: str,
296
+ preview: dict[str, Any],
297
+ details: dict[str, Any],
298
+ ) -> None:
299
+ if tool_name != PRIVATE_SWAP_APPROVAL_TOOL_NAME:
300
+ return
301
+ houdini_id = str(details.get("houdini_id") or "").strip()
302
+ deposit_address = str(details.get("deposit_address") or "").strip()
303
+ if not houdini_id or not deposit_address:
304
+ return
305
+ private_swap_order_cache[_approval_cache_key(user_id, tool_name)] = {
306
+ "digest": _preview_digest(preview),
307
+ "expires_at": time.time() + PRIVATE_SWAP_CACHE_TTL_SECONDS,
308
+ "order": {
309
+ "multi_id": str(details.get("multi_id") or "").strip() or None,
310
+ "houdini_id": houdini_id,
311
+ "deposit_address": deposit_address,
312
+ "order": details.get("order") if isinstance(details.get("order"), dict) else {},
313
+ },
314
+ }
315
+
316
+
317
+ def _latest_pending_private_swap_order(
318
+ user_id: str,
319
+ tool_name: str,
320
+ preview: dict[str, Any],
321
+ ) -> dict[str, Any] | None:
322
+ if tool_name != PRIVATE_SWAP_APPROVAL_TOOL_NAME:
323
+ return None
324
+ cached = private_swap_order_cache.get(_approval_cache_key(user_id, tool_name))
325
+ if not cached:
326
+ return None
327
+ if float(cached.get("expires_at") or 0) <= time.time():
328
+ private_swap_order_cache.pop(_approval_cache_key(user_id, tool_name), None)
329
+ return None
330
+ if cached.get("digest") != _preview_digest(preview):
331
+ return None
332
+ order = cached.get("order")
333
+ return order if isinstance(order, dict) else None
334
+
335
+
336
+ def _clear_pending_private_swap_order(user_id: str, tool_name: str) -> None:
337
+ if tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME:
338
+ private_swap_order_cache.pop(_approval_cache_key(user_id, tool_name), None)
339
+
340
+
341
+ def _list_pending_private_swap_orders(user_id: str) -> list[dict[str, Any]]:
342
+ key = _approval_cache_key(user_id, PRIVATE_SWAP_APPROVAL_TOOL_NAME)
343
+ pending = private_swap_order_cache.get(key)
344
+ if not pending or float(pending.get("expires_at") or 0) <= time.time():
345
+ private_swap_order_cache.pop(key, None)
346
+ return []
347
+ order = pending.get("order")
348
+ if not isinstance(order, dict):
349
+ return []
350
+ return [{**order, "expires_at_ms": int(float(pending["expires_at"]) * 1000)}]
351
+
352
+
353
+ def _normalize_wallet_backend(value: Any) -> str:
354
+ normalized = str(value or "").strip().lower()
355
+ aliases = {
356
+ "sol": "solana_local",
357
+ "solana": "solana_local",
358
+ "solana_local": "solana_local",
359
+ "solana-local": "solana_local",
360
+ "evm": "wdk_evm_local",
361
+ "ethereum": "wdk_evm_local",
362
+ "eth": "wdk_evm_local",
363
+ "base": "wdk_evm_local",
364
+ "wdk_evm_local": "wdk_evm_local",
365
+ "wdk-evm-local": "wdk_evm_local",
366
+ "evm_local": "wdk_evm_local",
367
+ "evm-local": "wdk_evm_local",
368
+ "btc": "wdk_btc_local",
369
+ "bitcoin": "wdk_btc_local",
370
+ "wdk_btc_local": "wdk_btc_local",
371
+ "wdk-btc-local": "wdk_btc_local",
372
+ "btc_local": "wdk_btc_local",
373
+ "btc-local": "wdk_btc_local",
374
+ }
375
+ backend = aliases.get(normalized, normalized)
376
+ if backend not in BACKENDS:
377
+ raise RuntimeError("Wallet backend must be solana, evm, base, ethereum, btc, or bitcoin.")
378
+ return backend
379
+
380
+
381
+ def _backend_label(backend: str) -> str:
382
+ if backend == "wdk_evm_local":
383
+ return "evm"
384
+ if backend == "wdk_btc_local":
385
+ return "bitcoin"
386
+ return "solana"
387
+
388
+
389
+ def _normalize_evm_network(value: Any) -> str:
390
+ normalized = str(value or "").strip().lower()
391
+ aliases = {
392
+ "mainnet": "ethereum",
393
+ "eth": "ethereum",
394
+ "eth-mainnet": "ethereum",
395
+ "base-mainnet": "base",
396
+ }
397
+ return aliases.get(normalized, normalized)
398
+
399
+
400
+ def _normalize_selectable_evm_network(value: Any) -> str:
401
+ network = _normalize_evm_network(value)
402
+ if network in {"sepolia", "base-sepolia", "base_sepolia"}:
403
+ raise RuntimeError("EVM testnets are no longer supported. Use ethereum or base.")
404
+ if network not in {"ethereum", "base"}:
405
+ raise RuntimeError("EVM network must be 'ethereum' or 'base'.")
406
+ return network
407
+
408
+
409
+ def _normalize_solana_network(value: Any) -> str | None:
410
+ network = str(value or "").strip().lower()
411
+ if not network:
412
+ return None
413
+ aliases = {
414
+ "solana": "mainnet",
415
+ "solana-mainnet": "mainnet",
416
+ "mainnet_beta": "mainnet",
417
+ "mainnet-beta": "mainnet",
418
+ }
419
+ normalized = aliases.get(network, network)
420
+ if normalized in {"devnet", "testnet"}:
421
+ raise RuntimeError("Solana devnet/testnet are no longer supported. Use mainnet.")
422
+ if normalized != "mainnet":
423
+ raise RuntimeError("Solana network must be mainnet.")
424
+ return normalized
425
+
426
+
427
+ def _normalize_btc_network(value: Any) -> str | None:
428
+ network = str(value or "").strip().lower()
429
+ if not network:
430
+ return None
431
+ aliases = {
432
+ "btc": "bitcoin",
433
+ "bitcoin_mainnet": "bitcoin",
434
+ "bitcoin-mainnet": "bitcoin",
435
+ "mainnet": "bitcoin",
436
+ }
437
+ normalized = aliases.get(network, network)
438
+ if normalized in {"testnet", "regtest"}:
439
+ raise RuntimeError("Bitcoin testnet/regtest are no longer supported. Use bitcoin.")
440
+ if normalized != "bitcoin":
441
+ raise RuntimeError("Bitcoin network must be bitcoin.")
442
+ return normalized
443
+
444
+
445
+ def _default_backend() -> str:
446
+ return _normalize_wallet_backend(
447
+ os.getenv("AGENT_WALLET_BACKEND")
448
+ or os.getenv("OPENCLAW_AGENT_WALLET_BACKEND")
449
+ or _configured_backend()
450
+ or "solana_local"
451
+ )
452
+
453
+
454
+ def _default_evm_network() -> str | None:
455
+ configured = _normalize_evm_network(os.getenv("WDK_EVM_NETWORK"))
456
+ if configured in {"ethereum", "base"}:
457
+ return configured
458
+ return _configured_network_for_backend("wdk_evm_local")
459
+
460
+
461
+ def _default_solana_network() -> str:
462
+ try:
463
+ return (
464
+ _normalize_solana_network(os.getenv("SOLANA_NETWORK"))
465
+ or _configured_network_for_backend("solana_local")
466
+ or "mainnet"
467
+ )
468
+ except RuntimeError:
469
+ return "mainnet"
470
+
471
+
472
+ def _default_btc_network() -> str:
473
+ try:
474
+ return _normalize_btc_network(os.getenv("WDK_BTC_NETWORK")) or _configured_network_for_backend(
475
+ "wdk_btc_local"
476
+ ) or "bitcoin"
477
+ except RuntimeError:
478
+ return "bitcoin"
479
+
480
+
481
+ def _infer_backend_for_tool(tool_name: str) -> str | None:
482
+ if (
483
+ tool_name.startswith("get_evm_")
484
+ or tool_name.startswith("manage_evm_")
485
+ or tool_name.startswith("swap_evm_")
486
+ or tool_name.startswith("transfer_evm_")
487
+ or tool_name == "set_evm_network"
488
+ ):
489
+ return "wdk_evm_local"
490
+ if tool_name.startswith("get_btc_") or tool_name == "transfer_btc":
491
+ return "wdk_btc_local"
492
+ if (
493
+ "solana" in tool_name
494
+ or "jupiter" in tool_name
495
+ or "kamino" in tool_name
496
+ or "bags" in tool_name
497
+ or tool_name
498
+ in {
499
+ "transfer_sol",
500
+ "transfer_spl_token",
501
+ "sign_wallet_message",
502
+ "close_empty_token_accounts",
503
+ "get_wallet_portfolio",
504
+ "get_solana_token_prices",
505
+ }
506
+ ):
507
+ return "solana_local"
508
+ return None
509
+
510
+
511
+ def _active_backend_for_tool(tool_name: str) -> str:
512
+ return selected_wallet_backend or _infer_backend_for_tool(tool_name) or _default_backend()
513
+
514
+
515
+ def _network_for_backend(backend: str) -> str:
516
+ if backend == "wdk_evm_local":
517
+ return selected_evm_network or _default_evm_network() or "ethereum"
518
+ if backend == "wdk_btc_local":
519
+ return selected_btc_network or _default_btc_network()
520
+ return selected_solana_network or _default_solana_network()
521
+
522
+
523
+ def _effective_config_for_backend(backend: str) -> dict[str, Any]:
524
+ config = _host_default_config()
525
+ config["backend"] = backend
526
+ config["network"] = _network_for_backend(backend)
527
+ return config
528
+
529
+
530
+ def _reject_secret_config_json(config: dict[str, Any]) -> None:
531
+ present = sorted(key for key in SECRET_CONFIG_KEYS if str(config.get(key) or "").strip())
532
+ if present:
533
+ raise RuntimeError(
534
+ "Sensitive keys are not allowed in Codex bridge config overrides: "
535
+ + ", ".join(present)
536
+ )
537
+
538
+
539
+ def _base_config(args: dict[str, Any], *, tool_name: str = "") -> dict[str, Any]:
540
+ backend = (
541
+ _normalize_wallet_backend(args.get("backend"))
542
+ if args.get("backend") is not None
543
+ else _active_backend_for_tool(tool_name)
544
+ )
545
+ config = _effective_config_for_backend(backend)
546
+ if "config" in args:
547
+ extra = args.get("config")
548
+ if not isinstance(extra, dict):
549
+ raise RuntimeError("config must be an object when provided.")
550
+ _reject_secret_config_json(extra)
551
+ config.update(extra)
552
+ network_override = args.get("network")
553
+ if network_override is not None:
554
+ if backend == "wdk_evm_local":
555
+ config["network"] = _normalize_selectable_evm_network(network_override)
556
+ elif backend == "wdk_btc_local":
557
+ config["network"] = _normalize_btc_network(network_override)
558
+ else:
559
+ config["network"] = _normalize_solana_network(network_override)
560
+ return config
561
+
562
+
563
+ def _user_id() -> str:
564
+ return (
565
+ os.getenv("AGENT_WALLET_USER_ID")
566
+ or os.getenv("OPENCLAW_AGENT_WALLET_USER_ID")
567
+ or str(_openclaw_plugin_config().get("userId") or "").strip()
568
+ or os.getenv("USER")
569
+ or "codex-local-user"
570
+ )
571
+
572
+
573
+ def _parse_cli_error(text: str) -> WalletCliError:
574
+ stripped = str(text or "").strip()
575
+ if not stripped:
576
+ return WalletCliError("agent-wallet CLI failed.")
577
+ try:
578
+ payload = json.loads(stripped)
579
+ except json.JSONDecodeError:
580
+ return WalletCliError(stripped)
581
+ if not isinstance(payload, dict):
582
+ return WalletCliError(stripped)
583
+ return WalletCliError(
584
+ str(payload.get("error") or "agent-wallet CLI failed."),
585
+ code=str(payload.get("code") or ""),
586
+ details=payload.get("details") if isinstance(payload.get("details"), dict) else {},
587
+ )
588
+
589
+
590
+ def _call_wallet_cli(command: str, extra_args: list[str]) -> dict[str, Any]:
591
+ package_root = _resolve_package_root()
592
+ completed = subprocess.run(
593
+ [
594
+ _python_bin(package_root),
595
+ "-m",
596
+ "agent_wallet.openclaw_cli",
597
+ command,
598
+ *extra_args,
599
+ ],
600
+ cwd=str(package_root),
601
+ env=_cli_env(package_root),
602
+ text=True,
603
+ capture_output=True,
604
+ timeout=float(os.getenv("AGENT_WALLET_CODEX_TIMEOUT", "180")),
605
+ check=False,
606
+ )
607
+ if completed.returncode != 0:
608
+ detail = completed.stderr.strip() or completed.stdout.strip()
609
+ raise _parse_cli_error(detail)
610
+ try:
611
+ return json.loads(completed.stdout.strip() or "{}")
612
+ except json.JSONDecodeError as exc:
613
+ raise WalletCliError(f"agent-wallet CLI returned invalid JSON: {exc}") from exc
614
+
615
+
616
+ def _invoke_tool(tool_name: str, arguments: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]:
617
+ return _call_wallet_cli(
618
+ "invoke",
619
+ [
620
+ "--user-id",
621
+ _user_id(),
622
+ "--tool",
623
+ tool_name,
624
+ "--arguments-json",
625
+ json.dumps(arguments),
626
+ "--config-json",
627
+ json.dumps(config),
628
+ ],
629
+ )
630
+
631
+
632
+ def _issue_approval_token(
633
+ tool_name: str,
634
+ config: dict[str, Any],
635
+ preview_payload: dict[str, Any],
636
+ ) -> str:
637
+ summary = preview_payload.get("confirmation_summary")
638
+ if not isinstance(summary, dict):
639
+ raise RuntimeError(f"No confirmation_summary available for {tool_name}.")
640
+ summary_for_token = dict(summary)
641
+ summary_for_token["_preview_digest"] = _preview_digest(preview_payload)
642
+ extra_args = [
643
+ "--user-id",
644
+ _user_id(),
645
+ "--tool",
646
+ tool_name,
647
+ "--summary-json",
648
+ json.dumps(summary_for_token),
649
+ "--config-json",
650
+ json.dumps(config),
651
+ ]
652
+ if preview_payload.get("is_mainnet") is True:
653
+ extra_args.append("--mainnet-confirmed")
654
+ if tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME:
655
+ extra_args.extend(["--ttl-seconds", "1800"])
656
+ payload = _call_wallet_cli("issue-approval", extra_args)
657
+ token = str(payload.get("approval_token") or "").strip()
658
+ if not token:
659
+ raise RuntimeError(f"issue-approval did not return an approval_token for {tool_name}.")
660
+ return token
661
+
662
+
663
+ def _is_solana_swap_intent_execute(params: dict[str, Any]) -> bool:
664
+ return str(params.get("mode") or "") == "intent_execute"
665
+
666
+
667
+ def _requires_approved_preview_payload(tool_name: str, params: dict[str, Any]) -> bool:
668
+ if tool_name == "swap_solana_tokens" and _is_solana_swap_intent_execute(params):
669
+ return False
670
+ return tool_name in PREVIEW_BOUND_SWAP_TOOLS
671
+
672
+
673
+ def _looks_like_approval_context_error(message: str) -> bool:
674
+ text = str(message or "").lower()
675
+ return any(
676
+ phrase in text
677
+ for phrase in (
678
+ "approval_token",
679
+ "approval token",
680
+ "approval context",
681
+ "approved preview",
682
+ "preview payload",
683
+ "previewed operation",
684
+ )
685
+ )
686
+
687
+
688
+ def _normalize_approval_context_error(error: Exception) -> Exception:
689
+ if not _looks_like_approval_context_error(str(error)):
690
+ return error
691
+ if isinstance(error, WalletCliError):
692
+ return WalletCliError(
693
+ f"{APPROVAL_CONTEXT_MISSING_MESSAGE} Original wallet error: {error}",
694
+ code=error.code,
695
+ details=error.details,
696
+ )
697
+ return RuntimeError(f"{APPROVAL_CONTEXT_MISSING_MESSAGE} Original wallet error: {error}")
698
+
699
+
700
+ def _attach_approval_for_execute(
701
+ tool_name: str,
702
+ config: dict[str, Any],
703
+ effective_params: dict[str, Any],
704
+ ) -> dict[str, Any] | None:
705
+ mode = str(effective_params.get("mode") or "")
706
+ if mode not in {"execute", "intent_execute"}:
707
+ return None
708
+ if tool_name == "swap_solana_tokens" and mode == "execute":
709
+ raise RuntimeError(
710
+ "Legacy exact-preview execute is disabled for Solana Jupiter swaps in Codex. "
711
+ "Use intent_preview, wait for explicit user confirmation, then call intent_execute."
712
+ )
713
+ cached = _latest_cached_preview(_user_id(), tool_name)
714
+ if cached and isinstance(cached.get("preview"), dict):
715
+ preview = cached["preview"]
716
+ effective_params["approval_token"] = _issue_approval_token(tool_name, config, preview)
717
+ if _requires_approved_preview_payload(tool_name, effective_params):
718
+ effective_params["_approved_preview"] = preview
719
+ return cached
720
+ approval_token = str(effective_params.get("approval_token") or "").strip()
721
+ if approval_token and _requires_approved_preview_payload(tool_name, effective_params):
722
+ cached_preview = _cached_preview_for_token(_user_id(), tool_name, approval_token)
723
+ if cached_preview is not None and "_approved_preview" not in effective_params:
724
+ effective_params["_approved_preview"] = cached_preview
725
+ if effective_params.get("approval_token"):
726
+ return None
727
+ raise RuntimeError(APPROVAL_CONTEXT_MISSING_MESSAGE)
728
+
729
+
730
+ class _SchemaOnlyBackend:
731
+ def __init__(self, *, name: str, chain: str, network: str):
732
+ self.name = name
733
+ self.chain = chain
734
+ self.network = network
735
+ self.sign_only = True
736
+
737
+ def get_capabilities(self):
738
+ from agent_wallet.wallet_layer.base import WalletCapabilities
739
+
740
+ return WalletCapabilities(
741
+ backend=self.name,
742
+ chain=self.chain,
743
+ custody_model="local",
744
+ sign_only=True,
745
+ has_signer=False,
746
+ can_get_address=True,
747
+ can_get_balance=True,
748
+ external_dependencies=[],
749
+ )
750
+
751
+ async def get_address(self):
752
+ return None
753
+
754
+ async def get_balance(self, address=None):
755
+ return {}
756
+
757
+
758
+ def _schema_backend(name: str) -> _SchemaOnlyBackend:
759
+ if name == "wdk_btc_local":
760
+ return _SchemaOnlyBackend(name=name, chain="bitcoin", network="bitcoin")
761
+ if name == "wdk_evm_local":
762
+ return _SchemaOnlyBackend(name=name, chain="evm", network="ethereum")
763
+ return _SchemaOnlyBackend(name="solana_local", chain="solana", network="mainnet")
764
+
765
+
766
+ def _tool_specs(backend_name: str) -> list[dict[str, Any]]:
767
+ package_root = _resolve_package_root()
768
+ package_root_text = str(package_root)
769
+ inserted = package_root_text not in sys.path
770
+ if inserted:
771
+ sys.path.insert(0, package_root_text)
772
+ try:
773
+ from agent_wallet.openclaw_adapter import OpenClawWalletAdapter
774
+
775
+ adapter = OpenClawWalletAdapter(_schema_backend(backend_name))
776
+ return [tool.model_dump() for tool in adapter.list_tools()]
777
+ finally:
778
+ if inserted:
779
+ try:
780
+ sys.path.remove(package_root_text)
781
+ except ValueError:
782
+ pass
783
+
784
+
785
+ def _sanitize_schema(schema: dict[str, Any]) -> dict[str, Any]:
786
+ sanitized = copy.deepcopy(schema)
787
+ if sanitized.get("type") != "object":
788
+ sanitized["type"] = "object"
789
+ for key in ("oneOf", "anyOf", "allOf", "enum", "not"):
790
+ sanitized.pop(key, None)
791
+ properties = sanitized.get("properties")
792
+ if isinstance(properties, dict):
793
+ properties.pop("approval_token", None)
794
+ else:
795
+ sanitized["properties"] = {}
796
+ required = sanitized.get("required")
797
+ if isinstance(required, list):
798
+ filtered = [field for field in required if field != "approval_token"]
799
+ if filtered:
800
+ sanitized["required"] = filtered
801
+ else:
802
+ sanitized.pop("required", None)
803
+ sanitized.setdefault("additionalProperties", False)
804
+ return sanitized
805
+
806
+
807
+ def _manual_tool_definitions() -> list[dict[str, Any]]:
808
+ return [
809
+ {
810
+ "name": "get_active_wallet_backend",
811
+ "description": (
812
+ "Show which wallet backend is active for this Codex MCP session and whether it "
813
+ "differs from the startup default."
814
+ ),
815
+ "input_schema": {
816
+ "type": "object",
817
+ "properties": {},
818
+ "additionalProperties": False,
819
+ },
820
+ "read_only": True,
821
+ },
822
+ {
823
+ "name": "set_wallet_backend",
824
+ "description": (
825
+ "Switch the active wallet backend for this Codex MCP session between Solana, EVM, "
826
+ "and Bitcoin without editing runtime config files."
827
+ ),
828
+ "input_schema": {
829
+ "type": "object",
830
+ "properties": {
831
+ "backend": {
832
+ "type": "string",
833
+ "description": "solana, evm, base, ethereum, btc, or bitcoin.",
834
+ },
835
+ "wallet": {
836
+ "type": "string",
837
+ "description": "Alias for backend.",
838
+ },
839
+ "network": {
840
+ "type": "string",
841
+ "description": "Optional network override for the selected backend.",
842
+ },
843
+ },
844
+ "additionalProperties": False,
845
+ },
846
+ "read_only": False,
847
+ },
848
+ {
849
+ "name": "set_evm_network",
850
+ "description": (
851
+ "Set the active EVM network for this Codex MCP session to ethereum or base."
852
+ ),
853
+ "input_schema": {
854
+ "type": "object",
855
+ "properties": {
856
+ "network": {
857
+ "type": "string",
858
+ "description": "ethereum or base.",
859
+ }
860
+ },
861
+ "required": ["network"],
862
+ "additionalProperties": False,
863
+ },
864
+ "read_only": False,
865
+ },
866
+ ]
867
+
868
+
869
+ def _build_tool_definitions() -> list[dict[str, Any]]:
870
+ merged: dict[str, dict[str, Any]] = {}
871
+ for backend_name in ("solana_local", "wdk_evm_local", "wdk_btc_local"):
872
+ for spec in _tool_specs(backend_name):
873
+ merged.setdefault(spec["name"], spec)
874
+ for spec in merged.values():
875
+ spec["input_schema"] = _sanitize_schema(spec["input_schema"])
876
+ if spec.get("read_only") is False:
877
+ spec["description"] = (
878
+ f"{spec['description']} Preview first when supported. Execute reuses cached "
879
+ "approval context inside the Codex bridge."
880
+ )
881
+ for spec in _manual_tool_definitions():
882
+ merged[spec["name"]] = spec
883
+ return [merged[name] for name in sorted(merged)]
884
+
885
+
886
+ async def _handle_get_active_wallet_backend() -> dict[str, Any]:
887
+ backend = selected_wallet_backend or _default_backend()
888
+ return {
889
+ "active_backend": backend,
890
+ "active_wallet": _backend_label(backend),
891
+ "active_network": _network_for_backend(backend),
892
+ "configured_backend": _default_backend(),
893
+ "session_override_active": bool(selected_wallet_backend),
894
+ "available_wallets": ["solana", "evm", "bitcoin"],
895
+ "usage": (
896
+ "Use set_wallet_backend to switch between Solana, EVM, and Bitcoin for this Codex "
897
+ "session. The runtime startup config remains unchanged."
898
+ ),
899
+ }
900
+
901
+
902
+ async def _handle_set_wallet_backend(params: dict[str, Any]) -> dict[str, Any]:
903
+ global selected_wallet_backend, selected_solana_network, selected_evm_network, selected_btc_network
904
+
905
+ requested = params.get("backend", params.get("wallet"))
906
+ backend = _normalize_wallet_backend(requested)
907
+ if backend == "wdk_evm_local":
908
+ implied = params.get("network") or selected_evm_network or _default_evm_network() or "ethereum"
909
+ selected_evm_network = _normalize_selectable_evm_network(implied)
910
+ elif backend == "wdk_btc_local":
911
+ selected_btc_network = _normalize_btc_network(
912
+ params.get("network") or selected_btc_network or _default_btc_network()
913
+ )
914
+ else:
915
+ selected_solana_network = _normalize_solana_network(
916
+ params.get("network") or selected_solana_network or _default_solana_network()
917
+ )
918
+
919
+ config = _effective_config_for_backend(backend)
920
+ payload = _invoke_tool(
921
+ "get_evm_network" if backend == "wdk_evm_local" else "get_wallet_capabilities",
922
+ {} if backend != "wdk_evm_local" else {"network": config["network"]},
923
+ config,
924
+ )
925
+ if payload.get("ok") is False:
926
+ raise RuntimeError(str(payload.get("error") or "set_wallet_backend failed"))
927
+ selected_wallet_backend = backend
928
+ return {
929
+ "selected_backend": backend,
930
+ "selected_wallet": _backend_label(backend),
931
+ "selected_network": _network_for_backend(backend),
932
+ "configured_backend": _default_backend(),
933
+ "session_override_active": True,
934
+ "config_file_changed": False,
935
+ "usage": (
936
+ "Subsequent wallet calls in this Codex MCP session use this wallet backend by "
937
+ "default. The runtime startup config remains unchanged."
938
+ ),
939
+ "data": payload.get("data", {}),
940
+ }
941
+
942
+
943
+ async def _handle_set_evm_network(params: dict[str, Any]) -> dict[str, Any]:
944
+ global selected_wallet_backend, selected_evm_network
945
+
946
+ network = _normalize_selectable_evm_network(params.get("network"))
947
+ config = _effective_config_for_backend("wdk_evm_local")
948
+ config["network"] = network
949
+ payload = _invoke_tool("get_evm_network", {"network": network}, config)
950
+ if payload.get("ok") is False:
951
+ raise RuntimeError(str(payload.get("error") or "set_evm_network failed"))
952
+ selected_wallet_backend = "wdk_evm_local"
953
+ selected_evm_network = network
954
+ return {
955
+ "selected_backend": "wdk_evm_local",
956
+ "selected_wallet": "evm",
957
+ "selected_network": network,
958
+ "session_active_network": network,
959
+ "session_override_active": True,
960
+ "usage": (
961
+ "Subsequent EVM wallet calls in this Codex MCP session use this network by default. "
962
+ "You can still override a single EVM call with its network parameter."
963
+ ),
964
+ "data": payload.get("data", {}),
965
+ }
966
+
967
+
968
+ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
969
+ if tool_name == "list_pending_solana_private_swaps":
970
+ return {"orders": _list_pending_private_swap_orders(_user_id())}
971
+
972
+ config = _base_config(params, tool_name=tool_name)
973
+ backend = _normalize_wallet_backend(config.get("backend"))
974
+ if backend == "wdk_evm_local" and params.get("network") is None and selected_evm_network:
975
+ params = {**params, "network": selected_evm_network}
976
+ config["network"] = selected_evm_network
977
+
978
+ effective_params = dict(params)
979
+ if tool_name != "continue_solana_private_swap":
980
+ _attach_approval_for_execute(tool_name, config, effective_params)
981
+ else:
982
+ cached = _latest_cached_preview(_user_id(), PRIVATE_SWAP_APPROVAL_TOOL_NAME)
983
+ if cached and isinstance(cached.get("preview"), dict):
984
+ effective_params["_approved_preview"] = cached["preview"]
985
+ effective_params["approval_token"] = _issue_approval_token(
986
+ PRIVATE_SWAP_APPROVAL_TOOL_NAME,
987
+ config,
988
+ cached["preview"],
989
+ )
990
+ pending = _latest_pending_private_swap_order(
991
+ _user_id(), PRIVATE_SWAP_APPROVAL_TOOL_NAME, cached["preview"]
992
+ )
993
+ if pending and effective_params.get("_resume_private_swap_order") is None:
994
+ effective_params["_resume_private_swap_order"] = pending
995
+ elif not effective_params.get("approval_token"):
996
+ raise RuntimeError(APPROVAL_CONTEXT_MISSING_MESSAGE)
997
+
998
+ try:
999
+ payload = _invoke_tool(tool_name, effective_params, config)
1000
+ except Exception as exc:
1001
+ raise _normalize_approval_context_error(exc) from exc
1002
+
1003
+ _cache_preview_for_approval(_user_id(), tool_name, payload)
1004
+ if tool_name == "swap_solana_privately" and payload.get("ok") is True:
1005
+ data = payload.get("data")
1006
+ approved_preview = effective_params.get("_approved_preview")
1007
+ if (
1008
+ isinstance(data, dict)
1009
+ and data.get("execution_state") == "awaiting_deposit_funding"
1010
+ and isinstance(approved_preview, dict)
1011
+ ):
1012
+ _cache_pending_private_swap_order(_user_id(), tool_name, approved_preview, data)
1013
+ elif isinstance(data, dict):
1014
+ _clear_pending_private_swap_order(_user_id(), tool_name)
1015
+ if tool_name == "continue_solana_private_swap" and payload.get("ok") is True:
1016
+ data = payload.get("data")
1017
+ if isinstance(data, dict) and data.get("execution_state") == "funding_submitted":
1018
+ _clear_pending_private_swap_order(_user_id(), PRIVATE_SWAP_APPROVAL_TOOL_NAME)
1019
+
1020
+ if payload.get("ok") is False:
1021
+ raise RuntimeError(str(payload.get("error") or f"{tool_name} failed"))
1022
+ return payload.get("data", {})
1023
+
1024
+
1025
+ def build_server():
1026
+ from fastmcp import FastMCP
1027
+ from fastmcp.tools import FunctionTool
1028
+
1029
+ mcp = FastMCP(
1030
+ "Agent Wallet",
1031
+ instructions=(
1032
+ "Use the local AgentLayer wallet runtime through explicit wallet tools. Keep wallet "
1033
+ "secrets local. Preview writes first when supported, and execute only after explicit "
1034
+ "user confirmation."
1035
+ ),
1036
+ )
1037
+
1038
+ async def _dispatch(tool_name: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
1039
+ params = params or {}
1040
+ if tool_name == "get_active_wallet_backend":
1041
+ return await _handle_get_active_wallet_backend()
1042
+ if tool_name == "set_wallet_backend":
1043
+ return await _handle_set_wallet_backend(params)
1044
+ if tool_name == "set_evm_network":
1045
+ return await _handle_set_evm_network(params)
1046
+ return await _handle_wallet_tool(tool_name, params)
1047
+
1048
+ for spec in _build_tool_definitions():
1049
+ tool_name = spec["name"]
1050
+
1051
+ def _tool_handler_factory(name: str):
1052
+ async def _tool_handler(**kwargs: Any) -> dict[str, Any]:
1053
+ return await _dispatch(name, kwargs)
1054
+
1055
+ return _tool_handler
1056
+
1057
+ mcp.add_tool(
1058
+ FunctionTool(
1059
+ name=tool_name,
1060
+ description=spec["description"],
1061
+ parameters=spec["input_schema"],
1062
+ output_schema={
1063
+ "type": "object",
1064
+ "additionalProperties": True,
1065
+ },
1066
+ fn=_tool_handler_factory(tool_name),
1067
+ )
1068
+ )
1069
+ return mcp
1070
+
1071
+
1072
+ def main() -> None:
1073
+ build_server().run(show_banner=False)
1074
+
1075
+
1076
+ if __name__ == "__main__":
1077
+ main()