@agentlayer.tech/wallet 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@
7
7
  OpenClaw wallet tools or policy.
8
8
  - Added `wallet hermes install --yes` and `AGENT_WALLET_BOOT_KEY_FILE` support
9
9
  for smoother Hermes onboarding without manual `.env` editing.
10
+ - Added Hermes EVM onboarding helpers:
11
+ `agent_wallet_evm_status` and `agent_wallet_evm_setup`.
12
+ - Added host-side EVM bootstrap scripts for packaged/runtime installs:
13
+ `manage_openclaw_evm_wallet.py`, `bootstrap_openclaw_evm.py`, and
14
+ `setup_evm_wallet.sh`.
15
+ - Kept Hermes EVM routing on the existing `wdk-evm-wallet` and
16
+ `provider-gateway` path for `ethereum` and `base`, without copying wallet
17
+ policy or duplicating tool implementations.
10
18
  - Replaced the repo license with `PolyForm Small Business 1.0.0`.
11
19
  - Clarified in `README.md` that individuals can audit, fork, run, and modify
12
20
  the code for themselves, and that company use follows the PolyForm small
@@ -15,6 +23,23 @@
15
23
  `wdk-evm-wallet`; cross-chain swaps now stay on LI.FI/Jupiter-backed paths
16
24
  with Mayan denied as a LI.FI bridge.
17
25
 
26
+ ## v0.1.12 - 2026-05-06
27
+
28
+ ### Added
29
+
30
+ - Hermes EVM runtime helpers for packaged installs:
31
+ `agent_wallet_evm_status` and `agent_wallet_evm_setup`.
32
+ - Host-side EVM lifecycle scripts in the runtime bundle:
33
+ `manage_openclaw_evm_wallet.py`, `bootstrap_openclaw_evm.py`, and
34
+ `setup_evm_wallet.sh`.
35
+
36
+ ### Changed
37
+
38
+ - Hermes installs now support the same local EVM onboarding shape as BTC:
39
+ inspect runtime health, auto-start the local service, create or unlock the
40
+ vault wallet, and bind paired `ethereum/base` networks through the thin
41
+ bridge.
42
+
18
43
  ## v0.1.0-beta.2 - 2026-03-31
19
44
 
20
45
  Second public beta release that expands the stack beyond the initial MCP, wallet, and AgentLayer bridge scope.
package/README.md CHANGED
@@ -4,6 +4,11 @@
4
4
  ```bash
5
5
  npx @agentlayer.tech/wallet install --yes
6
6
  ```
7
+ For install to Hermes use
8
+ ```bash
9
+ npx @agentlayer.tech/wallet install --yes && npx @agentlayer.tech/wallet hermes install --yes
10
+ ```
11
+
7
12
  AgentLayer is a beta local-first wallet and finance stack for agents.
8
13
 
9
14
  The repository includes:
@@ -92,28 +97,10 @@ sh agent-wallet/scripts/setup_btc_wallet.sh
92
97
  EVM:
93
98
 
94
99
  ```bash
95
- cd wdk-evm-wallet && sh run-local.sh
100
+ sh agent-wallet/scripts/setup_evm_wallet.sh
96
101
  ```
97
102
 
98
- Create a local EVM wallet binding for an OpenClaw user:
99
-
100
- ```bash
101
- printf '%s\n' 'your-local-evm-password' | \
102
- agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-create \
103
- --user-id your-user-id \
104
- --password-stdin \
105
- --config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
106
- ```
107
-
108
- Unlock an existing EVM wallet binding:
109
-
110
- ```bash
111
- printf '%s\n' 'your-local-evm-password' | \
112
- agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-unlock \
113
- --user-id your-user-id \
114
- --password-stdin \
115
- --config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
116
- ```
103
+ That host-side bootstrap can auto-start the local `wdk-evm-wallet` service, create or unlock the vault wallet, bind both `base` and `ethereum` for the same local user, and patch OpenClaw config to `backend=wdk_evm_local`.
117
104
 
118
105
  That generates three fresh local secrets in the current shell session. If you prefer Python instead of `openssl`:
119
106
 
@@ -149,7 +136,15 @@ OpenClaw remains the primary local environment, but the repo also ships an optio
149
136
  hermes/plugins/agent_wallet
150
137
  ```
151
138
 
152
- It exposes only two Hermes tools: `agent_wallet_tools` for discovery and `agent_wallet_invoke` for forwarding a single call into the existing Python wallet CLI. Install it by symlinking the plugin directory into Hermes:
139
+ It exposes a thin bridge, not a port of wallet logic:
140
+
141
+ - `agent_wallet_tools`
142
+ - `agent_wallet_invoke`
143
+ - `agent_wallet_approve`
144
+ - `agent_wallet_evm_status`
145
+ - `agent_wallet_evm_setup`
146
+
147
+ Install it by symlinking the plugin directory into Hermes:
153
148
 
154
149
  ```bash
155
150
  npx @agentlayer.tech/wallet hermes install --yes
@@ -229,49 +224,32 @@ sh agent-wallet/scripts/reveal_btc_seed.sh
229
224
 
230
225
  ## EVM setup
231
226
 
232
- The EVM runtime is installed by `setup.sh`, but the host-side wallet onboarding is still a manual CLI flow.
233
-
234
- Start the local EVM service:
227
+ The EVM runtime is installed by `setup.sh`, and the host-side onboarding now has the same one-command shape as BTC:
235
228
 
236
229
  ```bash
237
- cd wdk-evm-wallet && sh run-local.sh
230
+ sh agent-wallet/scripts/setup_evm_wallet.sh
238
231
  ```
239
232
 
240
- Create a local EVM wallet binding for an OpenClaw user:
233
+ That flow:
241
234
 
242
- ```bash
243
- printf '%s\n' 'your-local-evm-password' | \
244
- agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-create \
245
- --user-id your-user-id \
246
- --password-stdin \
247
- --config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
248
- ```
235
+ - prompts for `user-id`
236
+ - prompts for `ethereum`, `base`, `sepolia`, or `base-sepolia`
237
+ - defaults to `http://127.0.0.1:8081`
238
+ - can auto-start `wdk-evm-wallet/run-local.sh` if the local service is not already healthy
239
+ - creates or unlocks the local EVM wallet binding
240
+ - also binds the paired EVM network by default: `ethereum <-> base`, `sepolia <-> base-sepolia`
241
+ - patches OpenClaw config to `backend=wdk_evm_local`
249
242
 
250
- Unlock an existing EVM wallet binding:
243
+ You can still use the lower-level CLI if needed:
251
244
 
252
245
  ```bash
253
246
  printf '%s\n' 'your-local-evm-password' | \
254
- agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-unlock \
247
+ agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-create \
255
248
  --user-id your-user-id \
256
249
  --password-stdin \
257
250
  --config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
258
251
  ```
259
252
 
260
- Then switch the OpenClaw plugin config to the EVM backend:
261
-
262
- ```bash
263
- AGENT_WALLET_BOOT_KEY='...' \
264
- agent-wallet/.venv/bin/python agent-wallet/scripts/install_openclaw_local_config.py \
265
- --backend wdk_evm_local \
266
- --network base \
267
- --user-id your-user-id \
268
- --package-root agent-wallet \
269
- --extension-path .openclaw/extensions/agent-wallet \
270
- --python-bin agent-wallet/.venv/bin/python
271
- ```
272
-
273
- That final config step assumes `~/.openclaw/sealed_keys.json` already exists. The normal path is to let the main installer create it by running install with `AGENT_WALLET_BOOT_KEY`, `AGENT_WALLET_MASTER_KEY`, and `AGENT_WALLET_APPROVAL_SECRET` available.
274
-
275
253
  Important EVM notes:
276
254
 
277
255
  - only localhost service URLs are supported for the OpenClaw EVM flow
@@ -34,6 +34,9 @@ The optional Hermes plugin is intentionally a bridge, not a port of the OpenClaw
34
34
 
35
35
  - `agent_wallet_tools` - read-only discovery for the underlying Python adapter tool specs.
36
36
  - `agent_wallet_invoke` - a dispatcher that calls `python -m agent_wallet.openclaw_cli invoke`.
37
+ - `agent_wallet_approve` - a host approval-token issuer for exact execute operations.
38
+ - `agent_wallet_evm_status` - a read-only EVM runtime and binding inspector.
39
+ - `agent_wallet_evm_setup` - a host-side EVM bootstrap helper for Hermes.
37
40
 
38
41
  Install it with:
39
42
 
@@ -45,6 +48,12 @@ That command symlinks `hermes/plugins/agent_wallet` into `~/.hermes/plugins/agen
45
48
 
46
49
  Hermes tool config must not contain wallet secrets. Use the existing sealed runtime path and host-issued approval tokens for execute flows. `AGENT_WALLET_BOOT_KEY_FILE` lets OpenClaw and Hermes reference one local boot-key file instead of duplicating the boot key across multiple env files.
47
50
 
51
+ For EVM on Hermes, the intended host flow is:
52
+
53
+ - call `agent_wallet_evm_status` to inspect `wdk-evm-wallet` health and current bindings
54
+ - call `agent_wallet_evm_setup` once to auto-start the local service when needed, create or unlock the local EVM wallet, and patch local OpenClaw config to `backend=wdk_evm_local`
55
+ - then use ordinary `agent_wallet_invoke` calls for EVM reads, transfers, swaps, and Aave flows
56
+
48
57
  Current safe tools:
49
58
 
50
59
  - `get_wallet_capabilities`
@@ -375,6 +384,21 @@ For the local EVM backend (`backend=wdk_evm_local`), the lifecycle mirrors the B
375
384
  - `evm-wallet-unlock`
376
385
  - `evm-wallet-lock`
377
386
 
387
+ For a simpler host-side bootstrap, use:
388
+
389
+ ```bash
390
+ sh agent-wallet/scripts/setup_evm_wallet.sh
391
+ ```
392
+
393
+ That wrapper:
394
+
395
+ - prompts for `user-id` and EVM network when run interactively
396
+ - defaults to `http://127.0.0.1:8081`
397
+ - can auto-start `wdk-evm-wallet/run-local.sh` if the local service is not already healthy
398
+ - creates or unlocks the local EVM wallet binding
399
+ - also binds the paired EVM network by default: `ethereum <-> base`, `sepolia <-> base-sepolia`
400
+ - patches OpenClaw config to `backend=wdk_evm_local`
401
+
378
402
  Example host-side EVM wallet creation:
379
403
 
380
404
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.11"
7
+ version = "0.1.12"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env python3
2
+ """One-command host bootstrap for the local OpenClaw EVM wallet flow."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ from pathlib import Path
13
+ from urllib.error import URLError
14
+ from urllib.parse import urlparse
15
+ from urllib.request import urlopen
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
18
+
19
+ from agent_wallet.file_ops import atomic_write_text, chmod_if_exists
20
+
21
+
22
+ def _default_config_path() -> Path:
23
+ return Path(os.path.expanduser("~/.openclaw/openclaw.json"))
24
+
25
+
26
+ def _default_user_id() -> str:
27
+ return f"{os.getenv('USER', 'openclaw-user')}-local"
28
+
29
+
30
+ def _default_python_bin() -> str:
31
+ return os.getenv("OPENCLAW_AGENT_WALLET_PYTHON", sys.executable)
32
+
33
+
34
+ def _package_root() -> Path:
35
+ return Path(__file__).resolve().parents[1]
36
+
37
+
38
+ def _repo_root() -> Path:
39
+ return Path(__file__).resolve().parents[2]
40
+
41
+
42
+ def _script_path(name: str) -> Path:
43
+ return _package_root() / "scripts" / name
44
+
45
+
46
+ def _normalize_network(value: str) -> str:
47
+ network = str(value or "").strip().lower()
48
+ aliases = {
49
+ "mainnet": "ethereum",
50
+ "eth": "ethereum",
51
+ "eth-mainnet": "ethereum",
52
+ "base-mainnet": "base",
53
+ "base_sepolia": "base-sepolia",
54
+ }
55
+ network = aliases.get(network, network)
56
+ if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
57
+ return "ethereum"
58
+ return network
59
+
60
+
61
+ def build_parser() -> argparse.ArgumentParser:
62
+ parser = argparse.ArgumentParser(description=__doc__)
63
+ parser.add_argument("--config-path", default=str(_default_config_path()))
64
+ parser.add_argument("--plugin-id", default="agent-wallet")
65
+ parser.add_argument("--user-id", default=_default_user_id())
66
+ parser.add_argument("--network", default="base")
67
+ parser.add_argument("--service-url", default="http://127.0.0.1:8081")
68
+ parser.add_argument("--wdk-wallet-root", default=str(_repo_root() / "wdk-evm-wallet"))
69
+ parser.add_argument("--label", default="Agent EVM Wallet")
70
+ parser.add_argument("--account-index", type=int, default=0)
71
+ parser.add_argument("--python-bin", default=_default_python_bin())
72
+ parser.add_argument("--package-root", default=str(_package_root()))
73
+ parser.add_argument("--password-stdin", action=argparse.BooleanOptionalAction, default=False)
74
+ parser.add_argument("--sign-only", action=argparse.BooleanOptionalAction, default=False)
75
+ parser.add_argument("--auto-start-service", action=argparse.BooleanOptionalAction, default=True)
76
+ parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
77
+ return parser
78
+
79
+
80
+ def _ensure_openclaw_config(config_path: Path) -> bool:
81
+ if config_path.exists():
82
+ return False
83
+ config_path.parent.mkdir(parents=True, exist_ok=True)
84
+ atomic_write_text(
85
+ config_path,
86
+ json.dumps({"plugins": {"entries": {}}, "tools": {"alsoAllow": []}}, indent=2) + "\n",
87
+ mode=0o600,
88
+ )
89
+ chmod_if_exists(config_path, 0o600)
90
+ return True
91
+
92
+
93
+ def _run_script(
94
+ python_bin: str,
95
+ script_name: str,
96
+ args: list[str],
97
+ *,
98
+ stdin_text: str | None = None,
99
+ ) -> dict:
100
+ completed = subprocess.run(
101
+ [python_bin, str(_script_path(script_name)), *args],
102
+ check=True,
103
+ capture_output=True,
104
+ text=True,
105
+ input=stdin_text,
106
+ env=os.environ.copy(),
107
+ )
108
+ return json.loads(completed.stdout)
109
+
110
+
111
+ def _health_url(service_url: str) -> str:
112
+ return f"{service_url.rstrip('/')}/health"
113
+
114
+
115
+ def _service_is_healthy(service_url: str) -> bool:
116
+ try:
117
+ with urlopen(_health_url(service_url), timeout=1.5) as response:
118
+ return int(getattr(response, "status", 0) or 0) == 200
119
+ except (URLError, TimeoutError, OSError):
120
+ return False
121
+
122
+
123
+ def _is_local_service_url(service_url: str) -> bool:
124
+ parsed = urlparse(service_url)
125
+ return parsed.scheme in {"http", "https"} and parsed.hostname in {"127.0.0.1", "localhost", "::1"}
126
+
127
+
128
+ def _require_local_service_url(service_url: str) -> None:
129
+ if not _is_local_service_url(service_url):
130
+ raise SystemExit(
131
+ f"EVM bootstrap only supports a localhost service URL. Refusing non-local endpoint: {service_url}"
132
+ )
133
+
134
+
135
+ def _service_log_dir(config_path: Path) -> Path:
136
+ return config_path.expanduser().parent / "logs"
137
+
138
+
139
+ def _service_log_path(config_path: Path) -> Path:
140
+ return _service_log_dir(config_path) / "wdk-evm-wallet.log"
141
+
142
+
143
+ def _auto_start_local_service(
144
+ *,
145
+ service_url: str,
146
+ network: str,
147
+ wdk_wallet_root: Path,
148
+ config_path: Path,
149
+ ) -> dict[str, object]:
150
+ if _service_is_healthy(service_url):
151
+ return {"started": False, "already_healthy": True}
152
+
153
+ if not _is_local_service_url(service_url):
154
+ raise SystemExit(
155
+ f"EVM service at {service_url} is unreachable and auto-start is only supported for localhost URLs."
156
+ )
157
+
158
+ run_local = wdk_wallet_root / "run-local.sh"
159
+ if not run_local.exists():
160
+ raise SystemExit(f"Could not find wdk-evm-wallet launcher: {run_local}")
161
+
162
+ parsed = urlparse(service_url)
163
+ host = parsed.hostname or "127.0.0.1"
164
+ port = parsed.port or 8081
165
+ log_dir = _service_log_dir(config_path)
166
+ log_dir.mkdir(parents=True, exist_ok=True)
167
+ log_path = _service_log_path(config_path)
168
+
169
+ env = os.environ.copy()
170
+ env["HOST"] = host
171
+ env["PORT"] = str(port)
172
+ env["WDK_EVM_NETWORK"] = network
173
+
174
+ with log_path.open("a", encoding="utf-8") as log_file:
175
+ process = subprocess.Popen( # noqa: S603
176
+ ["sh", str(run_local)],
177
+ cwd=str(wdk_wallet_root),
178
+ env=env,
179
+ stdin=subprocess.DEVNULL,
180
+ stdout=log_file,
181
+ stderr=log_file,
182
+ start_new_session=True,
183
+ )
184
+
185
+ deadline = time.time() + 30.0
186
+ while time.time() < deadline:
187
+ if _service_is_healthy(service_url):
188
+ return {
189
+ "started": True,
190
+ "already_healthy": False,
191
+ "pid": process.pid,
192
+ "log_path": str(log_path),
193
+ }
194
+ if process.poll() is not None:
195
+ raise SystemExit(
196
+ f"wdk-evm-wallet exited before becoming healthy. Check log: {log_path}"
197
+ )
198
+ time.sleep(0.5)
199
+
200
+ raise SystemExit(
201
+ f"Timed out waiting for wdk-evm-wallet health at {_health_url(service_url)}. Check log: {log_path}"
202
+ )
203
+
204
+
205
+ def main() -> int:
206
+ args = build_parser().parse_args()
207
+ effective_network = _normalize_network(args.network)
208
+ _require_local_service_url(args.service_url)
209
+ config_path = Path(args.config_path).expanduser()
210
+ config_created = _ensure_openclaw_config(config_path)
211
+ service_bootstrap: dict[str, object] | None = None
212
+ if args.auto_start_service:
213
+ service_bootstrap = _auto_start_local_service(
214
+ service_url=args.service_url,
215
+ network=effective_network,
216
+ wdk_wallet_root=Path(args.wdk_wallet_root).expanduser(),
217
+ config_path=config_path,
218
+ )
219
+ elif not _service_is_healthy(args.service_url):
220
+ raise SystemExit(
221
+ f"EVM service is not healthy at {_health_url(args.service_url)} and --no-auto-start-service was set."
222
+ )
223
+
224
+ stdin_text = sys.stdin.read() if args.password_stdin else None
225
+ setup_payload = _run_script(
226
+ args.python_bin,
227
+ "manage_openclaw_evm_wallet.py",
228
+ [
229
+ "setup",
230
+ "--user-id",
231
+ args.user_id,
232
+ "--network",
233
+ effective_network,
234
+ "--service-url",
235
+ args.service_url,
236
+ "--label",
237
+ args.label,
238
+ "--account-index",
239
+ str(args.account_index),
240
+ *([] if not args.password_stdin else ["--password-stdin"]),
241
+ *(["--bind-network-pair"] if args.bind_network_pair else ["--no-bind-network-pair"]),
242
+ ],
243
+ stdin_text=stdin_text,
244
+ )
245
+
246
+ wallet = dict(setup_payload.get("wallet") or {})
247
+ install_args = [
248
+ "--config-path",
249
+ str(config_path),
250
+ "--plugin-id",
251
+ args.plugin_id,
252
+ "--user-id",
253
+ args.user_id,
254
+ "--backend",
255
+ "wdk_evm_local",
256
+ "--network",
257
+ effective_network,
258
+ "--wdk-evm-service-url",
259
+ args.service_url,
260
+ "--wdk-evm-wallet-id",
261
+ str(wallet.get("wallet_id") or ""),
262
+ "--wdk-evm-account-index",
263
+ str(wallet.get("account_index") or args.account_index),
264
+ "--package-root",
265
+ args.package_root,
266
+ "--python-bin",
267
+ args.python_bin,
268
+ "--sign-only" if args.sign_only else "--no-sign-only",
269
+ ]
270
+ install_payload = _run_script(
271
+ args.python_bin,
272
+ "install_openclaw_local_config.py",
273
+ install_args,
274
+ )
275
+
276
+ print(
277
+ json.dumps(
278
+ {
279
+ "ok": True,
280
+ "config_created": config_created,
281
+ "service_bootstrap": service_bootstrap,
282
+ "evm_setup": setup_payload,
283
+ "openclaw_install": install_payload,
284
+ }
285
+ )
286
+ )
287
+ return 0
288
+
289
+
290
+ if __name__ == "__main__":
291
+ raise SystemExit(main())
@@ -72,6 +72,9 @@ def build_parser() -> argparse.ArgumentParser:
72
72
  parser.add_argument("--wdk-btc-service-url", default="")
73
73
  parser.add_argument("--wdk-btc-wallet-id", default="")
74
74
  parser.add_argument("--wdk-btc-account-index", type=int, default=0)
75
+ parser.add_argument("--wdk-evm-service-url", default="")
76
+ parser.add_argument("--wdk-evm-wallet-id", default="")
77
+ parser.add_argument("--wdk-evm-account-index", type=int, default=0)
75
78
  parser.add_argument("--sign-only", action=argparse.BooleanOptionalAction, default=False)
76
79
  parser.add_argument("--encrypt-user-wallets", action=argparse.BooleanOptionalAction, default=True)
77
80
  parser.add_argument(
@@ -183,6 +186,12 @@ def main() -> None:
183
186
  plugin_config["wdkBtcWalletId"] = args.wdk_btc_wallet_id.strip()
184
187
  if args.wdk_btc_account_index is not None:
185
188
  plugin_config["wdkBtcAccountIndex"] = int(args.wdk_btc_account_index)
189
+ if args.wdk_evm_service_url.strip():
190
+ plugin_config["wdkEvmServiceUrl"] = args.wdk_evm_service_url.strip()
191
+ if args.wdk_evm_wallet_id.strip():
192
+ plugin_config["wdkEvmWalletId"] = args.wdk_evm_wallet_id.strip()
193
+ if args.wdk_evm_account_index is not None:
194
+ plugin_config["wdkEvmAccountIndex"] = int(args.wdk_evm_account_index)
186
195
  if args.write_master_key:
187
196
  raise SystemExit(
188
197
  "Refusing to write masterKey into config. Runtime secrets must live in sealed_keys.json."
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env python3
2
+ """Host-side helper for managing a local OpenClaw EVM wallet binding."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from getpass import getpass
10
+ from pathlib import Path
11
+ from urllib.error import URLError
12
+ from urllib.request import urlopen
13
+
14
+ PACKAGE_ROOT = Path(__file__).resolve().parents[1]
15
+ if str(PACKAGE_ROOT) not in sys.path:
16
+ sys.path.insert(0, str(PACKAGE_ROOT))
17
+
18
+ from agent_wallet.config import settings # noqa: E402
19
+ from agent_wallet.evm_user_wallets import ( # noqa: E402
20
+ bind_user_evm_wallet,
21
+ create_user_evm_wallet,
22
+ get_user_evm_wallet_binding,
23
+ import_user_evm_wallet,
24
+ list_user_evm_wallet_bindings,
25
+ lock_user_evm_wallet,
26
+ unlock_user_evm_wallet,
27
+ )
28
+ from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient # noqa: E402
29
+
30
+
31
+ def _normalize_network(value: str) -> str:
32
+ network = str(value or "").strip().lower()
33
+ aliases = {
34
+ "mainnet": "ethereum",
35
+ "eth": "ethereum",
36
+ "eth-mainnet": "ethereum",
37
+ "base-mainnet": "base",
38
+ "base_sepolia": "base-sepolia",
39
+ }
40
+ network = aliases.get(network, network)
41
+ if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
42
+ return "ethereum"
43
+ return network
44
+
45
+
46
+ def _paired_network(network: str) -> str | None:
47
+ mapping = {
48
+ "ethereum": "base",
49
+ "base": "ethereum",
50
+ "sepolia": "base-sepolia",
51
+ "base-sepolia": "sepolia",
52
+ }
53
+ return mapping.get(_normalize_network(network))
54
+
55
+
56
+ def _read_secret(
57
+ *,
58
+ prompt: str,
59
+ confirm_prompt: str | None = None,
60
+ stdin_mode: bool = False,
61
+ ) -> str:
62
+ if stdin_mode:
63
+ value = sys.stdin.read().strip()
64
+ if not value:
65
+ raise SystemExit(f"{prompt.rstrip(':')} is required on stdin.")
66
+ return value
67
+ value = getpass(prompt)
68
+ if confirm_prompt is not None:
69
+ confirmed = getpass(confirm_prompt)
70
+ if value != confirmed:
71
+ raise SystemExit("Secrets did not match.")
72
+ if not value.strip():
73
+ raise SystemExit(f"{prompt.rstrip(':')} is required.")
74
+ return value.strip()
75
+
76
+
77
+ def _read_password_and_seed_from_stdin() -> tuple[str, str]:
78
+ raw = sys.stdin.read().strip()
79
+ if not raw:
80
+ raise SystemExit("Password and seed phrase payload is required on stdin.")
81
+ lines = raw.splitlines()
82
+ if len(lines) < 2:
83
+ raise SystemExit(
84
+ "For import via stdin, provide password on the first line and the seed phrase on the remaining lines."
85
+ )
86
+ password = lines[0].strip()
87
+ seed_phrase = " ".join(line.strip() for line in lines[1:] if line.strip())
88
+ if not password or not seed_phrase:
89
+ raise SystemExit("Both password and seed phrase are required.")
90
+ return password, seed_phrase
91
+
92
+
93
+ def _service_health(service_url: str | None) -> dict[str, object]:
94
+ target = str(service_url or "").strip()
95
+ if not target:
96
+ return {"service_url": None, "healthy": False, "error": "service_url is not configured"}
97
+ health_url = f"{target.rstrip('/')}/health"
98
+ try:
99
+ with urlopen(health_url, timeout=1.5) as response:
100
+ payload = json.loads(response.read().decode("utf-8"))
101
+ return {
102
+ "service_url": target,
103
+ "healthy": int(getattr(response, "status", 0) or 0) == 200,
104
+ "health": payload,
105
+ }
106
+ except (URLError, TimeoutError, OSError, ValueError) as exc:
107
+ return {"service_url": target, "healthy": False, "error": str(exc)}
108
+
109
+
110
+ def _status_payload(user_id: str | None, network: str | None, service_url: str | None) -> dict[str, object]:
111
+ target_service_url = str(service_url or settings.wdk_evm_service_url).strip() or None
112
+ payload: dict[str, object] = {
113
+ "ok": True,
114
+ "network": _normalize_network(network or "ethereum"),
115
+ "service": _service_health(target_service_url),
116
+ }
117
+ target_network = _normalize_network(network or "ethereum")
118
+ if target_service_url:
119
+ try:
120
+ payload["network_info"] = WdkEvmLocalClient(target_service_url).get_sync("/v1/evm/network")
121
+ except Exception as exc: # pragma: no cover - defensive
122
+ payload["network_info_error"] = str(exc)
123
+ if user_id:
124
+ payload["bindings"] = list_user_evm_wallet_bindings(user_id)
125
+ try:
126
+ payload["binding"] = get_user_evm_wallet_binding(user_id, network=target_network)
127
+ except Exception as exc:
128
+ payload["binding_error"] = str(exc)
129
+ return payload
130
+
131
+
132
+ def main() -> int:
133
+ parser = argparse.ArgumentParser(description="Manage a local OpenClaw EVM wallet binding")
134
+ subparsers = parser.add_subparsers(dest="command", required=True)
135
+
136
+ common_parent = argparse.ArgumentParser(add_help=False)
137
+ common_parent.add_argument("--network", default="ethereum")
138
+ common_parent.add_argument("--service-url")
139
+
140
+ get_parser = subparsers.add_parser("get", parents=[common_parent])
141
+ get_parser.add_argument("--user-id", required=True)
142
+
143
+ list_parser = subparsers.add_parser("list", parents=[common_parent])
144
+ list_parser.add_argument("--user-id", required=True)
145
+
146
+ status_parser = subparsers.add_parser("status", parents=[common_parent])
147
+ status_parser.add_argument("--user-id", default="")
148
+
149
+ setup_parser = subparsers.add_parser("setup", parents=[common_parent])
150
+ setup_parser.add_argument("--user-id", required=True)
151
+ setup_parser.add_argument("--label")
152
+ setup_parser.add_argument("--account-index", type=int)
153
+ setup_parser.add_argument("--password-stdin", action="store_true")
154
+ setup_parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
155
+
156
+ create_parser = subparsers.add_parser("create", parents=[common_parent])
157
+ create_parser.add_argument("--user-id", required=True)
158
+ create_parser.add_argument("--label")
159
+ create_parser.add_argument("--account-index", type=int)
160
+ create_parser.add_argument("--reveal-seed", action="store_true")
161
+ create_parser.add_argument("--password-stdin", action="store_true")
162
+ create_parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
163
+
164
+ import_parser = subparsers.add_parser("import", parents=[common_parent])
165
+ import_parser.add_argument("--user-id", required=True)
166
+ import_parser.add_argument("--label")
167
+ import_parser.add_argument("--account-index", type=int)
168
+ import_parser.add_argument("--password-stdin", action="store_true")
169
+ import_parser.add_argument("--seed-stdin", action="store_true")
170
+ import_parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
171
+
172
+ unlock_parser = subparsers.add_parser("unlock", parents=[common_parent])
173
+ unlock_parser.add_argument("--user-id", required=True)
174
+ unlock_parser.add_argument("--password-stdin", action="store_true")
175
+ unlock_parser.add_argument("--wallet-id", default="")
176
+ unlock_parser.add_argument("--account-index", type=int)
177
+ unlock_parser.add_argument("--bind-network-pair", action=argparse.BooleanOptionalAction, default=True)
178
+
179
+ lock_parser = subparsers.add_parser("lock", parents=[common_parent])
180
+ lock_parser.add_argument("--user-id", required=True)
181
+ lock_parser.add_argument("--wallet-id", default="")
182
+ lock_parser.add_argument("--account-index", type=int)
183
+
184
+ args = parser.parse_args()
185
+ effective_network = _normalize_network(args.network)
186
+
187
+ def _config_hint(wallet: dict[str, object]) -> dict[str, object]:
188
+ return {
189
+ "backend": "wdk_evm_local",
190
+ "network": effective_network,
191
+ "wdkEvmServiceUrl": args.service_url,
192
+ "wdkEvmWalletId": wallet.get("wallet_id"),
193
+ "wdkEvmAccountIndex": wallet.get("account_index"),
194
+ }
195
+
196
+ def _bind_pair(wallet: dict[str, object]) -> dict[str, object] | None:
197
+ if not getattr(args, "bind_network_pair", False):
198
+ return None
199
+ paired = _paired_network(effective_network)
200
+ if not paired:
201
+ return None
202
+ return bind_user_evm_wallet(
203
+ args.user_id,
204
+ wallet_id=str(wallet.get("wallet_id") or ""),
205
+ network=paired,
206
+ service_url=args.service_url,
207
+ account_index=wallet.get("account_index"),
208
+ tolerate_locked=True,
209
+ fallback_address=str(wallet.get("address") or "").strip() or None,
210
+ )
211
+
212
+ if args.command == "status":
213
+ payload = _status_payload(args.user_id or None, effective_network, args.service_url)
214
+ elif args.command == "list":
215
+ payload = {"ok": True, "wallets": list_user_evm_wallet_bindings(args.user_id)}
216
+ elif args.command == "get":
217
+ wallet = get_user_evm_wallet_binding(args.user_id, network=effective_network)
218
+ payload = {"ok": True, "wallet": wallet, "openclaw_config_hint": _config_hint(wallet)}
219
+ elif args.command == "setup":
220
+ password = _read_secret(
221
+ prompt="EVM wallet password: ",
222
+ confirm_prompt=None,
223
+ stdin_mode=bool(args.password_stdin),
224
+ )
225
+ try:
226
+ existing = get_user_evm_wallet_binding(args.user_id, network=effective_network)
227
+ except Exception:
228
+ existing = None
229
+
230
+ if existing is None:
231
+ wallet = create_user_evm_wallet(
232
+ args.user_id,
233
+ password=password,
234
+ label=args.label,
235
+ network=effective_network,
236
+ service_url=args.service_url,
237
+ account_index=args.account_index,
238
+ )
239
+ paired_binding = _bind_pair(wallet)
240
+ payload = {
241
+ "ok": True,
242
+ "action": "created",
243
+ "wallet": wallet,
244
+ "paired_binding": paired_binding,
245
+ "openclaw_config_hint": _config_hint(wallet),
246
+ }
247
+ else:
248
+ wallet = unlock_user_evm_wallet(
249
+ args.user_id,
250
+ password=password,
251
+ network=effective_network,
252
+ service_url=args.service_url,
253
+ account_index=args.account_index,
254
+ )
255
+ paired_binding = _bind_pair(wallet)
256
+ payload = {
257
+ "ok": True,
258
+ "action": "unlocked",
259
+ "wallet": wallet,
260
+ "paired_binding": paired_binding,
261
+ "openclaw_config_hint": _config_hint(wallet),
262
+ }
263
+ elif args.command == "create":
264
+ wallet = create_user_evm_wallet(
265
+ args.user_id,
266
+ password=_read_secret(
267
+ prompt="EVM wallet password: ",
268
+ confirm_prompt="Confirm EVM wallet password: ",
269
+ stdin_mode=bool(args.password_stdin),
270
+ ),
271
+ label=args.label,
272
+ network=effective_network,
273
+ service_url=args.service_url,
274
+ reveal_seed_phrase=bool(args.reveal_seed),
275
+ account_index=args.account_index,
276
+ )
277
+ payload = {
278
+ "ok": True,
279
+ "wallet": wallet,
280
+ "paired_binding": _bind_pair(wallet),
281
+ }
282
+ elif args.command == "import":
283
+ if args.password_stdin and args.seed_stdin:
284
+ password, seed_phrase = _read_password_and_seed_from_stdin()
285
+ else:
286
+ password = _read_secret(
287
+ prompt="EVM wallet password: ",
288
+ confirm_prompt="Confirm EVM wallet password: ",
289
+ stdin_mode=bool(args.password_stdin),
290
+ )
291
+ seed_phrase = _read_secret(
292
+ prompt="EVM seed phrase: ",
293
+ stdin_mode=bool(args.seed_stdin),
294
+ )
295
+ wallet = import_user_evm_wallet(
296
+ args.user_id,
297
+ password=password,
298
+ seed_phrase=seed_phrase,
299
+ label=args.label,
300
+ network=effective_network,
301
+ service_url=args.service_url,
302
+ account_index=args.account_index,
303
+ )
304
+ payload = {
305
+ "ok": True,
306
+ "wallet": wallet,
307
+ "paired_binding": _bind_pair(wallet),
308
+ }
309
+ elif args.command == "unlock":
310
+ wallet = unlock_user_evm_wallet(
311
+ args.user_id,
312
+ password=_read_secret(
313
+ prompt="EVM wallet password: ",
314
+ stdin_mode=bool(args.password_stdin),
315
+ ),
316
+ network=effective_network,
317
+ service_url=args.service_url,
318
+ wallet_id=args.wallet_id or None,
319
+ account_index=args.account_index,
320
+ )
321
+ payload = {
322
+ "ok": True,
323
+ "wallet": wallet,
324
+ "paired_binding": _bind_pair(wallet),
325
+ }
326
+ else:
327
+ payload = {
328
+ "ok": True,
329
+ "wallet": lock_user_evm_wallet(
330
+ args.user_id,
331
+ network=effective_network,
332
+ service_url=args.service_url,
333
+ wallet_id=args.wallet_id or None,
334
+ account_index=args.account_index,
335
+ ),
336
+ }
337
+
338
+ print(json.dumps(payload))
339
+ return 0
340
+
341
+
342
+ if __name__ == "__main__":
343
+ raise SystemExit(main())
@@ -0,0 +1,151 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
+ PACKAGE_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
6
+
7
+ resolve_python_bin() {
8
+ if [ -n "${OPENCLAW_AGENT_WALLET_PYTHON:-}" ] && [ -x "${OPENCLAW_AGENT_WALLET_PYTHON}" ]; then
9
+ printf "%s" "${OPENCLAW_AGENT_WALLET_PYTHON}"
10
+ return 0
11
+ fi
12
+
13
+ if [ -x "/tmp/agent-wallet-venv/bin/python" ]; then
14
+ printf "%s" "/tmp/agent-wallet-venv/bin/python"
15
+ return 0
16
+ fi
17
+
18
+ if [ -x "$PACKAGE_ROOT/.venv/bin/python" ]; then
19
+ printf "%s" "$PACKAGE_ROOT/.venv/bin/python"
20
+ return 0
21
+ fi
22
+
23
+ if command -v python3 >/dev/null 2>&1; then
24
+ command -v python3
25
+ return 0
26
+ fi
27
+
28
+ command -v python
29
+ }
30
+
31
+ PYTHON_BIN=$(resolve_python_bin)
32
+ export OPENCLAW_AGENT_WALLET_PYTHON="$PYTHON_BIN"
33
+
34
+ has_flag() {
35
+ flag=$1
36
+ shift
37
+ for arg in "$@"; do
38
+ case "$arg" in
39
+ "$flag"|"$flag"=*)
40
+ return 0
41
+ ;;
42
+ esac
43
+ done
44
+ return 1
45
+ }
46
+
47
+ prompt_with_default() {
48
+ label=$1
49
+ default_value=$2
50
+ if [ -t 0 ]; then
51
+ printf "%s [%s]: " "$label" "$default_value" >&2
52
+ read -r value
53
+ if [ -z "${value:-}" ]; then
54
+ printf "%s" "$default_value"
55
+ else
56
+ printf "%s" "$value"
57
+ fi
58
+ return 0
59
+ fi
60
+ printf "%s" "$default_value"
61
+ }
62
+
63
+ normalize_network_value() {
64
+ case $(printf "%s" "$1" | tr '[:upper:]' '[:lower:]') in
65
+ 1|ethereum|eth|mainnet)
66
+ printf "ethereum"
67
+ ;;
68
+ 2|base)
69
+ printf "base"
70
+ ;;
71
+ 3|sepolia)
72
+ printf "sepolia"
73
+ ;;
74
+ 4|base-sepolia|base_sepolia)
75
+ printf "base-sepolia"
76
+ ;;
77
+ *)
78
+ return 1
79
+ ;;
80
+ esac
81
+ }
82
+
83
+ prompt_network_choice() {
84
+ default_value=$1
85
+ if ! [ -t 0 ]; then
86
+ printf "%s" "$default_value"
87
+ return 0
88
+ fi
89
+
90
+ case "$default_value" in
91
+ ethereum) default_hint="1" ;;
92
+ base) default_hint="2" ;;
93
+ sepolia) default_hint="3" ;;
94
+ base-sepolia) default_hint="4" ;;
95
+ *) default_hint="2" ;;
96
+ esac
97
+
98
+ while true; do
99
+ printf "EVM network:\n" >&2
100
+ printf " 1) ethereum\n" >&2
101
+ printf " 2) base\n" >&2
102
+ printf " 3) sepolia\n" >&2
103
+ printf " 4) base-sepolia\n" >&2
104
+ printf "Choose network [%s]: " "$default_hint" >&2
105
+ read -r choice
106
+ if [ -z "${choice:-}" ]; then
107
+ choice=$default_hint
108
+ fi
109
+ if network=$(normalize_network_value "$choice"); then
110
+ printf "%s" "$network"
111
+ return 0
112
+ fi
113
+ printf "Invalid choice. Enter 1, 2, 3, 4, ethereum, base, sepolia, or base-sepolia.\n" >&2
114
+ done
115
+ }
116
+
117
+ DEFAULT_USER_ID=${OPENCLAW_EVM_USER_ID:-${USER:-openclaw-user}-local}
118
+ DEFAULT_NETWORK=${OPENCLAW_EVM_NETWORK:-base}
119
+ DEFAULT_SERVICE_URL=${OPENCLAW_EVM_SERVICE_URL:-http://127.0.0.1:8081}
120
+
121
+ if ! has_flag --user-id "$@"; then
122
+ USER_ID=$(prompt_with_default "OpenClaw user id" "$DEFAULT_USER_ID")
123
+ set -- "$@" --user-id "$USER_ID"
124
+ fi
125
+
126
+ if ! has_flag --network "$@"; then
127
+ NETWORK=$(prompt_network_choice "$DEFAULT_NETWORK")
128
+ set -- "$@" --network "$NETWORK"
129
+ fi
130
+
131
+ if ! has_flag --service-url "$@"; then
132
+ set -- "$@" --service-url "$DEFAULT_SERVICE_URL"
133
+ fi
134
+
135
+ if ! has_flag --config-path "$@" && [ -n "${OPENCLAW_EVM_CONFIG_PATH:-}" ]; then
136
+ set -- "$@" --config-path "$OPENCLAW_EVM_CONFIG_PATH"
137
+ fi
138
+
139
+ if ! has_flag --wdk-wallet-root "$@" && [ -n "${OPENCLAW_EVM_WDK_WALLET_ROOT:-}" ]; then
140
+ set -- "$@" --wdk-wallet-root "$OPENCLAW_EVM_WDK_WALLET_ROOT"
141
+ fi
142
+
143
+ if ! has_flag --python-bin "$@"; then
144
+ set -- "$@" --python-bin "$PYTHON_BIN"
145
+ fi
146
+
147
+ if ! has_flag --package-root "$@"; then
148
+ set -- "$@" --package-root "$PACKAGE_ROOT"
149
+ fi
150
+
151
+ exec "$PYTHON_BIN" "$SCRIPT_DIR/bootstrap_openclaw_evm.py" "$@"
@@ -1,7 +1,19 @@
1
1
  """Hermes Agent plugin bridge for AgentLayer wallet tools."""
2
2
 
3
- from .schemas import AGENT_WALLET_APPROVE, AGENT_WALLET_INVOKE, AGENT_WALLET_TOOLS
4
- from .tools import agent_wallet_approve, agent_wallet_invoke, agent_wallet_tools
3
+ from .schemas import (
4
+ AGENT_WALLET_APPROVE,
5
+ AGENT_WALLET_EVM_SETUP,
6
+ AGENT_WALLET_EVM_STATUS,
7
+ AGENT_WALLET_INVOKE,
8
+ AGENT_WALLET_TOOLS,
9
+ )
10
+ from .tools import (
11
+ agent_wallet_approve,
12
+ agent_wallet_evm_setup,
13
+ agent_wallet_evm_status,
14
+ agent_wallet_invoke,
15
+ agent_wallet_tools,
16
+ )
5
17
 
6
18
 
7
19
  def register(ctx):
@@ -27,3 +39,17 @@ def register(ctx):
27
39
  handler=agent_wallet_approve,
28
40
  description=AGENT_WALLET_APPROVE["description"],
29
41
  )
42
+ ctx.register_tool(
43
+ name=AGENT_WALLET_EVM_STATUS["name"],
44
+ toolset="agent_wallet",
45
+ schema=AGENT_WALLET_EVM_STATUS,
46
+ handler=agent_wallet_evm_status,
47
+ description=AGENT_WALLET_EVM_STATUS["description"],
48
+ )
49
+ ctx.register_tool(
50
+ name=AGENT_WALLET_EVM_SETUP["name"],
51
+ toolset="agent_wallet",
52
+ schema=AGENT_WALLET_EVM_SETUP,
53
+ handler=agent_wallet_evm_setup,
54
+ description=AGENT_WALLET_EVM_SETUP["description"],
55
+ )
@@ -5,3 +5,5 @@ provides_tools:
5
5
  - agent_wallet_tools
6
6
  - agent_wallet_invoke
7
7
  - agent_wallet_approve
8
+ - agent_wallet_evm_status
9
+ - agent_wallet_evm_setup
@@ -132,3 +132,75 @@ AGENT_WALLET_APPROVE = {
132
132
  "additionalProperties": False,
133
133
  },
134
134
  }
135
+
136
+ AGENT_WALLET_EVM_STATUS = {
137
+ "name": "agent_wallet_evm_status",
138
+ "description": (
139
+ "Inspect the local EVM wallet runtime used by AgentLayer/OpenClaw. "
140
+ "Returns wdk-evm-wallet health, network info, and existing user wallet "
141
+ "bindings without changing wallet state."
142
+ ),
143
+ "parameters": {
144
+ "type": "object",
145
+ "properties": {
146
+ "user_id": {
147
+ "type": "string",
148
+ "description": "Optional local wallet owner id to inspect.",
149
+ },
150
+ "network": {
151
+ "type": "string",
152
+ "description": "Optional EVM network hint, such as ethereum, base, sepolia, or base-sepolia.",
153
+ },
154
+ "service_url": {
155
+ "type": "string",
156
+ "description": "Optional localhost override for the wdk-evm-wallet service.",
157
+ },
158
+ },
159
+ "additionalProperties": False,
160
+ },
161
+ }
162
+
163
+ AGENT_WALLET_EVM_SETUP = {
164
+ "name": "agent_wallet_evm_setup",
165
+ "description": (
166
+ "Create or unlock the local EVM wallet binding used by AgentLayer/OpenClaw "
167
+ "for Hermes. This can auto-start the localhost-only wdk-evm-wallet service, "
168
+ "set up the selected network, and bind the same wallet to the paired EVM network "
169
+ "such as ethereum/base."
170
+ ),
171
+ "parameters": {
172
+ "type": "object",
173
+ "properties": {
174
+ "password": {
175
+ "type": "string",
176
+ "description": "Local EVM wallet password used to create or unlock the vault wallet.",
177
+ },
178
+ "user_id": {
179
+ "type": "string",
180
+ "description": "Optional local wallet owner id. Defaults to AGENT_WALLET_USER_ID, USER, or hermes-local-user.",
181
+ },
182
+ "network": {
183
+ "type": "string",
184
+ "description": "Selected EVM network, typically ethereum or base.",
185
+ },
186
+ "label": {
187
+ "type": "string",
188
+ "description": "Optional wallet label used when creating a new local EVM wallet.",
189
+ },
190
+ "service_url": {
191
+ "type": "string",
192
+ "description": "Optional localhost override for the wdk-evm-wallet service.",
193
+ },
194
+ "auto_start_service": {
195
+ "type": "boolean",
196
+ "description": "Whether to auto-start the local wdk-evm-wallet service when it is not healthy. Defaults to true.",
197
+ },
198
+ "bind_network_pair": {
199
+ "type": "boolean",
200
+ "description": "Whether to also bind the paired EVM network such as ethereum/base. Defaults to true.",
201
+ },
202
+ },
203
+ "required": ["password"],
204
+ "additionalProperties": False,
205
+ },
206
+ }
@@ -174,6 +174,13 @@ def _python_bin(package_root: Path) -> str:
174
174
  return "python3"
175
175
 
176
176
 
177
+ def _script_path(package_root: Path, name: str) -> Path:
178
+ script = package_root / "scripts" / name
179
+ if not script.exists():
180
+ raise RuntimeError(f"Required host script is missing: {script}")
181
+ return script
182
+
183
+
177
184
  def _user_id(args: dict[str, Any]) -> str:
178
185
  value = (
179
186
  args.get("user_id")
@@ -277,6 +284,37 @@ def _call_wallet_cli(args: dict[str, Any]) -> dict[str, Any]:
277
284
  return {"ok": False, "error": f"wallet CLI returned invalid JSON: {exc}"}
278
285
 
279
286
 
287
+ def _run_host_script(
288
+ package_root: Path,
289
+ script_name: str,
290
+ script_args: list[str],
291
+ *,
292
+ stdin_text: str | None = None,
293
+ ) -> dict[str, Any]:
294
+ command = [
295
+ _python_bin(package_root),
296
+ str(_script_path(package_root, script_name)),
297
+ *script_args,
298
+ ]
299
+ completed = subprocess.run(
300
+ command,
301
+ cwd=str(package_root),
302
+ env=_cli_env(package_root),
303
+ text=True,
304
+ input=stdin_text,
305
+ capture_output=True,
306
+ timeout=float(os.getenv("AGENT_WALLET_HERMES_TIMEOUT", "120")),
307
+ check=False,
308
+ )
309
+ if completed.returncode != 0:
310
+ detail = completed.stderr.strip() or completed.stdout.strip()
311
+ return {"ok": False, "error": detail or f"host script exited {completed.returncode}"}
312
+ try:
313
+ return json.loads(completed.stdout.strip() or "{}")
314
+ except json.JSONDecodeError as exc:
315
+ return {"ok": False, "error": f"host script returned invalid JSON: {exc}"}
316
+
317
+
280
318
  def _call_issue_approval(args: dict[str, Any]) -> dict[str, Any]:
281
319
  if args.get("user_confirmed") is not True:
282
320
  raise RuntimeError(
@@ -337,6 +375,54 @@ def _call_issue_approval(args: dict[str, Any]) -> dict[str, Any]:
337
375
  return {"ok": False, "error": f"wallet CLI returned invalid JSON: {exc}"}
338
376
 
339
377
 
378
+ def _call_evm_status(args: dict[str, Any]) -> dict[str, Any]:
379
+ package_root = _resolve_package_root()
380
+ command = ["status"]
381
+ user_id = str(args.get("user_id") or "").strip()
382
+ network = str(args.get("network") or "").strip()
383
+ service_url = str(args.get("service_url") or "").strip()
384
+ if user_id:
385
+ command.extend(["--user-id", user_id])
386
+ if network:
387
+ command.extend(["--network", network])
388
+ if service_url:
389
+ command.extend(["--service-url", service_url])
390
+ return _run_host_script(package_root, "manage_openclaw_evm_wallet.py", command)
391
+
392
+
393
+ def _call_evm_setup(args: dict[str, Any]) -> dict[str, Any]:
394
+ package_root = _resolve_package_root()
395
+ password = str(args.get("password") or "").strip()
396
+ if not password:
397
+ raise RuntimeError("password is required.")
398
+ command = [
399
+ "--user-id",
400
+ _user_id(args),
401
+ "--password-stdin",
402
+ ]
403
+ network = str(args.get("network") or "").strip()
404
+ label = str(args.get("label") or "").strip()
405
+ service_url = str(args.get("service_url") or "").strip()
406
+ auto_start_service = args.get("auto_start_service")
407
+ bind_network_pair = args.get("bind_network_pair")
408
+ if network:
409
+ command.extend(["--network", network])
410
+ if label:
411
+ command.extend(["--label", label])
412
+ if service_url:
413
+ command.extend(["--service-url", service_url])
414
+ if auto_start_service is False:
415
+ command.append("--no-auto-start-service")
416
+ if bind_network_pair is False:
417
+ command.append("--no-bind-network-pair")
418
+ return _run_host_script(
419
+ package_root,
420
+ "bootstrap_openclaw_evm.py",
421
+ command,
422
+ stdin_text=f"{password}\n",
423
+ )
424
+
425
+
340
426
  class _SchemaOnlyBackend:
341
427
  def __init__(self, *, name: str, chain: str, network: str):
342
428
  self.name = name
@@ -431,3 +517,17 @@ def agent_wallet_approve(args: dict, **kwargs) -> str:
431
517
  return _json(_call_issue_approval(args or {}))
432
518
  except Exception as exc:
433
519
  return _json({"ok": False, "error": str(exc)})
520
+
521
+
522
+ def agent_wallet_evm_status(args: dict, **kwargs) -> str:
523
+ try:
524
+ return _json(_call_evm_status(args or {}))
525
+ except Exception as exc:
526
+ return _json({"ok": False, "error": str(exc)})
527
+
528
+
529
+ def agent_wallet_evm_setup(args: dict, **kwargs) -> str:
530
+ try:
531
+ return _json(_call_evm_setup(args or {}))
532
+ except Exception as exc:
533
+ return _json({"ok": False, "error": str(exc)})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {