@agentlayer.tech/wallet 0.1.58 → 0.1.64
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 +30 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/boot_key_migration.py +158 -0
- package/agent-wallet/agent_wallet/boot_key_recovery.py +46 -0
- package/agent-wallet/agent_wallet/config.py +21 -1
- package/agent-wallet/agent_wallet/keystore.py +303 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +20 -0
- package/agent-wallet/agent_wallet/openclaw_runtime.py +20 -0
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/bin/openclaw-agent-wallet.mjs +73 -4
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.64",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"agentlayer_autonomous_approve",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.1.62 - 2026-07-04
|
|
6
|
+
|
|
7
|
+
- Made default boot-key storage prompt-free on macOS. The automatic keystore
|
|
8
|
+
selection no longer probes macOS Keychain because even a read/probe through
|
|
9
|
+
`/usr/bin/security` can open a GUI password dialog during install or session
|
|
10
|
+
startup. macOS Keychain remains available only through explicit opt-in with
|
|
11
|
+
`AGENT_WALLET_KEYSTORE_BACKEND=macos-keychain` (or `native`), while the
|
|
12
|
+
default path uses the local fallback without showing Keychain prompts.
|
|
13
|
+
- `agent-wallet/agent_wallet/keystore.py`
|
|
14
|
+
- `agent-wallet/tests/smoke_keystore.py`
|
|
15
|
+
|
|
16
|
+
## v0.1.61 - 2026-07-04
|
|
17
|
+
|
|
18
|
+
- Hardened boot-key installation and migration around desktop keystores:
|
|
19
|
+
- The npm installer provisions the boot key through the runtime Python
|
|
20
|
+
keystore bridge instead of unconditionally persisting it in the release
|
|
21
|
+
`.env`, while keeping a legacy `.env` fallback when no non-interactive
|
|
22
|
+
keystore round-trip is available.
|
|
23
|
+
- The Node keystore bridge is bounded by a timeout so a hung Python/keychain
|
|
24
|
+
operation cannot stall install or reinstall.
|
|
25
|
+
- macOS keychain writes now run with non-interactive stdin, convert timeouts
|
|
26
|
+
into failed command results, and make the problematic
|
|
27
|
+
`set-generic-password-partition-list` ACL update strictly best-effort.
|
|
28
|
+
- Keystore resolution now verifies that a native backend can read the live
|
|
29
|
+
boot key or complete a write/read/delete probe before selecting it, falling
|
|
30
|
+
back cleanly when the OS tool exists but cannot authorize writes.
|
|
31
|
+
- `agent-wallet/agent_wallet/keystore.py`
|
|
32
|
+
- `bin/openclaw-agent-wallet.mjs`
|
|
33
|
+
- `agent-wallet/tests/smoke_keystore.py`
|
|
34
|
+
|
|
5
35
|
## v0.1.58 - 2026-07-01
|
|
6
36
|
|
|
7
37
|
- Resolved SPL token symbol/name in `get_wallet_portfolio` via a batched,
|
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ The CLI uses a versioned runtime layout:
|
|
|
102
102
|
~/.openclaw/agent-wallet-runtime/current # symlink → active release
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
On first install, `--yes` generates local runtime secrets. The installer stores `master_key` and `approval_secret` in `~/.openclaw/sealed_keys.json`;
|
|
105
|
+
On first install, `--yes` generates local runtime secrets. The installer stores `master_key` and `approval_secret` in `~/.openclaw/sealed_keys.json`; the boot key needed to unlock that sealed bundle stays in the prompt-free local fallback by default. macOS Keychain is opt-in (`AGENT_WALLET_KEYSTORE_BACKEND=macos-keychain`) because probing it can open a system password dialog during install or startup.
|
|
106
106
|
|
|
107
107
|
Useful CLI commands (require the global install above):
|
|
108
108
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.64
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Transparent, verified migration of the boot key into the OS keystore.
|
|
2
|
+
|
|
3
|
+
Runs at backend startup. Non-destructive: plaintext is swept ONLY after the
|
|
4
|
+
keystore read-back verifies. Idempotent and best-effort — a partial sweep
|
|
5
|
+
re-runs cleanly next start. See BOOT_KEY_KEYCHAIN_ARCHITECTURE.md.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from agent_wallet.config import (
|
|
14
|
+
read_boot_key_from_keystore,
|
|
15
|
+
resolve_openclaw_home,
|
|
16
|
+
settings,
|
|
17
|
+
)
|
|
18
|
+
from agent_wallet.file_ops import atomic_write_text
|
|
19
|
+
from agent_wallet.keystore import BOOT_KEY_ITEM, PlaintextFileStore, resolve_keystore
|
|
20
|
+
|
|
21
|
+
_ENV_LINE_PREFIX = "AGENT_WALLET_BOOT_KEY="
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read_legacy_boot_key() -> str:
|
|
25
|
+
"""The authoritative live key from legacy sources only (env override -> file)."""
|
|
26
|
+
direct = os.getenv("AGENT_WALLET_BOOT_KEY", settings.agent_wallet_boot_key).strip()
|
|
27
|
+
if direct:
|
|
28
|
+
return direct
|
|
29
|
+
key_file = os.getenv("AGENT_WALLET_BOOT_KEY_FILE", settings.agent_wallet_boot_key_file).strip()
|
|
30
|
+
if key_file:
|
|
31
|
+
try:
|
|
32
|
+
return Path(key_file).expanduser().read_text(encoding="utf-8").strip()
|
|
33
|
+
except OSError:
|
|
34
|
+
return ""
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _env_boot_key_value(line: str) -> str | None:
|
|
39
|
+
"""Return the boot-key value on an AGENT_WALLET_BOOT_KEY= line, else None."""
|
|
40
|
+
stripped = line.strip()
|
|
41
|
+
if not stripped.startswith(_ENV_LINE_PREFIX):
|
|
42
|
+
return None
|
|
43
|
+
return stripped[len(_ENV_LINE_PREFIX):].strip().strip('"').strip("'")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _strip_boot_key_line(env_path: Path, expected: str) -> bool:
|
|
47
|
+
"""Remove only the AGENT_WALLET_BOOT_KEY line whose value == expected.
|
|
48
|
+
|
|
49
|
+
Preserve all other vars, and never strip a line carrying a *different* value
|
|
50
|
+
(that would signal a key mismatch a human should look at). Returns True if
|
|
51
|
+
the file changed.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
lines = env_path.read_text(encoding="utf-8").splitlines()
|
|
55
|
+
except OSError:
|
|
56
|
+
return False
|
|
57
|
+
kept = [ln for ln in lines if _env_boot_key_value(ln) != expected]
|
|
58
|
+
if len(kept) == len(lines):
|
|
59
|
+
return False
|
|
60
|
+
atomic_write_text(env_path, ("\n".join(kept) + "\n") if kept else "", mode=0o600)
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _candidate_env_files(runtime: Path) -> list[Path]:
|
|
65
|
+
"""Boot-key .env locations to sweep, deduped.
|
|
66
|
+
|
|
67
|
+
Deliberately bounded: a recursive ``**/.env`` walk descends into the hundreds
|
|
68
|
+
of ``node_modules`` trees under the release dirs and effectively hangs onboarding
|
|
69
|
+
(observed: 650+ node_modules, glob never returns). The boot key was only ever
|
|
70
|
+
written to ``releases/<v>/agent-wallet/.env``; ``current``/``previous`` are
|
|
71
|
+
symlinks into ``releases`` and are resolved to dedupe against their targets.
|
|
72
|
+
"""
|
|
73
|
+
candidates = list(runtime.glob("releases/*/agent-wallet/.env"))
|
|
74
|
+
candidates.append(runtime / "current" / "agent-wallet" / ".env")
|
|
75
|
+
candidates.append(runtime / "previous" / "agent-wallet" / ".env")
|
|
76
|
+
seen: set[str] = set()
|
|
77
|
+
out: list[Path] = []
|
|
78
|
+
for path in candidates:
|
|
79
|
+
try:
|
|
80
|
+
key = str(path.resolve())
|
|
81
|
+
except OSError:
|
|
82
|
+
key = str(path)
|
|
83
|
+
if key in seen:
|
|
84
|
+
continue
|
|
85
|
+
seen.add(key)
|
|
86
|
+
if path.exists():
|
|
87
|
+
out.append(path)
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _sweep_plaintext(home: Path, expected: str) -> tuple[int, bool]:
|
|
92
|
+
"""Strip matching boot-key lines from every runtime .env; delete the boot-key file.
|
|
93
|
+
|
|
94
|
+
Idempotent and value-scoped: only plaintext equal to the authoritative
|
|
95
|
+
keystore key is removed, so re-running after a fresh installer write (which
|
|
96
|
+
re-emits the same key) cleans it up on the next start.
|
|
97
|
+
"""
|
|
98
|
+
runtime = home / "agent-wallet-runtime"
|
|
99
|
+
swept = 0
|
|
100
|
+
for env_path in _candidate_env_files(runtime):
|
|
101
|
+
try:
|
|
102
|
+
if _strip_boot_key_line(env_path, expected):
|
|
103
|
+
swept += 1
|
|
104
|
+
except Exception:
|
|
105
|
+
continue # best-effort; re-runs next start
|
|
106
|
+
removed = False
|
|
107
|
+
boot_file = runtime / "boot-key"
|
|
108
|
+
try:
|
|
109
|
+
if boot_file.read_text(encoding="utf-8").strip() == expected:
|
|
110
|
+
boot_file.unlink()
|
|
111
|
+
removed = True
|
|
112
|
+
except FileNotFoundError:
|
|
113
|
+
pass
|
|
114
|
+
except OSError:
|
|
115
|
+
pass
|
|
116
|
+
return swept, removed
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def migrate_boot_key_to_keystore() -> dict:
|
|
120
|
+
"""Move a legacy plaintext boot key into the keystore, verify, then sweep plaintext.
|
|
121
|
+
|
|
122
|
+
Runs on every startup (guarded once per process). When the keystore already
|
|
123
|
+
holds the key, it still re-sweeps any plaintext an installer re-emitted, so a
|
|
124
|
+
per-release .env write never re-establishes a permanent leak.
|
|
125
|
+
"""
|
|
126
|
+
store = resolve_keystore()
|
|
127
|
+
home = resolve_openclaw_home()
|
|
128
|
+
|
|
129
|
+
# Do not sweep into a plaintext fallback: that offers no at-rest improvement,
|
|
130
|
+
# and the user explicitly chose to stay on the current file in that case.
|
|
131
|
+
if isinstance(store, PlaintextFileStore):
|
|
132
|
+
return {"migrated": False, "backend": store.backend_id, "swept_env_files": 0,
|
|
133
|
+
"removed_boot_key_file": False, "reason": "no-os-keystore"}
|
|
134
|
+
|
|
135
|
+
authoritative = read_boot_key_from_keystore()
|
|
136
|
+
first_time = False
|
|
137
|
+
if not authoritative:
|
|
138
|
+
legacy_key = _read_legacy_boot_key()
|
|
139
|
+
if not legacy_key:
|
|
140
|
+
return {"migrated": False, "backend": store.backend_id, "swept_env_files": 0,
|
|
141
|
+
"removed_boot_key_file": False, "reason": "no-legacy-key"}
|
|
142
|
+
try:
|
|
143
|
+
store.set(BOOT_KEY_ITEM, legacy_key)
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
return {"migrated": False, "backend": store.backend_id, "swept_env_files": 0,
|
|
146
|
+
"removed_boot_key_file": False, "reason": f"keystore-set-failed: {exc}"}
|
|
147
|
+
# Verify-before-delete.
|
|
148
|
+
if read_boot_key_from_keystore() != legacy_key:
|
|
149
|
+
return {"migrated": False, "backend": store.backend_id, "swept_env_files": 0,
|
|
150
|
+
"removed_boot_key_file": False, "reason": "verify-failed"}
|
|
151
|
+
authoritative = legacy_key
|
|
152
|
+
first_time = True
|
|
153
|
+
|
|
154
|
+
swept, removed = _sweep_plaintext(home, authoritative)
|
|
155
|
+
migrated = first_time or swept > 0 or removed
|
|
156
|
+
return {"migrated": migrated, "backend": store.backend_id, "swept_env_files": swept,
|
|
157
|
+
"removed_boot_key_file": removed,
|
|
158
|
+
"reason": "ok" if migrated else "already-clean"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""User-facing boot-key export/import for paper backup and machine recovery.
|
|
2
|
+
|
|
3
|
+
The boot key is a retrievable string (baseline A), so recovery is simple: show
|
|
4
|
+
it for a paper backup, or write a recorded value back into the keystore on a new
|
|
5
|
+
machine. Full recovery kit = boot key + the already-encrypted sealed_keys.json +
|
|
6
|
+
wallet files. See BOOT_KEY_KEYCHAIN_ARCHITECTURE.md.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from agent_wallet.config import read_boot_key_from_keystore, resolve_boot_key
|
|
12
|
+
from agent_wallet.keystore import BOOT_KEY_ITEM, resolve_keystore
|
|
13
|
+
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
14
|
+
|
|
15
|
+
_EXPORT_WARNING = (
|
|
16
|
+
"SENSITIVE: anyone with this boot key AND your ~/.openclaw encrypted files "
|
|
17
|
+
"controls every wallet (Solana/EVM/BTC). Write it down and store it offline."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def export_boot_key() -> str:
|
|
22
|
+
"""Return the live boot key. Raises if none is resolvable."""
|
|
23
|
+
key = resolve_boot_key()
|
|
24
|
+
if not key:
|
|
25
|
+
raise WalletBackendError(
|
|
26
|
+
"No boot key is available to export. The wallet is not set up on this machine."
|
|
27
|
+
)
|
|
28
|
+
return key
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def import_boot_key(value: str) -> dict:
|
|
32
|
+
"""Write a recorded boot key into the OS keystore and verify the read-back."""
|
|
33
|
+
key = str(value or "").strip()
|
|
34
|
+
if not key:
|
|
35
|
+
raise WalletBackendError("A non-empty boot key is required for import.")
|
|
36
|
+
store = resolve_keystore()
|
|
37
|
+
store.set(BOOT_KEY_ITEM, key)
|
|
38
|
+
if read_boot_key_from_keystore() != key:
|
|
39
|
+
raise WalletBackendError(
|
|
40
|
+
f"Boot key import could not be verified from the {store.backend_id} keystore."
|
|
41
|
+
)
|
|
42
|
+
return {"imported": True, "backend": store.backend_id}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def export_warning() -> str:
|
|
46
|
+
return _EXPORT_WARNING
|
|
@@ -385,11 +385,31 @@ def _env_bool(name: str, default: bool) -> bool:
|
|
|
385
385
|
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
386
386
|
|
|
387
387
|
|
|
388
|
+
def read_boot_key_from_keystore() -> str:
|
|
389
|
+
"""Read the boot key from the OS keystore. Never raises; '' on any failure."""
|
|
390
|
+
try:
|
|
391
|
+
from agent_wallet.keystore import BOOT_KEY_ITEM, resolve_keystore
|
|
392
|
+
|
|
393
|
+
value = resolve_keystore().get(BOOT_KEY_ITEM)
|
|
394
|
+
return value.strip() if isinstance(value, str) else ""
|
|
395
|
+
except Exception:
|
|
396
|
+
return ""
|
|
397
|
+
|
|
398
|
+
|
|
388
399
|
def resolve_boot_key() -> str:
|
|
389
|
-
"""Resolve the boot key used to unlock sealed secrets from disk.
|
|
400
|
+
"""Resolve the boot key used to unlock sealed secrets from disk.
|
|
401
|
+
|
|
402
|
+
Precedence (superset of the legacy lookup, so existing installs keep working):
|
|
403
|
+
1. AGENT_WALLET_BOOT_KEY env / settings override
|
|
404
|
+
2. OS keystore (the hardened path)
|
|
405
|
+
3. AGENT_WALLET_BOOT_KEY_FILE / boot-key file (legacy)
|
|
406
|
+
"""
|
|
390
407
|
direct = os.getenv("AGENT_WALLET_BOOT_KEY", settings.agent_wallet_boot_key).strip()
|
|
391
408
|
if direct:
|
|
392
409
|
return direct
|
|
410
|
+
from_keystore = read_boot_key_from_keystore()
|
|
411
|
+
if from_keystore:
|
|
412
|
+
return from_keystore
|
|
393
413
|
key_file = os.getenv("AGENT_WALLET_BOOT_KEY_FILE", settings.agent_wallet_boot_key_file).strip()
|
|
394
414
|
if not key_file:
|
|
395
415
|
return ""
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Cross-platform, dependency-free secret storage for the wallet boot key.
|
|
2
|
+
|
|
3
|
+
Backends shell out to tools already present on each desktop OS:
|
|
4
|
+
- macOS: /usr/bin/security (generic password in the login keychain)
|
|
5
|
+
- Windows: powershell DPAPI (ConvertFrom/ConvertTo-SecureString), per-user+machine
|
|
6
|
+
- Linux: secret-tool (libsecret) iff a Secret Service is reachable
|
|
7
|
+
- fallback: a 0600 plaintext file (current behavior) — never chosen over a
|
|
8
|
+
working keystore.
|
|
9
|
+
|
|
10
|
+
The store holds ONE install-level secret (the boot key). See
|
|
11
|
+
BOOT_KEY_KEYCHAIN_ARCHITECTURE.md.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import platform
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Protocol, runtime_checkable
|
|
22
|
+
|
|
23
|
+
from agent_wallet.config import resolve_openclaw_home
|
|
24
|
+
from agent_wallet.file_ops import atomic_write_text, chmod_if_exists
|
|
25
|
+
|
|
26
|
+
KEYSTORE_SERVICE = "ai.agentlayer.wallet"
|
|
27
|
+
BOOT_KEY_ITEM = "boot_key"
|
|
28
|
+
_PROBE_ITEM = "__probe__"
|
|
29
|
+
_KEYSTORE_BACKEND_ENV = "AGENT_WALLET_KEYSTORE_BACKEND"
|
|
30
|
+
|
|
31
|
+
_SECURITY_BIN = "/usr/bin/security"
|
|
32
|
+
_SUBPROCESS_TIMEOUT = 10.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _service() -> str:
|
|
36
|
+
"""Keychain/Secret-Service service name. Overridable so tests never touch the
|
|
37
|
+
real shared slot (the OS keychain is global, not scoped to OPENCLAW_HOME)."""
|
|
38
|
+
return os.getenv("AGENT_WALLET_KEYSTORE_SERVICE", "").strip() or KEYSTORE_SERVICE
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _backend_preference() -> str:
|
|
42
|
+
"""Selected keystore backend.
|
|
43
|
+
|
|
44
|
+
auto (default) prefers the native OS keystore (macOS Keychain / Windows DPAPI /
|
|
45
|
+
Linux Secret Service) and falls back to a 0600 plaintext file. macOS Keychain
|
|
46
|
+
is prompt-free because set() writes with `-A` (open ACL, no partition-list).
|
|
47
|
+
Override with AGENT_WALLET_KEYSTORE_BACKEND=plaintext|macos-keychain|native|...
|
|
48
|
+
"""
|
|
49
|
+
return os.getenv(_KEYSTORE_BACKEND_ENV, "auto").strip().lower()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class KeyStoreError(Exception):
|
|
53
|
+
"""Raised when a keystore backend operation fails unexpectedly."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@runtime_checkable
|
|
57
|
+
class KeyStore(Protocol):
|
|
58
|
+
backend_id: str
|
|
59
|
+
|
|
60
|
+
def available(self) -> bool: ...
|
|
61
|
+
def get(self, name: str) -> str | None: ...
|
|
62
|
+
def set(self, name: str, value: str) -> None: ...
|
|
63
|
+
def delete(self, name: str) -> None: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _timeout_output(value: str | bytes | None) -> str:
|
|
67
|
+
if value is None:
|
|
68
|
+
return ""
|
|
69
|
+
if isinstance(value, bytes):
|
|
70
|
+
return value.decode(errors="replace")
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _run(
|
|
75
|
+
argv: list[str],
|
|
76
|
+
*,
|
|
77
|
+
input_text: str | None = None,
|
|
78
|
+
timeout: float = _SUBPROCESS_TIMEOUT,
|
|
79
|
+
) -> subprocess.CompletedProcess[str]:
|
|
80
|
+
kwargs: dict[str, object] = {
|
|
81
|
+
"capture_output": True,
|
|
82
|
+
"text": True,
|
|
83
|
+
"timeout": timeout,
|
|
84
|
+
"check": False,
|
|
85
|
+
}
|
|
86
|
+
if input_text is None:
|
|
87
|
+
kwargs["stdin"] = subprocess.DEVNULL
|
|
88
|
+
else:
|
|
89
|
+
kwargs["input"] = input_text
|
|
90
|
+
try:
|
|
91
|
+
return subprocess.run(argv, **kwargs)
|
|
92
|
+
except subprocess.TimeoutExpired as exc:
|
|
93
|
+
return subprocess.CompletedProcess(
|
|
94
|
+
argv,
|
|
95
|
+
124,
|
|
96
|
+
_timeout_output(exc.stdout),
|
|
97
|
+
_timeout_output(exc.stderr) or f"timed out after {timeout:g}s",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class MacKeychainStore:
|
|
102
|
+
backend_id = "macos-keychain"
|
|
103
|
+
|
|
104
|
+
def available(self) -> bool:
|
|
105
|
+
return platform.system() == "Darwin" and Path(_SECURITY_BIN).exists()
|
|
106
|
+
|
|
107
|
+
def get(self, name: str) -> str | None:
|
|
108
|
+
proc = _run([_SECURITY_BIN, "find-generic-password", "-s", _service(), "-a", name, "-w"])
|
|
109
|
+
if proc.returncode != 0:
|
|
110
|
+
return None # item not found (44) or other non-fatal lookup miss
|
|
111
|
+
value = proc.stdout.rstrip("\n")
|
|
112
|
+
return value or None
|
|
113
|
+
|
|
114
|
+
def set(self, name: str, value: str) -> None:
|
|
115
|
+
# Recreate the item fresh with an open ACL. This is the ONLY reliably
|
|
116
|
+
# prompt-free way to write a login-keychain item non-interactively:
|
|
117
|
+
# -A -> accessible to any application without a prompt, and it
|
|
118
|
+
# bypasses the Sierra+ partition-list mechanism entirely.
|
|
119
|
+
# delete+add -> never -U-update an existing item; updating one created
|
|
120
|
+
# with a different ACL (or setting its partition list)
|
|
121
|
+
# pops a GUI keychain-password dialog in the background.
|
|
122
|
+
# Trade-off: any process running as this user can read the key without a
|
|
123
|
+
# prompt — the same runtime exposure as a 0600 file. At-rest protection
|
|
124
|
+
# (backups, synced home dirs, a stolen disk) is fully preserved.
|
|
125
|
+
_run([_SECURITY_BIN, "delete-generic-password", "-s", _service(), "-a", name])
|
|
126
|
+
proc = _run([
|
|
127
|
+
_SECURITY_BIN, "add-generic-password",
|
|
128
|
+
"-s", _service(), "-a", name,
|
|
129
|
+
"-w", value, "-A",
|
|
130
|
+
])
|
|
131
|
+
if proc.returncode != 0:
|
|
132
|
+
raise KeyStoreError(f"security add-generic-password failed: {proc.stderr.strip()}")
|
|
133
|
+
|
|
134
|
+
def delete(self, name: str) -> None:
|
|
135
|
+
_run([_SECURITY_BIN, "delete-generic-password", "-s", _service(), "-a", name])
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class WindowsDpapiStore:
|
|
139
|
+
backend_id = "windows-dpapi"
|
|
140
|
+
|
|
141
|
+
def _blob_path(self, name: str) -> Path:
|
|
142
|
+
return resolve_openclaw_home() / "keystore" / f"{name}.dpapi"
|
|
143
|
+
|
|
144
|
+
def available(self) -> bool:
|
|
145
|
+
return platform.system() == "Windows" and shutil.which("powershell") is not None
|
|
146
|
+
|
|
147
|
+
def get(self, name: str) -> str | None:
|
|
148
|
+
path = self._blob_path(name)
|
|
149
|
+
if not path.exists():
|
|
150
|
+
return None
|
|
151
|
+
blob = path.read_text(encoding="utf-8").strip()
|
|
152
|
+
if not blob:
|
|
153
|
+
return None
|
|
154
|
+
script = (
|
|
155
|
+
"$ErrorActionPreference='Stop';"
|
|
156
|
+
"$b=[Console]::In.ReadToEnd().Trim();"
|
|
157
|
+
"$ss=ConvertTo-SecureString $b;"
|
|
158
|
+
"$p=[Runtime.InteropServices.Marshal]::PtrToStringUni("
|
|
159
|
+
"[Runtime.InteropServices.Marshal]::SecureStringToBSTR($ss));"
|
|
160
|
+
"[Console]::Out.Write($p)"
|
|
161
|
+
)
|
|
162
|
+
proc = _run(["powershell", "-NoProfile", "-NonInteractive", "-Command", script], input_text=blob)
|
|
163
|
+
if proc.returncode != 0:
|
|
164
|
+
return None
|
|
165
|
+
return proc.stdout or None
|
|
166
|
+
|
|
167
|
+
def set(self, name: str, value: str) -> None:
|
|
168
|
+
script = (
|
|
169
|
+
"$ErrorActionPreference='Stop';"
|
|
170
|
+
"$v=[Console]::In.ReadToEnd();"
|
|
171
|
+
"$ss=ConvertTo-SecureString $v -AsPlainText -Force;"
|
|
172
|
+
"[Console]::Out.Write((ConvertFrom-SecureString $ss))"
|
|
173
|
+
)
|
|
174
|
+
proc = _run(["powershell", "-NoProfile", "-NonInteractive", "-Command", script], input_text=value)
|
|
175
|
+
if proc.returncode != 0 or not proc.stdout.strip():
|
|
176
|
+
raise KeyStoreError(f"DPAPI ConvertFrom-SecureString failed: {proc.stderr.strip()}")
|
|
177
|
+
atomic_write_text(self._blob_path(name), proc.stdout.strip() + "\n", mode=0o600)
|
|
178
|
+
|
|
179
|
+
def delete(self, name: str) -> None:
|
|
180
|
+
path = self._blob_path(name)
|
|
181
|
+
try:
|
|
182
|
+
path.unlink()
|
|
183
|
+
except FileNotFoundError:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class LinuxSecretServiceStore:
|
|
188
|
+
backend_id = "linux-secretservice"
|
|
189
|
+
|
|
190
|
+
def available(self) -> bool:
|
|
191
|
+
if platform.system() != "Linux" or shutil.which("secret-tool") is None:
|
|
192
|
+
return False
|
|
193
|
+
# A probe lookup succeeds (rc 0/1) only when a Secret Service answers;
|
|
194
|
+
# a missing/unreachable service errors out (rc >1) or times out.
|
|
195
|
+
try:
|
|
196
|
+
proc = _run(["secret-tool", "lookup", "service", _service(), "account", "__probe__"])
|
|
197
|
+
except subprocess.TimeoutExpired:
|
|
198
|
+
return False
|
|
199
|
+
return proc.returncode in (0, 1)
|
|
200
|
+
|
|
201
|
+
def get(self, name: str) -> str | None:
|
|
202
|
+
proc = _run(["secret-tool", "lookup", "service", _service(), "account", name])
|
|
203
|
+
if proc.returncode != 0:
|
|
204
|
+
return None
|
|
205
|
+
value = proc.stdout.rstrip("\n")
|
|
206
|
+
return value or None
|
|
207
|
+
|
|
208
|
+
def set(self, name: str, value: str) -> None:
|
|
209
|
+
proc = _run(
|
|
210
|
+
["secret-tool", "store", "--label", f"{_service()} {name}",
|
|
211
|
+
"service", _service(), "account", name],
|
|
212
|
+
input_text=value,
|
|
213
|
+
)
|
|
214
|
+
if proc.returncode != 0:
|
|
215
|
+
raise KeyStoreError(f"secret-tool store failed: {proc.stderr.strip()}")
|
|
216
|
+
|
|
217
|
+
def delete(self, name: str) -> None:
|
|
218
|
+
_run(["secret-tool", "clear", "service", _service(), "account", name])
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class PlaintextFileStore:
|
|
222
|
+
"""Fallback: a 0600 file under OPENCLAW_HOME/keystore. Always available."""
|
|
223
|
+
|
|
224
|
+
backend_id = "plaintext-file"
|
|
225
|
+
|
|
226
|
+
def _path(self, name: str) -> Path:
|
|
227
|
+
return resolve_openclaw_home() / "keystore" / f"{name}.plaintext"
|
|
228
|
+
|
|
229
|
+
def available(self) -> bool:
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
def get(self, name: str) -> str | None:
|
|
233
|
+
path = self._path(name)
|
|
234
|
+
if not path.exists():
|
|
235
|
+
return None
|
|
236
|
+
chmod_if_exists(path)
|
|
237
|
+
value = path.read_text(encoding="utf-8").strip()
|
|
238
|
+
return value or None
|
|
239
|
+
|
|
240
|
+
def set(self, name: str, value: str) -> None:
|
|
241
|
+
atomic_write_text(self._path(name), value.strip() + "\n", mode=0o600)
|
|
242
|
+
|
|
243
|
+
def delete(self, name: str) -> None:
|
|
244
|
+
try:
|
|
245
|
+
self._path(name).unlink()
|
|
246
|
+
except FileNotFoundError:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def resolve_keystore() -> KeyStore:
|
|
251
|
+
"""Return the first available OS-native backend, else the plaintext fallback."""
|
|
252
|
+
preference = _backend_preference()
|
|
253
|
+
if preference in {"plain", "plaintext", "plaintext-file", "file"}:
|
|
254
|
+
return PlaintextFileStore()
|
|
255
|
+
|
|
256
|
+
candidates: list[KeyStore]
|
|
257
|
+
if preference in {"macos", "macos-keychain", "keychain"}:
|
|
258
|
+
candidates = [MacKeychainStore()]
|
|
259
|
+
elif preference in {"windows", "windows-dpapi", "dpapi"}:
|
|
260
|
+
candidates = [WindowsDpapiStore()]
|
|
261
|
+
elif preference in {"linux", "linux-secretservice", "secretservice"}:
|
|
262
|
+
candidates = [LinuxSecretServiceStore()]
|
|
263
|
+
elif preference == "native":
|
|
264
|
+
candidates = [MacKeychainStore(), WindowsDpapiStore(), LinuxSecretServiceStore()]
|
|
265
|
+
else:
|
|
266
|
+
# Auto: native OS keystore first, plaintext fallback last. macOS Keychain
|
|
267
|
+
# is prompt-free now that set() uses `-A` (open ACL, no partition-list) and
|
|
268
|
+
# _backend_usable falls back gracefully if a session still can't authorize.
|
|
269
|
+
candidates = [MacKeychainStore(), WindowsDpapiStore(), LinuxSecretServiceStore()]
|
|
270
|
+
|
|
271
|
+
for candidate in candidates:
|
|
272
|
+
try:
|
|
273
|
+
if candidate.available() and _backend_usable(candidate):
|
|
274
|
+
return candidate
|
|
275
|
+
except Exception:
|
|
276
|
+
continue
|
|
277
|
+
return PlaintextFileStore()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _backend_usable(candidate: KeyStore) -> bool:
|
|
281
|
+
"""Return true when a native backend can read the live key or write safely.
|
|
282
|
+
|
|
283
|
+
A desktop keystore binary can exist while the session cannot authorize
|
|
284
|
+
non-interactive writes. Treat that as unavailable so install/migration uses
|
|
285
|
+
the bounded fallback instead of hanging or repeatedly failing.
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
if candidate.get(BOOT_KEY_ITEM):
|
|
289
|
+
return True
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
probe_value = "ok"
|
|
294
|
+
try:
|
|
295
|
+
candidate.set(_PROBE_ITEM, probe_value)
|
|
296
|
+
return candidate.get(_PROBE_ITEM) == probe_value
|
|
297
|
+
except Exception:
|
|
298
|
+
return False
|
|
299
|
+
finally:
|
|
300
|
+
try:
|
|
301
|
+
candidate.delete(_PROBE_ITEM)
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
@@ -582,6 +582,15 @@ def main() -> int:
|
|
|
582
582
|
read_worker_parser.add_argument("--user-id", required=True)
|
|
583
583
|
read_worker_parser.add_argument("--config-json", default="{}")
|
|
584
584
|
|
|
585
|
+
subparsers.add_parser("boot-key-export")
|
|
586
|
+
|
|
587
|
+
boot_key_import_parser = subparsers.add_parser("boot-key-import")
|
|
588
|
+
boot_key_import_parser.add_argument(
|
|
589
|
+
"--key-stdin",
|
|
590
|
+
action="store_true",
|
|
591
|
+
help="Read the boot key to import from stdin.",
|
|
592
|
+
)
|
|
593
|
+
|
|
585
594
|
args = parser.parse_args()
|
|
586
595
|
|
|
587
596
|
try:
|
|
@@ -705,6 +714,17 @@ def main() -> int:
|
|
|
705
714
|
payload = asyncio.run(_run_evm_wallet_lock(args.user_id, config))
|
|
706
715
|
elif args.command == "read-worker":
|
|
707
716
|
return asyncio.run(_serve_read_worker(args.user_id, config))
|
|
717
|
+
elif args.command == "boot-key-export":
|
|
718
|
+
from agent_wallet.boot_key_recovery import export_boot_key, export_warning
|
|
719
|
+
|
|
720
|
+
print(export_warning(), file=sys.stderr)
|
|
721
|
+
payload = {"boot_key": export_boot_key()}
|
|
722
|
+
elif args.command == "boot-key-import":
|
|
723
|
+
if not args.key_stdin:
|
|
724
|
+
raise WalletBackendError("boot-key-import requires --key-stdin.")
|
|
725
|
+
from agent_wallet.boot_key_recovery import import_boot_key
|
|
726
|
+
|
|
727
|
+
payload = import_boot_key(_read_stdin_secret("boot key"))
|
|
708
728
|
else:
|
|
709
729
|
payload = asyncio.run(
|
|
710
730
|
_run_invoke(
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from agent_wallet.approval import issue_approval_token
|
|
9
|
+
from agent_wallet.boot_key_migration import migrate_boot_key_to_keystore
|
|
9
10
|
from agent_wallet.btc_user_wallets import get_user_btc_wallet_binding
|
|
10
11
|
from agent_wallet.config import normalize_btc_network, normalize_evm_network, settings
|
|
11
12
|
from agent_wallet.evm_user_wallets import ensure_user_evm_wallet_ready
|
|
@@ -19,6 +20,24 @@ from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendErro
|
|
|
19
20
|
from agent_wallet.wallet_layer.wdk_evm import WdkEvmLocalWalletBackend
|
|
20
21
|
from agent_wallet.wallet_layer.wdk_btc import WdkBtcLocalWalletBackend
|
|
21
22
|
|
|
23
|
+
_boot_key_migration_done = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ensure_boot_key_migrated_once() -> None:
|
|
27
|
+
"""Run the boot-key keystore migration at most once per process. Never raises.
|
|
28
|
+
|
|
29
|
+
Best-effort: a failure here must never block wallet onboarding — the legacy
|
|
30
|
+
plaintext sources still resolve via ``resolve_boot_key``.
|
|
31
|
+
"""
|
|
32
|
+
global _boot_key_migration_done
|
|
33
|
+
if _boot_key_migration_done:
|
|
34
|
+
return
|
|
35
|
+
_boot_key_migration_done = True
|
|
36
|
+
try:
|
|
37
|
+
migrate_boot_key_to_keystore()
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
22
41
|
|
|
23
42
|
@dataclass(slots=True)
|
|
24
43
|
class OpenClawWalletRuntimeContext:
|
|
@@ -98,6 +117,7 @@ def onboard_openclaw_user_wallet(
|
|
|
98
117
|
wdk_evm_account_index: int | None = None,
|
|
99
118
|
) -> OpenClawWalletRuntimeContext:
|
|
100
119
|
"""Provision and assemble a runtime-ready wallet context for one OpenClaw user."""
|
|
120
|
+
ensure_boot_key_migrated_once()
|
|
101
121
|
backend_name = str(backend or settings.agent_wallet_backend).strip().lower()
|
|
102
122
|
effective_sign_only = True if read_only else (
|
|
103
123
|
settings.agent_wallet_sign_only if sign_only is None else sign_only
|
|
@@ -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.64",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -23,6 +23,7 @@ const TELEMETRY_FLUSH_THROTTLE_SECONDS = 20;
|
|
|
23
23
|
const TELEMETRY_FORCE_LINES = 25;
|
|
24
24
|
const TELEMETRY_MAX_EVENTS_PER_FLUSH = 100;
|
|
25
25
|
const TELEMETRY_HTTP_TIMEOUT_MS = 1500;
|
|
26
|
+
const KEYSTORE_BRIDGE_TIMEOUT_MS = 30000;
|
|
26
27
|
|
|
27
28
|
function printHelp() {
|
|
28
29
|
console.log(`openclaw-agent-wallet
|
|
@@ -148,6 +149,11 @@ function telemetrySource(env = process.env) {
|
|
|
148
149
|
: "global_cli";
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
function positiveIntEnv(name, fallback, env = process.env) {
|
|
153
|
+
const parsed = Number.parseInt(String(env[name] || ""), 10);
|
|
154
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
155
|
+
}
|
|
156
|
+
|
|
151
157
|
function telemetryGatewayUrl(env = process.env) {
|
|
152
158
|
return String(env.PROVIDER_GATEWAY_URL || DEFAULT_PROVIDER_GATEWAY_URL).trim().replace(/\/+$/, "");
|
|
153
159
|
}
|
|
@@ -900,6 +906,60 @@ function ensureBootKeyFile(env = process.env) {
|
|
|
900
906
|
return { path: keyFile, status: "created" };
|
|
901
907
|
}
|
|
902
908
|
|
|
909
|
+
// Read the boot key from the OS keystore via the current runtime's Python
|
|
910
|
+
// (best-effort, "" on any failure). Lets a re-install after the runtime migration
|
|
911
|
+
// — which moves the key into the keystore and deletes every plaintext copy — still
|
|
912
|
+
// resolve the existing boot key instead of refusing to touch sealed secrets.
|
|
913
|
+
function readBootKeyFromKeystore(env = process.env) {
|
|
914
|
+
const runtimeRoot = resolvedCurrentRuntimeRoot(env);
|
|
915
|
+
if (!runtimeRoot) return "";
|
|
916
|
+
const py = resolveVenvPython(runtimeRoot);
|
|
917
|
+
if (!py) return "";
|
|
918
|
+
try {
|
|
919
|
+
const res = spawnSync(
|
|
920
|
+
py,
|
|
921
|
+
["-c", "from agent_wallet.config import read_boot_key_from_keystore as r; print(r())"],
|
|
922
|
+
{
|
|
923
|
+
cwd: path.join(runtimeRoot, "agent-wallet"),
|
|
924
|
+
encoding: "utf8",
|
|
925
|
+
timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
|
|
926
|
+
env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
|
|
927
|
+
},
|
|
928
|
+
);
|
|
929
|
+
if ((res.status ?? 1) !== 0) return "";
|
|
930
|
+
return String(res.stdout || "").trim();
|
|
931
|
+
} catch {
|
|
932
|
+
return "";
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Provision the boot key into the OS keystore via the freshly installed runtime's
|
|
937
|
+
// Python. Returns true only when the import stored AND verified the key, so the
|
|
938
|
+
// caller may safely drop the plaintext .env copy. Best-effort: false on any failure
|
|
939
|
+
// (e.g. no usable keystore), in which case the caller keeps the legacy .env write.
|
|
940
|
+
function provisionBootKeyToKeystore(releaseRoot, env, bootKey) {
|
|
941
|
+
const key = String(bootKey || "").trim();
|
|
942
|
+
if (!key) return false;
|
|
943
|
+
const py = resolveVenvPython(releaseRoot);
|
|
944
|
+
if (!py) return false;
|
|
945
|
+
try {
|
|
946
|
+
const res = spawnSync(
|
|
947
|
+
py,
|
|
948
|
+
["-m", "agent_wallet.openclaw_cli", "boot-key-import", "--key-stdin"],
|
|
949
|
+
{
|
|
950
|
+
cwd: path.join(releaseRoot, "agent-wallet"),
|
|
951
|
+
input: key,
|
|
952
|
+
encoding: "utf8",
|
|
953
|
+
timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
|
|
954
|
+
env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
|
|
955
|
+
},
|
|
956
|
+
);
|
|
957
|
+
return (res.status ?? 1) === 0;
|
|
958
|
+
} catch {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
903
963
|
function resolveVenvPython(releaseRoot) {
|
|
904
964
|
const candidates = [
|
|
905
965
|
path.join(releaseRoot, "agent-wallet", ".venv", "bin", "python"),
|
|
@@ -1169,7 +1229,8 @@ function buildInstallerEnv(args) {
|
|
|
1169
1229
|
const existingBootKey =
|
|
1170
1230
|
resolveBootKeyFromFile(env) ||
|
|
1171
1231
|
readTextIfExists(defaultBootKeyFile(env)).trim() ||
|
|
1172
|
-
currentBootKey(env)
|
|
1232
|
+
currentBootKey(env) ||
|
|
1233
|
+
readBootKeyFromKeystore(env);
|
|
1173
1234
|
if (existingBootKey) {
|
|
1174
1235
|
env.AGENT_WALLET_BOOT_KEY = existingBootKey;
|
|
1175
1236
|
}
|
|
@@ -1318,9 +1379,17 @@ function runInstall(args, { commandName = "install" } = {}) {
|
|
|
1318
1379
|
}
|
|
1319
1380
|
|
|
1320
1381
|
if (env.AGENT_WALLET_BOOT_KEY) {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1382
|
+
const envPath = path.join(releaseRoot, "agent-wallet", ".env");
|
|
1383
|
+
// Prefer the OS keystore: provision the boot key there and keep the plaintext
|
|
1384
|
+
// out of the release .env entirely. Only fall back to the legacy .env write when
|
|
1385
|
+
// no keystore round-trip is possible, so the runtime can still resolve the key.
|
|
1386
|
+
if (provisionBootKeyToKeystore(releaseRoot, env, env.AGENT_WALLET_BOOT_KEY)) {
|
|
1387
|
+
envFileUnset(envPath, ["AGENT_WALLET_BOOT_KEY"]);
|
|
1388
|
+
} else {
|
|
1389
|
+
envFileSet(envPath, {
|
|
1390
|
+
AGENT_WALLET_BOOT_KEY: env.AGENT_WALLET_BOOT_KEY,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1324
1393
|
}
|
|
1325
1394
|
|
|
1326
1395
|
const pythonInfo = activePythonRuntimeInfo(env);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-wallet",
|
|
3
3
|
"displayName": "Agent Wallet",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.64",
|
|
5
5
|
"description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AgentLayer"
|
package/package.json
CHANGED