@agentlayer.tech/wallet 0.1.27 → 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.
- package/.openclaw/extensions/agent-wallet/README.md +4 -5
- package/.openclaw/extensions/agent-wallet/dist/index.js +31 -31
- package/.openclaw/extensions/agent-wallet/index.ts +31 -31
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +2 -2
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +52 -0
- package/README.md +9 -0
- package/agent-wallet/README.md +18 -22
- package/agent-wallet/agent_wallet/bootstrap.py +28 -12
- package/agent-wallet/agent_wallet/btc_user_wallets.py +2 -7
- package/agent-wallet/agent_wallet/config.py +99 -22
- package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -14
- package/agent-wallet/agent_wallet/openclaw_adapter.py +72 -108
- package/agent-wallet/agent_wallet/openclaw_runtime.py +3 -12
- package/agent-wallet/agent_wallet/providers/kamino.py +21 -4
- package/agent-wallet/agent_wallet/providers/solana_rpc.py +0 -23
- package/agent-wallet/agent_wallet/providers/x402.py +198 -18
- package/agent-wallet/agent_wallet/user_wallets.py +4 -3
- package/agent-wallet/agent_wallet/wallet_layer/base.py +3 -3
- package/agent-wallet/agent_wallet/wallet_layer/factory.py +8 -5
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +437 -44
- package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +2 -8
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +13 -13
- package/agent-wallet/examples/openclaw_runtime_onboarding.py +1 -1
- package/agent-wallet/examples/openclaw_user_wallet_example.py +1 -1
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/bootstrap_openclaw_btc.py +3 -5
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -12
- package/agent-wallet/scripts/build_release_bundle.py +1 -0
- package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1 -4
- package/agent-wallet/scripts/install_agent_wallet.py +1 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +4 -6
- package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +2 -4
- package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +2 -15
- package/agent-wallet/scripts/reveal_btc_seed.sh +7 -16
- package/agent-wallet/scripts/setup_btc_wallet.sh +7 -16
- package/agent-wallet/scripts/setup_evm_wallet.sh +1 -11
- package/agent-wallet/scripts/switch_openclaw_wallet_network.py +4 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +0 -1
- package/bin/openclaw-agent-wallet.mjs +289 -0
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +20 -0
- package/claude-code/plugins/agent-wallet/.mcp.json +14 -0
- package/claude-code/plugins/agent-wallet/README.md +65 -0
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +34 -0
- package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +38 -0
- package/codex/plugins/agent-wallet/.mcp.json +15 -0
- package/codex/plugins/agent-wallet/README.md +39 -0
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +21 -0
- package/codex/plugins/agent-wallet/server.py +1077 -0
- package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
- package/hermes/plugins/agent_wallet/schemas.py +2 -2
- package/hermes/plugins/agent_wallet/tools.py +17 -3
- package/package.json +6 -1
- 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()
|