@agentlayer.tech/wallet 0.1.28 → 0.1.32

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