@agentlayer.tech/wallet 0.1.73 → 0.1.75
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/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +72 -0
- package/RELEASING.md +23 -4
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/boot_key_migration.py +10 -2
- package/agent-wallet/agent_wallet/boot_key_recovery.py +6 -4
- package/agent-wallet/agent_wallet/config.py +125 -23
- package/agent-wallet/agent_wallet/encrypted_storage.py +85 -0
- package/agent-wallet/agent_wallet/evm_user_wallets.py +116 -23
- package/agent-wallet/agent_wallet/keystore.py +82 -2
- package/agent-wallet/agent_wallet/sealed_keys.py +19 -18
- package/agent-wallet/agent_wallet/user_wallets.py +6 -5
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +8 -3
- package/agent-wallet/scripts/install_agent_wallet.py +4 -1
- package/bin/openclaw-agent-wallet.mjs +738 -88
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/.mcp.json +1 -2
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +2 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +3 -2
- package/wdk-evm-wallet/src/config.js +24 -0
- package/wdk-evm-wallet/src/server.js +2 -0
|
@@ -22,11 +22,14 @@ from agent_wallet.config import (
|
|
|
22
22
|
resolve_openclaw_home,
|
|
23
23
|
settings,
|
|
24
24
|
)
|
|
25
|
+
from agent_wallet.file_ops import atomic_write_text
|
|
25
26
|
from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient
|
|
26
27
|
from agent_wallet.user_wallets import normalize_user_id
|
|
27
28
|
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
28
29
|
|
|
29
30
|
LOCAL_WDK_EVM_HOSTS = {"127.0.0.1", "localhost", "::1"}
|
|
31
|
+
_SERVICE_OWNER_FILENAME = "service-owner.json"
|
|
32
|
+
_INSTANCE_ID_FILENAME = "instance-id"
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
def _normalize_evm_network(value: str | None) -> str:
|
|
@@ -93,6 +96,52 @@ def _expected_local_service_data_dir() -> Path:
|
|
|
93
96
|
return (resolve_openclaw_home() / "wdk-evm-wallet").resolve()
|
|
94
97
|
|
|
95
98
|
|
|
99
|
+
def _expected_local_service_instance_id() -> str:
|
|
100
|
+
path = _expected_local_service_data_dir() / _INSTANCE_ID_FILENAME
|
|
101
|
+
try:
|
|
102
|
+
existing = path.read_text(encoding="utf-8").strip()
|
|
103
|
+
except OSError:
|
|
104
|
+
existing = ""
|
|
105
|
+
if existing:
|
|
106
|
+
return existing
|
|
107
|
+
generated = secrets.token_hex(16)
|
|
108
|
+
atomic_write_text(path, generated + "\n", mode=0o600)
|
|
109
|
+
return generated
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _service_owner_path() -> Path:
|
|
113
|
+
return _expected_local_service_data_dir() / _SERVICE_OWNER_FILENAME
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _read_service_owner() -> dict[str, Any] | None:
|
|
117
|
+
try:
|
|
118
|
+
payload = json.loads(_service_owner_path().read_text(encoding="utf-8"))
|
|
119
|
+
except (OSError, ValueError):
|
|
120
|
+
return None
|
|
121
|
+
return payload if isinstance(payload, dict) else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _write_service_owner(health: dict[str, Any], service_url: str) -> int:
|
|
125
|
+
expected_instance = _expected_local_service_instance_id()
|
|
126
|
+
instance_id = str(health.get("instanceId") or "").strip()
|
|
127
|
+
try:
|
|
128
|
+
pid = int(health.get("pid") or 0)
|
|
129
|
+
except (TypeError, ValueError):
|
|
130
|
+
pid = 0
|
|
131
|
+
if instance_id != expected_instance or pid <= 0:
|
|
132
|
+
raise WalletBackendError("wdk-evm-wallet health did not confirm local process ownership.")
|
|
133
|
+
payload = {
|
|
134
|
+
"version": 1,
|
|
135
|
+
"pid": pid,
|
|
136
|
+
"instance_id": instance_id,
|
|
137
|
+
"port": urlparse(service_url).port or 8081,
|
|
138
|
+
"data_dir": str(_expected_local_service_data_dir()),
|
|
139
|
+
"service_version": str(health.get("version") or "").strip() or None,
|
|
140
|
+
}
|
|
141
|
+
atomic_write_text(_service_owner_path(), json.dumps(payload, indent=2) + "\n", mode=0o600)
|
|
142
|
+
return pid
|
|
143
|
+
|
|
144
|
+
|
|
96
145
|
def _same_path(left: str | Path | None, right: str | Path | None) -> bool:
|
|
97
146
|
if left is None or right is None:
|
|
98
147
|
return False
|
|
@@ -120,6 +169,10 @@ def _should_restart_local_service(
|
|
|
120
169
|
if reported_data_dir and not _same_path(reported_data_dir, _expected_local_service_data_dir()):
|
|
121
170
|
return True
|
|
122
171
|
|
|
172
|
+
reported_instance = str(health.get("instanceId") or "").strip()
|
|
173
|
+
if reported_data_dir and reported_instance != _expected_local_service_instance_id():
|
|
174
|
+
return True
|
|
175
|
+
|
|
123
176
|
return False
|
|
124
177
|
|
|
125
178
|
|
|
@@ -147,42 +200,77 @@ def _listening_pids(port: int) -> list[int]:
|
|
|
147
200
|
return pids
|
|
148
201
|
|
|
149
202
|
|
|
150
|
-
def _stop_local_service(service_url: str) -> None:
|
|
203
|
+
def _stop_local_service(service_url: str, health: dict[str, Any] | None = None) -> None:
|
|
151
204
|
"""Gracefully stop a local wdk-evm-wallet daemon so a fresh one can start.
|
|
152
205
|
|
|
153
206
|
SIGTERM the listener(s), wait for /health to drop, then SIGKILL as a fallback.
|
|
154
207
|
"""
|
|
155
208
|
port = urlparse(service_url).port or 8081
|
|
156
|
-
|
|
157
|
-
if not
|
|
158
|
-
# Nothing identifiable to stop (e.g. lsof missing). Surface a clear,
|
|
159
|
-
# actionable error rather than racing into an EADDRINUSE start failure.
|
|
209
|
+
current_health = health if health is not None else _service_health(service_url)
|
|
210
|
+
if not current_health or current_health.get("service") != "wdk-evm-wallet":
|
|
160
211
|
raise WalletBackendError(
|
|
161
|
-
f"
|
|
162
|
-
"identified to restart it. Stop it manually and retry."
|
|
212
|
+
f"Refusing to stop an unidentified service on port {port}."
|
|
163
213
|
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
214
|
+
listeners = _listening_pids(port)
|
|
215
|
+
owner = _read_service_owner()
|
|
216
|
+
expected_instance = _expected_local_service_instance_id()
|
|
217
|
+
reported_instance = str(current_health.get("instanceId") or "").strip()
|
|
218
|
+
reported_data_dir = str(current_health.get("dataDir") or "").strip()
|
|
219
|
+
try:
|
|
220
|
+
reported_pid = int(current_health.get("pid") or 0)
|
|
221
|
+
except (TypeError, ValueError):
|
|
222
|
+
reported_pid = 0
|
|
223
|
+
|
|
224
|
+
owned_pid = 0
|
|
225
|
+
if reported_instance == expected_instance and reported_pid > 0:
|
|
226
|
+
owner_matches = (
|
|
227
|
+
not owner
|
|
228
|
+
or (
|
|
229
|
+
int(owner.get("pid") or 0) == reported_pid
|
|
230
|
+
and str(owner.get("instance_id") or "") == expected_instance
|
|
231
|
+
and int(owner.get("port") or 0) == port
|
|
232
|
+
)
|
|
233
|
+
or int(owner.get("pid") or 0) not in listeners
|
|
234
|
+
)
|
|
235
|
+
if owner_matches and (not listeners or reported_pid in listeners):
|
|
236
|
+
owned_pid = reported_pid
|
|
237
|
+
elif not reported_instance and reported_data_dir and len(listeners) == 1:
|
|
238
|
+
# One-time compatibility path for pre-instance-id daemons.
|
|
239
|
+
owned_pid = listeners[0]
|
|
240
|
+
|
|
241
|
+
if owned_pid <= 0:
|
|
242
|
+
raise WalletBackendError(
|
|
243
|
+
f"A stale wdk-evm-wallet is running on port {port} but ownership could not be verified. "
|
|
244
|
+
"Stop it manually and retry."
|
|
245
|
+
)
|
|
246
|
+
try:
|
|
247
|
+
os.kill(owned_pid, signal.SIGTERM)
|
|
248
|
+
except ProcessLookupError:
|
|
249
|
+
pass
|
|
250
|
+
except PermissionError as exc:
|
|
251
|
+
raise WalletBackendError(
|
|
252
|
+
f"Cannot stop stale wdk-evm-wallet (pid {owned_pid}): {exc}."
|
|
253
|
+
) from exc
|
|
173
254
|
deadline = time.time() + 10.0
|
|
174
255
|
while time.time() < deadline:
|
|
175
256
|
if _service_health(service_url) is None:
|
|
257
|
+
try:
|
|
258
|
+
_service_owner_path().unlink()
|
|
259
|
+
except FileNotFoundError:
|
|
260
|
+
pass
|
|
176
261
|
return
|
|
177
262
|
time.sleep(0.3)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
continue
|
|
263
|
+
try:
|
|
264
|
+
os.kill(owned_pid, signal.SIGKILL)
|
|
265
|
+
except ProcessLookupError:
|
|
266
|
+
pass
|
|
183
267
|
deadline = time.time() + 5.0
|
|
184
268
|
while time.time() < deadline:
|
|
185
269
|
if _service_health(service_url) is None:
|
|
270
|
+
try:
|
|
271
|
+
_service_owner_path().unlink()
|
|
272
|
+
except FileNotFoundError:
|
|
273
|
+
pass
|
|
186
274
|
return
|
|
187
275
|
time.sleep(0.3)
|
|
188
276
|
raise WalletBackendError(f"Failed to stop stale wdk-evm-wallet on port {port}.")
|
|
@@ -222,8 +310,10 @@ def _auto_start_local_service(service_url: str, network: str) -> None:
|
|
|
222
310
|
if not _is_local_service_url(service_url):
|
|
223
311
|
return
|
|
224
312
|
if not _should_restart_local_service(health, wallet_root=wallet_root):
|
|
313
|
+
if str(health.get("instanceId") or "").strip():
|
|
314
|
+
_write_service_owner(health, service_url)
|
|
225
315
|
return
|
|
226
|
-
_stop_local_service(service_url)
|
|
316
|
+
_stop_local_service(service_url, health)
|
|
227
317
|
if not _is_local_service_url(service_url):
|
|
228
318
|
raise WalletBackendError(
|
|
229
319
|
f"wdk-evm-wallet is unreachable at {_health_url(service_url)} and auto-start only supports localhost URLs."
|
|
@@ -237,6 +327,7 @@ def _auto_start_local_service(service_url: str, network: str) -> None:
|
|
|
237
327
|
env["HOST"] = parsed.hostname or "127.0.0.1"
|
|
238
328
|
env["PORT"] = str(parsed.port or 8081)
|
|
239
329
|
env["WDK_EVM_NETWORK"] = _normalize_evm_network(network)
|
|
330
|
+
env["WDK_EVM_INSTANCE_ID"] = _expected_local_service_instance_id()
|
|
240
331
|
process = subprocess.Popen( # noqa: S603
|
|
241
332
|
["sh", str(wallet_root / "run-local.sh")],
|
|
242
333
|
cwd=str(wallet_root),
|
|
@@ -248,7 +339,9 @@ def _auto_start_local_service(service_url: str, network: str) -> None:
|
|
|
248
339
|
)
|
|
249
340
|
deadline = time.time() + 30.0
|
|
250
341
|
while time.time() < deadline:
|
|
251
|
-
|
|
342
|
+
health = _service_health(service_url)
|
|
343
|
+
if health is not None:
|
|
344
|
+
_write_service_owner(health, service_url)
|
|
252
345
|
return
|
|
253
346
|
if process.poll() is not None:
|
|
254
347
|
raise WalletBackendError("wdk-evm-wallet exited before becoming healthy.")
|
|
@@ -13,6 +13,7 @@ BOOT_KEY_KEYCHAIN_ARCHITECTURE.md.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import json
|
|
16
17
|
import os
|
|
17
18
|
import platform
|
|
18
19
|
import shutil
|
|
@@ -27,6 +28,8 @@ KEYSTORE_SERVICE = "ai.agentlayer.wallet"
|
|
|
27
28
|
BOOT_KEY_ITEM = "boot_key"
|
|
28
29
|
_PROBE_ITEM = "__probe__"
|
|
29
30
|
_KEYSTORE_BACKEND_ENV = "AGENT_WALLET_KEYSTORE_BACKEND"
|
|
31
|
+
_KEYSTORE_STATE_VERSION = 1
|
|
32
|
+
_KEYSTORE_STATE_FILENAME = "backend.json"
|
|
30
33
|
|
|
31
34
|
_SECURITY_BIN = "/usr/bin/security"
|
|
32
35
|
_SUBPROCESS_TIMEOUT = 10.0
|
|
@@ -49,6 +52,31 @@ def _backend_preference() -> str:
|
|
|
49
52
|
return os.getenv(_KEYSTORE_BACKEND_ENV, "auto").strip().lower()
|
|
50
53
|
|
|
51
54
|
|
|
55
|
+
def _state_path() -> Path:
|
|
56
|
+
return resolve_openclaw_home() / "keystore" / _KEYSTORE_STATE_FILENAME
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _read_backend_state() -> dict[str, str] | None:
|
|
60
|
+
try:
|
|
61
|
+
payload = json.loads(_state_path().read_text(encoding="utf-8"))
|
|
62
|
+
except (OSError, ValueError):
|
|
63
|
+
return None
|
|
64
|
+
if not isinstance(payload, dict) or payload.get("version") != _KEYSTORE_STATE_VERSION:
|
|
65
|
+
return None
|
|
66
|
+
backend = str(payload.get("backend") or "").strip()
|
|
67
|
+
service = str(payload.get("service") or "").strip()
|
|
68
|
+
if backend not in {
|
|
69
|
+
"macos-keychain",
|
|
70
|
+
"windows-dpapi",
|
|
71
|
+
"linux-secretservice",
|
|
72
|
+
"plaintext-file",
|
|
73
|
+
}:
|
|
74
|
+
return None
|
|
75
|
+
if service != _service():
|
|
76
|
+
return None
|
|
77
|
+
return {"backend": backend, "service": service}
|
|
78
|
+
|
|
79
|
+
|
|
52
80
|
class KeyStoreError(Exception):
|
|
53
81
|
"""Raised when a keystore backend operation fails unexpectedly."""
|
|
54
82
|
|
|
@@ -247,7 +275,7 @@ class PlaintextFileStore:
|
|
|
247
275
|
pass
|
|
248
276
|
|
|
249
277
|
|
|
250
|
-
_keystore_cache: dict[tuple[str, str], KeyStore] = {}
|
|
278
|
+
_keystore_cache: dict[tuple[str, str, str], KeyStore] = {}
|
|
251
279
|
|
|
252
280
|
|
|
253
281
|
def clear_keystore_cache() -> None:
|
|
@@ -263,7 +291,11 @@ def resolve_keystore() -> KeyStore:
|
|
|
263
291
|
(preference, service) for the process lifetime. Cleared by
|
|
264
292
|
``agent_wallet.config.clear_secret_caches`` / ``reload_settings``.
|
|
265
293
|
"""
|
|
266
|
-
cache_key = (
|
|
294
|
+
cache_key = (
|
|
295
|
+
_backend_preference(),
|
|
296
|
+
_service(),
|
|
297
|
+
str(resolve_openclaw_home().resolve()),
|
|
298
|
+
)
|
|
267
299
|
cached = _keystore_cache.get(cache_key)
|
|
268
300
|
if cached is not None:
|
|
269
301
|
return cached
|
|
@@ -293,6 +325,12 @@ def _resolve_keystore_uncached() -> KeyStore:
|
|
|
293
325
|
# _backend_usable falls back gracefully if a session still can't authorize.
|
|
294
326
|
candidates = [MacKeychainStore(), WindowsDpapiStore(), LinuxSecretServiceStore()]
|
|
295
327
|
|
|
328
|
+
if preference == "auto":
|
|
329
|
+
state = _read_backend_state()
|
|
330
|
+
pinned = _store_for_backend(state["backend"]) if state else None
|
|
331
|
+
if pinned is not None:
|
|
332
|
+
candidates = [pinned, *[item for item in candidates if item.backend_id != pinned.backend_id]]
|
|
333
|
+
|
|
296
334
|
for candidate in candidates:
|
|
297
335
|
try:
|
|
298
336
|
if candidate.available() and _backend_usable(candidate):
|
|
@@ -302,6 +340,48 @@ def _resolve_keystore_uncached() -> KeyStore:
|
|
|
302
340
|
return PlaintextFileStore()
|
|
303
341
|
|
|
304
342
|
|
|
343
|
+
def _store_for_backend(backend_id: str) -> KeyStore | None:
|
|
344
|
+
stores: dict[str, KeyStore] = {
|
|
345
|
+
"macos-keychain": MacKeychainStore(),
|
|
346
|
+
"windows-dpapi": WindowsDpapiStore(),
|
|
347
|
+
"linux-secretservice": LinuxSecretServiceStore(),
|
|
348
|
+
"plaintext-file": PlaintextFileStore(),
|
|
349
|
+
}
|
|
350
|
+
return stores.get(backend_id)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def record_keystore_backend(store: KeyStore) -> dict[str, object]:
|
|
354
|
+
"""Persist a verified boot-key backend without replacing a temporary fallback."""
|
|
355
|
+
existing = _read_backend_state()
|
|
356
|
+
preference = _backend_preference()
|
|
357
|
+
if preference == "auto" and existing and existing["backend"] != store.backend_id:
|
|
358
|
+
return {
|
|
359
|
+
"recorded": False,
|
|
360
|
+
"backend": existing["backend"],
|
|
361
|
+
"fallback_backend": store.backend_id,
|
|
362
|
+
}
|
|
363
|
+
payload = {
|
|
364
|
+
"version": _KEYSTORE_STATE_VERSION,
|
|
365
|
+
"backend": store.backend_id,
|
|
366
|
+
"service": _service(),
|
|
367
|
+
}
|
|
368
|
+
atomic_write_text(_state_path(), json.dumps(payload, indent=2) + "\n", mode=0o600)
|
|
369
|
+
return {"recorded": True, "backend": store.backend_id, "fallback_backend": None}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def keystore_backend_status() -> dict[str, object]:
|
|
373
|
+
"""Return non-secret diagnostics for the selected and persisted backend."""
|
|
374
|
+
state = _read_backend_state()
|
|
375
|
+
selected = resolve_keystore().backend_id
|
|
376
|
+
pinned = state["backend"] if state else None
|
|
377
|
+
return {
|
|
378
|
+
"selected": selected,
|
|
379
|
+
"pinned": pinned,
|
|
380
|
+
"fallback_active": bool(pinned and selected != pinned),
|
|
381
|
+
"preference": _backend_preference(),
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
305
385
|
def _backend_usable(candidate: KeyStore) -> bool:
|
|
306
386
|
"""Return true when a native backend can read the live key or write safely.
|
|
307
387
|
|
|
@@ -5,7 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from agent_wallet.encrypted_storage import
|
|
8
|
+
from agent_wallet.encrypted_storage import (
|
|
9
|
+
decrypt_secret_material,
|
|
10
|
+
encrypt_secret_material,
|
|
11
|
+
remove_envelope_migration_backup,
|
|
12
|
+
)
|
|
9
13
|
from agent_wallet.file_ops import atomic_write_text
|
|
10
14
|
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
11
15
|
|
|
@@ -45,6 +49,7 @@ def seal_keys(boot_key: str, secrets: dict[str, str]) -> Path:
|
|
|
45
49
|
encrypted = encrypt_secret_material(payload, master_key=boot_key)
|
|
46
50
|
path = resolve_sealed_keys_path()
|
|
47
51
|
atomic_write_text(path, encrypted, mode=0o600)
|
|
52
|
+
remove_envelope_migration_backup(path)
|
|
48
53
|
clear_unseal_cache()
|
|
49
54
|
return path
|
|
50
55
|
|
|
@@ -79,28 +84,20 @@ def unseal_keys(boot_key: str) -> dict[str, str]:
|
|
|
79
84
|
for key, value in payload.items():
|
|
80
85
|
if isinstance(key, str) and isinstance(value, str):
|
|
81
86
|
secrets[key] = value
|
|
82
|
-
_maybe_migrate_envelope_kdf(boot_key,
|
|
87
|
+
_maybe_migrate_envelope_kdf(boot_key, plaintext, raw_text)
|
|
83
88
|
_unseal_cache.clear()
|
|
84
89
|
_unseal_cache[cache_key] = dict(secrets)
|
|
85
90
|
return secrets
|
|
86
91
|
|
|
87
92
|
|
|
88
|
-
def _maybe_migrate_envelope_kdf(boot_key: str,
|
|
89
|
-
"""
|
|
93
|
+
def _maybe_migrate_envelope_kdf(boot_key: str, plaintext: str, raw_text: str) -> None:
|
|
94
|
+
"""Opt-in re-seal of argon2id as HKDF with a verified rollback backup.
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
security while dropping ~1s of KDF per cold process. Rollback caveat:
|
|
94
|
-
pre-hkdf runtimes cannot read the rewritten file — kill switch is
|
|
95
|
-
AGENT_WALLET_ENVELOPE_KDF_MIGRATION=0.
|
|
96
|
+
Normal reads never migrate. Set AGENT_WALLET_ENVELOPE_KDF_MIGRATION=1 to
|
|
97
|
+
opt in; failures leave the readable original in place.
|
|
96
98
|
"""
|
|
97
99
|
from agent_wallet.config import envelope_kdf_migration_enabled
|
|
98
|
-
from agent_wallet.encrypted_storage import
|
|
99
|
-
KDF_ARGON2ID,
|
|
100
|
-
KDF_HKDF_SHA256,
|
|
101
|
-
_default_write_kdf,
|
|
102
|
-
envelope_kdf,
|
|
103
|
-
)
|
|
100
|
+
from agent_wallet.encrypted_storage import KDF_ARGON2ID, _default_write_kdf, envelope_kdf
|
|
104
101
|
|
|
105
102
|
try:
|
|
106
103
|
if not envelope_kdf_migration_enabled():
|
|
@@ -109,9 +106,13 @@ def _maybe_migrate_envelope_kdf(boot_key: str, secrets: dict[str, str], raw_text
|
|
|
109
106
|
return # forced-argon2id installs must not rewrite in place forever
|
|
110
107
|
if envelope_kdf(raw_text) != KDF_ARGON2ID:
|
|
111
108
|
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
from agent_wallet.encrypted_storage import migrate_envelope_to_hkdf
|
|
110
|
+
|
|
111
|
+
migrate_envelope_to_hkdf(
|
|
112
|
+
resolve_sealed_keys_path(),
|
|
113
|
+
plaintext,
|
|
114
|
+
master_key=boot_key,
|
|
115
|
+
)
|
|
115
116
|
clear_unseal_cache()
|
|
116
117
|
except Exception:
|
|
117
118
|
pass
|
|
@@ -183,12 +183,11 @@ def _maybe_migrate_wallet_envelope_kdf(
|
|
|
183
183
|
key_scope: str | None,
|
|
184
184
|
address: str,
|
|
185
185
|
) -> None:
|
|
186
|
-
"""
|
|
186
|
+
"""Opt-in rewrite of argon2id as HKDF with a verified rollback backup.
|
|
187
187
|
|
|
188
188
|
Re-encrypts with the SAME key scope that just decrypted the file, so this
|
|
189
|
-
never performs a scope migration through the back door.
|
|
190
|
-
|
|
191
|
-
AGENT_WALLET_ENVELOPE_KDF_MIGRATION=0.
|
|
189
|
+
never performs a scope migration through the back door. Normal reads do not
|
|
190
|
+
mutate storage; opt in with AGENT_WALLET_ENVELOPE_KDF_MIGRATION=1.
|
|
192
191
|
"""
|
|
193
192
|
from agent_wallet.config import envelope_kdf_migration_enabled
|
|
194
193
|
from agent_wallet.encrypted_storage import (
|
|
@@ -214,7 +213,9 @@ def _maybe_migrate_wallet_envelope_kdf(
|
|
|
214
213
|
master_key = resolve_wallet_master_key()
|
|
215
214
|
if not master_key.strip():
|
|
216
215
|
return
|
|
217
|
-
|
|
216
|
+
from agent_wallet.encrypted_storage import migrate_envelope_to_hkdf
|
|
217
|
+
|
|
218
|
+
migrate_envelope_to_hkdf(
|
|
218
219
|
path,
|
|
219
220
|
secret_material,
|
|
220
221
|
master_key=master_key,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.75",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -142,9 +142,11 @@ def _auto_start_local_service(
|
|
|
142
142
|
# steady state is a no-op. See agent_wallet.evm_user_wallets for the shared
|
|
143
143
|
# health/version/stop helpers.
|
|
144
144
|
from agent_wallet.evm_user_wallets import (
|
|
145
|
+
_expected_local_service_instance_id,
|
|
145
146
|
_service_health,
|
|
146
147
|
_should_restart_local_service,
|
|
147
148
|
_stop_local_service,
|
|
149
|
+
_write_service_owner,
|
|
148
150
|
)
|
|
149
151
|
|
|
150
152
|
restarted = False
|
|
@@ -152,7 +154,7 @@ def _auto_start_local_service(
|
|
|
152
154
|
if health is not None:
|
|
153
155
|
if not _should_restart_local_service(health, wallet_root=wdk_wallet_root):
|
|
154
156
|
return {"started": False, "already_healthy": True}
|
|
155
|
-
_stop_local_service(service_url)
|
|
157
|
+
_stop_local_service(service_url, health)
|
|
156
158
|
restarted = True
|
|
157
159
|
|
|
158
160
|
if not _is_local_service_url(service_url):
|
|
@@ -175,6 +177,7 @@ def _auto_start_local_service(
|
|
|
175
177
|
env["HOST"] = host
|
|
176
178
|
env["PORT"] = str(port)
|
|
177
179
|
env["WDK_EVM_NETWORK"] = network
|
|
180
|
+
env["WDK_EVM_INSTANCE_ID"] = _expected_local_service_instance_id()
|
|
178
181
|
|
|
179
182
|
with log_path.open("a", encoding="utf-8") as log_file:
|
|
180
183
|
process = subprocess.Popen( # noqa: S603
|
|
@@ -189,12 +192,14 @@ def _auto_start_local_service(
|
|
|
189
192
|
|
|
190
193
|
deadline = time.time() + 30.0
|
|
191
194
|
while time.time() < deadline:
|
|
192
|
-
|
|
195
|
+
health = _service_health(service_url)
|
|
196
|
+
if health is not None:
|
|
197
|
+
pid = _write_service_owner(health, service_url)
|
|
193
198
|
return {
|
|
194
199
|
"started": True,
|
|
195
200
|
"already_healthy": False,
|
|
196
201
|
"restarted": restarted,
|
|
197
|
-
"pid":
|
|
202
|
+
"pid": pid,
|
|
198
203
|
"log_path": str(log_path),
|
|
199
204
|
}
|
|
200
205
|
if process.poll() is not None:
|
|
@@ -25,6 +25,7 @@ INCLUDED_RUNTIME_ROOT_FILES = [
|
|
|
25
25
|
"setup.sh",
|
|
26
26
|
]
|
|
27
27
|
INCLUDED_RUNTIME_TOP_LEVEL_DIRS = [
|
|
28
|
+
"claude-code",
|
|
28
29
|
"codex",
|
|
29
30
|
".openclaw",
|
|
30
31
|
"agent-wallet",
|
|
@@ -862,6 +863,7 @@ def main() -> None:
|
|
|
862
863
|
source_wdk_btc_root = Path(args.wdk_btc_root).expanduser().resolve()
|
|
863
864
|
source_wdk_evm_root = Path(args.wdk_evm_root).expanduser().resolve()
|
|
864
865
|
runtime_root = Path(args.runtime_root).expanduser().resolve()
|
|
866
|
+
final_runtime_root = os.getenv("OPENCLAW_INSTALL_FINAL_ROOT", "").strip()
|
|
865
867
|
config_path = Path(args.config_path).expanduser()
|
|
866
868
|
env_path = Path(args.env_path).expanduser()
|
|
867
869
|
env_example_path = Path(args.env_example_path).expanduser()
|
|
@@ -1039,7 +1041,8 @@ def main() -> None:
|
|
|
1039
1041
|
"extension_path": str(extension_path),
|
|
1040
1042
|
"wdk_btc_root": str(wdk_btc_root),
|
|
1041
1043
|
"wdk_evm_root": str(wdk_evm_root),
|
|
1042
|
-
"runtime_root": str(runtime_root),
|
|
1044
|
+
"runtime_root": final_runtime_root or str(runtime_root),
|
|
1045
|
+
"staging_root": str(runtime_root) if final_runtime_root else None,
|
|
1043
1046
|
"install_from_runtime": bool(args.install_from_runtime),
|
|
1044
1047
|
"python_bin": str(python_bin),
|
|
1045
1048
|
"venv_created": venv_created,
|