@agentlayer.tech/wallet 0.1.55 → 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.
Files changed (29) hide show
  1. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
  2. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  3. package/CHANGELOG.md +81 -0
  4. package/README.md +1 -1
  5. package/VERSION +1 -1
  6. package/agent-wallet/.env.example +1 -0
  7. package/agent-wallet/agent_wallet/__init__.py +1 -1
  8. package/agent-wallet/agent_wallet/boot_key_migration.py +158 -0
  9. package/agent-wallet/agent_wallet/boot_key_recovery.py +46 -0
  10. package/agent-wallet/agent_wallet/config.py +30 -1
  11. package/agent-wallet/agent_wallet/keystore.py +303 -0
  12. package/agent-wallet/agent_wallet/openclaw_cli.py +115 -39
  13. package/agent-wallet/agent_wallet/openclaw_runtime.py +28 -3
  14. package/agent-wallet/agent_wallet/providers/jupiter.py +34 -0
  15. package/agent-wallet/agent_wallet/user_wallets.py +46 -3
  16. package/agent-wallet/agent_wallet/wallet_layer/solana.py +34 -8
  17. package/agent-wallet/openclaw.plugin.json +1 -1
  18. package/agent-wallet/pyproject.toml +1 -1
  19. package/bin/openclaw-agent-wallet.mjs +73 -4
  20. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  21. package/claude-code/plugins/agent-wallet/commands/wallet-sol.md +9 -15
  22. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  23. package/codex/plugins/agent-wallet/README.md +9 -0
  24. package/codex/plugins/agent-wallet/server.py +353 -4
  25. package/codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md +29 -0
  26. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  27. package/package.json +1 -1
  28. package/wdk-btc-wallet/package.json +1 -1
  29. 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.55",
5
+ "version": "0.1.64",
6
6
  "contracts": {
7
7
  "tools": [
8
8
  "agentlayer_autonomous_approve",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayertech/agent-wallet-plugin",
3
- "version": "0.1.55",
3
+ "version": "0.1.64",
4
4
  "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN ../../../LICENSE",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,87 @@
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
+
35
+ ## v0.1.58 - 2026-07-01
36
+
37
+ - Resolved SPL token symbol/name in `get_wallet_portfolio` via a batched,
38
+ concurrent Jupiter token-search lookup, so `/wallet-sol` shows tickers
39
+ (e.g. `USDC`) instead of raw mint addresses. The mint/token_address is
40
+ still returned in full and rendered shortened next to the label rather
41
+ than dropped. Falls back gracefully (symbol=None) when a mint isn't
42
+ indexed or the lookup provider is unavailable.
43
+ - `agent-wallet/agent_wallet/providers/jupiter.py`
44
+ - `agent-wallet/agent_wallet/wallet_layer/solana.py`
45
+ - `claude-code/plugins/agent-wallet/commands/wallet-sol.md`
46
+ - `codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md`
47
+ - Dropped the `/wallet-sol` source-metadata footer line (`source`,
48
+ `token_discovery_source`, `pricing_source`, `pricing_errors`) from the
49
+ rendered chat output for a more compact report.
50
+ - `claude-code/plugins/agent-wallet/commands/wallet-sol.md`
51
+ - `codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md`
52
+
53
+ ## v0.1.57 - 2026-07-01
54
+
55
+ - Added a bundled Codex `wallet-sol` skill so Codex users can render the
56
+ connected Solana wallet portfolio in chat the same way Claude Code's
57
+ `/wallet-sol` command does.
58
+ - `codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md`
59
+ - `codex/plugins/agent-wallet/README.md`
60
+ - Fixed the resident read worker (used by `get_wallet_balance` /
61
+ `get_wallet_portfolio`, e.g. `/wallet-sol`) decrypting the wallet's private
62
+ key on every read-only cold start even though it never needed it. Read-only
63
+ onboarding now resolves the address from the existing plaintext wallet pin
64
+ file once a wallet has been provisioned, instead of unsealing secrets and
65
+ deriving a signer just to discard it.
66
+ - `agent-wallet/agent_wallet/user_wallets.py`
67
+ - Reduced `/wallet-sol` context payload by dropping the unused raw Jupiter
68
+ price blob (`price_raw`) from portfolio token entries.
69
+ - `agent-wallet/agent_wallet/wallet_layer/solana.py`
70
+ - Parallelized Jupiter price batch fetches in `get_portfolio` instead of
71
+ awaiting each 20-mint batch sequentially, speeding up portfolio lookups for
72
+ wallets with many SPL token accounts.
73
+ - `agent-wallet/agent_wallet/wallet_layer/solana.py`
74
+ - Added a background prewarm of the resident read worker at MCP server
75
+ startup so interpreter boot and onboarding overlap with the user issuing
76
+ the first read-only command instead of blocking it. Opt out with
77
+ `AGENT_WALLET_PREWARM_READ_WORKER=0`.
78
+ - `codex/plugins/agent-wallet/server.py`
79
+ - Added idle eviction for resident read workers left over from a stale
80
+ config (e.g. after switching Solana network or wallet backend), bounded by
81
+ `AGENT_WALLET_READ_WORKER_IDLE_SECONDS` (default 10 minutes), and closed
82
+ resident read workers on SIGTERM in addition to the existing `atexit`
83
+ cleanup, since Python does not run `atexit` hooks on signal termination.
84
+ - `codex/plugins/agent-wallet/server.py`
85
+
5
86
  ## v0.1.54 - 2026-06-29
6
87
 
7
88
  - Fixed Claude Code autonomous-mode slash command activation so
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`; only the boot key needed to unlock that sealed bundle is written to the runtime `.env`.
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.55
1
+ 0.1.64
@@ -43,6 +43,7 @@ SOLANA_AGENT_KEYPAIR_PATH=
43
43
  JUPITER_API_BASE_URL=https://lite-api.jup.ag/swap/v1
44
44
  JUPITER_ULTRA_API_BASE_URL=https://lite-api.jup.ag/ultra/v1
45
45
  JUPITER_PRICE_API_BASE_URL=https://lite-api.jup.ag/price/v3
46
+ JUPITER_TOKEN_SEARCH_API_BASE_URL=https://lite-api.jup.ag/tokens/v2
46
47
  JUPITER_API_KEY=
47
48
 
48
49
  # LI.FI cross-chain routing. API key is optional for basic read-only quote/status calls.
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Keep in sync with package.json, pyproject.toml, and the npm installer version.
4
4
  # scripts/check_release_version.mjs enforces this on release.
5
- __version__ = "0.1.55"
5
+ __version__ = "0.1.64"
6
6
 
7
7
  __all__ = [
8
8
  "config",
@@ -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
@@ -48,6 +48,7 @@ class Settings(BaseSettings):
48
48
  jupiter_swap_v2_api_base_url: str = "https://api.jup.ag/swap/v2"
49
49
  jupiter_ultra_api_base_url: str = "https://lite-api.jup.ag/ultra/v1"
50
50
  jupiter_price_api_base_url: str = "https://lite-api.jup.ag/price/v3"
51
+ jupiter_token_search_api_base_url: str = "https://lite-api.jup.ag/tokens/v2"
51
52
  jupiter_api_key: str = ""
52
53
  lifi_api_base_url: str = "https://li.quest/v1"
53
54
  lifi_api_key: str = ""
@@ -74,6 +75,14 @@ class Settings(BaseSettings):
74
75
  settings = Settings()
75
76
 
76
77
 
78
+ def reload_settings() -> Settings:
79
+ """Reload settings from the current environment without changing object identity."""
80
+ refreshed = Settings()
81
+ for field_name in Settings.model_fields:
82
+ setattr(settings, field_name, getattr(refreshed, field_name))
83
+ return settings
84
+
85
+
77
86
  def normalize_solana_network(network: str | None) -> str:
78
87
  """Canonicalize supported Solana network names and reject test clusters."""
79
88
  normalized = str(network or "").strip().lower() or "mainnet"
@@ -376,11 +385,31 @@ def _env_bool(name: str, default: bool) -> bool:
376
385
  return raw.strip().lower() in {"1", "true", "yes", "on"}
377
386
 
378
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
+
379
399
  def resolve_boot_key() -> str:
380
- """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
+ """
381
407
  direct = os.getenv("AGENT_WALLET_BOOT_KEY", settings.agent_wallet_boot_key).strip()
382
408
  if direct:
383
409
  return direct
410
+ from_keystore = read_boot_key_from_keystore()
411
+ if from_keystore:
412
+ return from_keystore
384
413
  key_file = os.getenv("AGENT_WALLET_BOOT_KEY_FILE", settings.agent_wallet_boot_key_file).strip()
385
414
  if not key_file:
386
415
  return ""