@agentlayer.tech/wallet 0.1.90 → 0.1.91

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.
@@ -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.90",
5
+ "version": "0.1.91",
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.90",
3
+ "version": "0.1.91",
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,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Added a universal npm installer for OpenClaw, Codex, Claude Code, and Hermes.
6
+ `wallet install` now detects installed agent frameworks, supports explicit
7
+ `--hosts`, `--exclude`, and `--runtime-only` selection, and exposes
8
+ `wallet detect --json`. Runtime updates remain managed-only, so discovering a
9
+ new framework later does not silently enroll it. OpenClaw config creation is
10
+ now a host-specific step; wallet files and sealed secrets remain in their
11
+ existing `~/.openclaw` locations and are not replaced during updates.
12
+
13
+ - Confirmed the native OS keystore as the default boot-key backend. `auto`
14
+ prefers macOS Keychain, Windows DPAPI, or Linux Secret Service and uses the
15
+ local `0600` file only as a fallback. The npm installer now warns macOS users
16
+ that the system may request Keychain confirmation; this does not rotate or
17
+ migrate the boot key during ordinary updates.
18
+
5
19
  - Hardened Morpho EVM operations with a 30% gas-limit buffer calculated from
6
20
  the exact final calldata immediately before sending. Morpho vault and market
7
21
  writes now wait for their final on-chain receipt and report reverts instead
package/README.md CHANGED
@@ -6,23 +6,32 @@
6
6
  [![Node 24.x](https://img.shields.io/badge/node-24.x-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
7
7
  [![docs](https://img.shields.io/badge/docs-agent--layer.tech-blue)](https://docs.agent-layer.tech/)
8
8
 
9
- For Openclaw:
9
+ Install AgentLayer for the agent frameworks already present on the machine:
10
10
 
11
11
  ```bash
12
12
  npx @agentlayer.tech/wallet install --yes
13
13
  ```
14
14
 
15
- For Codex:
15
+ The installer detects OpenClaw, Codex, Claude Code, and Hermes, installs the
16
+ shared local runtime once, and connects every detected host. Without `--yes`,
17
+ an interactive terminal asks about each detected host.
18
+
19
+ To choose hosts explicitly:
16
20
 
17
21
  ```bash
18
- npx @agentlayer.tech/wallet install --yes && npx @agentlayer.tech/wallet codex install --yes
22
+ npx @agentlayer.tech/wallet install --yes --hosts codex,claude-code
23
+ npx @agentlayer.tech/wallet install --yes --hosts detected --exclude hermes
24
+ npx @agentlayer.tech/wallet install --yes --runtime-only
25
+ npx @agentlayer.tech/wallet detect --json
19
26
  ```
20
27
 
21
- For Claude Code:
28
+ The legacy `wallet codex install`, `wallet claude-code install`, and
29
+ `wallet hermes install` commands remain supported as explicit repair/install
30
+ commands.
22
31
 
23
- ```bash
24
- npx @agentlayer.tech/wallet install --yes && npx @agentlayer.tech/wallet claude-code install --yes
25
- ```
32
+ On a machine with an existing AgentLayer runtime, a plain `wallet install`
33
+ touches only already-managed hosts. To enroll a framework installed later, run
34
+ `wallet install --yes --hosts detected` or name it explicitly.
26
35
 
27
36
  Or install entirely from inside the Claude Code CLI, via the plugin marketplace
28
37
  (no terminal/npx needed) — two commands, then restart:
@@ -32,13 +41,6 @@ Or install entirely from inside the Claude Code CLI, via the plugin marketplace
32
41
  /plugin install agent-wallet@agentlayer
33
42
  ```
34
43
 
35
- For Hermes:
36
-
37
- ```bash
38
- npx @agentlayer.tech/wallet install --yes && npx @agentlayer.tech/wallet hermes install --yes
39
- ```
40
-
41
-
42
44
  AgentLayer is a beta local-first wallet and finance stack for agents.
43
45
 
44
46
  The repository includes:
@@ -72,7 +74,7 @@ System prerequisites:
72
74
  - `node` `24.x`
73
75
  - `npm`
74
76
 
75
- Install the local runtime:
77
+ Install the local runtime and connect detected agent frameworks:
76
78
 
77
79
  ```bash
78
80
  npx @agentlayer.tech/wallet install --yes
@@ -102,13 +104,26 @@ The CLI uses a versioned runtime layout:
102
104
  ~/.openclaw/agent-wallet-runtime/current # symlink → active release
103
105
  ```
104
106
 
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.
107
+ `~/.openclaw` remains the shared AgentLayer runtime root even when OpenClaw is
108
+ not installed. A Codex-only or Claude-only install creates the runtime there
109
+ but does not create or patch `openclaw.json`.
110
+
111
+ On first install, `--yes` generates local runtime secrets. The installer stores
112
+ `master_key` and `approval_secret` in `~/.openclaw/sealed_keys.json`. The boot
113
+ key needed to unlock that sealed bundle uses `AGENT_WALLET_KEYSTORE_BACKEND=auto`
114
+ by default: it prefers the native OS keystore and falls back to a local `0600`
115
+ file only when the native backend is unavailable. On macOS this means the login
116
+ Keychain (`service=ai.agentlayer.wallet`, `account=boot_key`). macOS may show a
117
+ Keychain access or password confirmation depending on the current Keychain
118
+ state; approve it only when you initiated the AgentLayer install or update.
119
+ Existing installs keep the same boot-key value during upgrades.
106
120
 
107
121
  Useful CLI commands (require the global install above):
108
122
 
109
123
  ```bash
110
124
  wallet status # show active runtime version and health
111
125
  wallet doctor # diagnose common config problems
126
+ wallet detect --json # show detected and already-managed hosts
112
127
  wallet update --yes # upgrade to the latest published version
113
128
  wallet update --yes --dry-run # preview the upgrade without applying it
114
129
  wallet rollback # revert to the previous release
@@ -116,7 +131,14 @@ wallet hermes install --yes # (re)connect Hermes to the current runtime
116
131
  wallet codex install --yes # (re)connect Codex to the current runtime
117
132
  ```
118
133
 
119
- `wallet update --yes` fetches the latest npm package, installs it under `releases/<version>`, and flips the `current` symlink. Python and Node dependency snapshots are reused when the dependency fingerprint has not changed, so subsequent updates are fast. All frameworks that read from `current/` (Claude Code, Codex, Hermes) pick up the new code automatically on their next session start; OpenClaw requires a gateway restart to reload its TypeScript extension.
134
+ `wallet update --yes` fetches the latest npm package, installs it under
135
+ `releases/<version>`, and flips the `current` symlink. Updates repair only hosts
136
+ already recorded as AgentLayer-managed; a newly detected framework is never
137
+ silently enrolled during update. Python and Node dependency snapshots are
138
+ reused when the dependency fingerprint has not changed, so subsequent updates
139
+ are fast. All frameworks that read from `current/` (Claude Code, Codex, Hermes)
140
+ pick up the new code automatically on their next session start; OpenClaw
141
+ requires a gateway restart to reload its TypeScript extension.
120
142
 
121
143
  ## Native OpenClaw plugin installs
122
144
 
@@ -280,10 +302,10 @@ For the default Solana flow, run the installer directly:
280
302
  npx @agentlayer.tech/wallet install --yes
281
303
  ```
282
304
 
283
- That installs the runtime, patches the OpenClaw plugin config, generates local
305
+ That installs the runtime, connects each detected framework, generates local
284
306
  runtime secrets when missing, and creates the first encrypted per-user Solana
285
- mainnet wallet. The agent receives the public address and guarded wallet tools,
286
- not the private key.
307
+ mainnet wallet. `openclaw.json` is patched only when OpenClaw is selected. The
308
+ agent receives the public address and guarded wallet tools, not the private key.
287
309
 
288
310
  BTC and EVM are separate host-side setup flows.
289
311
 
@@ -377,10 +399,11 @@ The installer then:
377
399
  - creates `agent-wallet/.env` from `agent-wallet/.env.example` if it does not exist
378
400
  - creates `agent-wallet/.runtime-venv` and installs the Python backend
379
401
  - installs Node dependencies for `wdk-btc-wallet`, `wdk-evm-wallet`, and `flash-sdk-bridge`
380
- - creates a minimal `~/.openclaw/openclaw.json` if one does not exist
381
402
  - if the required secrets are already present, writes or updates `~/.openclaw/sealed_keys.json`
382
- - if the required secrets are already present, patches `~/.openclaw/openclaw.json` to load the `agent-wallet` extension and point it at the installed runtime
383
- - `wallet hermes install --yes` additionally connects Hermes Agent to the same runtime without copying wallet tools or policy
403
+ - detects OpenClaw, Codex, Claude Code, and Hermes before changing host config
404
+ - connects the selected hosts to `agent-wallet-runtime/current`
405
+ - creates or patches `~/.openclaw/openclaw.json` only when OpenClaw is selected
406
+ - records non-secret ownership metadata so later updates repair only managed hosts
384
407
 
385
408
  When the installer reaches the final config step, the default plugin config is:
386
409
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.90
1
+ 0.1.91
@@ -138,7 +138,20 @@ If you want a simpler local setup flow, use the installer:
138
138
  sh ../setup.sh
139
139
  ```
140
140
 
141
- If you already use a local coding agent on the same machine, it can run this installer for you. You can simply ask the agent to install the wallet, and it can perform the file setup, Python setup, and OpenClaw config patching steps automatically.
141
+ The public npm entrypoint is a universal host orchestrator:
142
+
143
+ ```bash
144
+ npx @agentlayer.tech/wallet install --yes
145
+ npx @agentlayer.tech/wallet install --yes --hosts codex,claude-code
146
+ npx @agentlayer.tech/wallet detect --json
147
+ ```
148
+
149
+ It detects OpenClaw, Codex, Claude Code, and Hermes before installation. The
150
+ direct `setup.sh`/Python path retains its OpenClaw-oriented default for
151
+ backward compatibility; the npm CLI passes `--no-configure-openclaw` and lets
152
+ the selected OpenClaw host adapter own `openclaw.json`.
153
+
154
+ If you already use a local coding agent on the same machine, it can run this installer for you. You can simply ask the agent to install the wallet, and it can perform the file setup, Python setup, and selected host-bridge setup automatically.
142
155
 
143
156
  By default it will:
144
157
 
@@ -562,6 +575,18 @@ That native plugin package is additive. Keep the existing runtime installer for
562
575
  npx @agentlayer.tech/wallet install --yes
563
576
  ```
564
577
 
578
+ To bind a welcome invite from `https://www.agent-layer.tech/onboard` to the
579
+ local Base address during installation:
580
+
581
+ ```bash
582
+ npx @agentlayer.tech/wallet install --yes --invite alw_...
583
+ ```
584
+
585
+ The installer sends only the one-time invite and the public EVM address to the
586
+ AgentLayer onboarding API. It never sends a private key, seed, wallet password,
587
+ local user ID, or telemetry install ID. A network or EVM provisioning failure
588
+ does not consume the invite and does not roll back the wallet installation.
589
+
565
590
  The ClawHub plugin package auto-checks `~/.openclaw/agent-wallet-runtime/current/agent-wallet` before it falls back to a local workspace checkout.
566
591
 
567
592
  Public-safe helper scripts are available in `agent-wallet/scripts/`:
@@ -20,8 +20,9 @@ Raising the floor requires all of the following:
20
20
 
21
21
  | Path | Purpose | Removal condition |
22
22
  |---|---|---|
23
- | JavaScript boot-key fallback | Updates runtimes that predate the verified Python installer resolver. | The supported floor includes `resolve_boot_key_for_installer`, and direct-upgrade fixtures no longer require the fallback. |
23
+ | JavaScript boot-key fallback | Updates runtimes that predate the verified Python installer resolver. | The supported floor no longer includes releases without `resolve_boot_key_for_installer`; until then, isolated CI upgrades the published `0.1.53` package to the packed candidate and verifies boot-key, sealed-bundle, wallet-file, and address identity. |
24
24
  | Directory runtime-pointer migration | Converts pre-versioned `current/` directories into release entries. | The supported floor guarantees symlink-based runtime pointers. |
25
+ | OpenClaw legacy adoption | Recognizes an AgentLayer entry whose package or extension path belongs to the active runtime home. | The supported floor guarantees an `integrations.json` OpenClaw ownership entry. |
25
26
  | Hermes legacy adoption | Recognizes an AgentLayer plugin installed before the ownership registry. | The supported floor guarantees an `integrations.json` Hermes ownership entry. |
26
27
  | Codex legacy adoption | Recognizes a matching local marketplace registration and AgentLayer manifest. | The supported floor guarantees an `integrations.json` Codex ownership entry. |
27
28
  | Claude Code legacy adoption | Recognizes the AgentLayer marketplace and plugin manifest before registry ownership. | The supported floor guarantees an `integrations.json` Claude Code ownership entry. |
@@ -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.90"
5
+ __version__ = "0.1.91"
6
6
 
7
7
  __all__ = [
8
8
  "config",
@@ -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.90",
5
+ "version": "0.1.91",
6
6
  "skills": ["skills/wallet-operator"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.90"
7
+ version = "0.1.91"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -7,13 +7,20 @@ import hashlib
7
7
  import json
8
8
  import os
9
9
  import platform
10
+ import re
10
11
  import shutil
11
12
  import subprocess
12
13
  import sys
13
14
  import tempfile
15
+ import urllib.error
16
+ import urllib.request
14
17
  import venv
15
18
  from pathlib import Path
16
19
 
20
+ WELCOME_INVITE_PATTERN = re.compile(r"^alw_[A-Za-z0-9_-]{43}$")
21
+ DEFAULT_ONBOARDING_BIND_URL = "https://www.agent-layer.tech/api/onboarding/bind-wallet"
22
+ ONBOARDING_HTTP_TIMEOUT_SECONDS = 5.0
23
+
17
24
  INCLUDED_RUNTIME_ROOT_FILES = [
18
25
  ".env.example",
19
26
  "AGENTS.md",
@@ -192,9 +199,16 @@ def build_parser() -> argparse.ArgumentParser:
192
199
  parser.add_argument("--network", default="mainnet")
193
200
  parser.add_argument("--rpc-url", default="")
194
201
  parser.add_argument("--rpc-urls", default="")
202
+ parser.add_argument("--invite", default="")
195
203
  parser.add_argument("--sign-only", action=argparse.BooleanOptionalAction, default=False)
196
204
  parser.add_argument("--sync-runtime", action=argparse.BooleanOptionalAction, default=True)
197
205
  parser.add_argument("--install-from-runtime", action=argparse.BooleanOptionalAction, default=False)
206
+ parser.add_argument(
207
+ "--configure-openclaw",
208
+ action=argparse.BooleanOptionalAction,
209
+ default=True,
210
+ help="Patch openclaw.json during install (disabled by the universal host orchestrator).",
211
+ )
198
212
  parser.add_argument("--skip-python-setup", action=argparse.BooleanOptionalAction, default=False)
199
213
  parser.add_argument("--skip-node-setup", action=argparse.BooleanOptionalAction, default=False)
200
214
  parser.add_argument("--dry-run", action=argparse.BooleanOptionalAction, default=False)
@@ -856,6 +870,149 @@ def _bootstrap_evm_wallet(
856
870
  }
857
871
 
858
872
 
873
+ def _onboarding_bind_url() -> str:
874
+ return (
875
+ os.getenv("AGENTLAYER_ONBOARDING_BIND_URL", "").strip()
876
+ or DEFAULT_ONBOARDING_BIND_URL
877
+ )
878
+
879
+
880
+ def _onboarding_error_code(payload: object, fallback: str) -> str:
881
+ if not isinstance(payload, dict):
882
+ return fallback
883
+ code = str(payload.get("error") or "").strip().lower()
884
+ allowed = {
885
+ "address_already_used",
886
+ "internal_error",
887
+ "invalid_base_address",
888
+ "invalid_invite",
889
+ "invite_already_bound",
890
+ "invite_expired",
891
+ "invite_revoked",
892
+ "request_too_large",
893
+ }
894
+ return code if code in allowed else fallback
895
+
896
+
897
+ def _decode_onboarding_response(raw: bytes) -> object:
898
+ try:
899
+ return json.loads(raw.decode("utf-8"))
900
+ except (UnicodeDecodeError, json.JSONDecodeError):
901
+ return {}
902
+
903
+
904
+ def _bind_welcome_invite(
905
+ invite: str,
906
+ address: str,
907
+ *,
908
+ api_url: str | None = None,
909
+ timeout_seconds: float = ONBOARDING_HTTP_TIMEOUT_SECONDS,
910
+ opener: object = urllib.request.urlopen,
911
+ attempts: int = 2,
912
+ ) -> dict[str, object]:
913
+ normalized_invite = invite.strip()
914
+ normalized_address = address.strip()
915
+ if not WELCOME_INVITE_PATTERN.fullmatch(normalized_invite):
916
+ return {"ok": False, "status": "invalid_invite", "retryable": False}
917
+ if not re.fullmatch(r"0x[0-9a-fA-F]{40}", normalized_address):
918
+ return {"ok": False, "status": "invalid_base_address", "retryable": False}
919
+
920
+ body = json.dumps({"address": normalized_address}).encode("utf-8")
921
+ request = urllib.request.Request(
922
+ api_url or _onboarding_bind_url(),
923
+ data=body,
924
+ method="POST",
925
+ headers={
926
+ "Authorization": f"Bearer {normalized_invite}",
927
+ "Content-Type": "application/json",
928
+ "Accept": "application/json",
929
+ "User-Agent": "AgentLayer-Wallet-Installer",
930
+ },
931
+ )
932
+ max_attempts = max(1, min(int(attempts), 3))
933
+ for attempt in range(max_attempts):
934
+ try:
935
+ with opener(request, timeout=timeout_seconds) as response: # type: ignore[operator]
936
+ status_code = int(getattr(response, "status", 200))
937
+ payload = _decode_onboarding_response(response.read(65_536))
938
+ if status_code < 200 or status_code >= 300 or not isinstance(payload, dict):
939
+ if status_code >= 500 and attempt + 1 < max_attempts:
940
+ continue
941
+ return {
942
+ "ok": False,
943
+ "status": _onboarding_error_code(payload, "service_error"),
944
+ "retryable": status_code >= 500,
945
+ }
946
+ binding_status = str(payload.get("status") or "")
947
+ response_address = str(payload.get("address") or "")
948
+ if (
949
+ payload.get("ok") is not True
950
+ or binding_status not in {"bound", "already_bound"}
951
+ or response_address.lower() != normalized_address.lower()
952
+ ):
953
+ return {"ok": False, "status": "invalid_response", "retryable": True}
954
+ return {
955
+ "ok": True,
956
+ "status": binding_status,
957
+ "network": "base",
958
+ "address": response_address,
959
+ }
960
+ except urllib.error.HTTPError as exc:
961
+ payload = _decode_onboarding_response(exc.read(65_536))
962
+ if exc.code >= 500 and attempt + 1 < max_attempts:
963
+ continue
964
+ return {
965
+ "ok": False,
966
+ "status": _onboarding_error_code(payload, "service_error"),
967
+ "retryable": exc.code >= 500,
968
+ }
969
+ except (urllib.error.URLError, TimeoutError, OSError):
970
+ if attempt + 1 < max_attempts:
971
+ continue
972
+ return {"ok": False, "status": "network_error", "retryable": True}
973
+ return {"ok": False, "status": "network_error", "retryable": True}
974
+
975
+
976
+ def _bind_invite_after_evm_onboard(
977
+ invite: str,
978
+ evm_onboard_result: dict[str, object] | None,
979
+ *,
980
+ api_url: str | None = None,
981
+ opener: object = urllib.request.urlopen,
982
+ ) -> dict[str, object] | None:
983
+ if not invite.strip():
984
+ return None
985
+ if not isinstance(evm_onboard_result, dict) or not evm_onboard_result.get("ok"):
986
+ return {"ok": False, "status": "pending_evm_wallet", "retryable": True}
987
+ address = str(evm_onboard_result.get("address") or "").strip()
988
+ if not address:
989
+ return {"ok": False, "status": "pending_evm_wallet", "retryable": True}
990
+ return _bind_welcome_invite(
991
+ invite,
992
+ address,
993
+ api_url=api_url,
994
+ opener=opener,
995
+ )
996
+
997
+
998
+ def _invite_binding_warning(binding_result: dict[str, object]) -> str:
999
+ """Describe a failed invite bind without claiming it can always be retried."""
1000
+ status = str(binding_result.get("status") or "unknown")
1001
+ if status == "invite_already_bound":
1002
+ return (
1003
+ "warning: the welcome invite is already bound to a different Base "
1004
+ "wallet and cannot be used with this wallet. Status: "
1005
+ + status
1006
+ )
1007
+ if binding_result.get("retryable") is True:
1008
+ return (
1009
+ "warning: the welcome invite was not bound; the invite remains "
1010
+ "available for a safe retry. Status: "
1011
+ + status
1012
+ )
1013
+ return "warning: the welcome invite was not bound and cannot be retried. Status: " + status
1014
+
1015
+
859
1016
  def main() -> None:
860
1017
  args = build_parser().parse_args()
861
1018
  source_package_root = Path(args.package_root).expanduser().resolve()
@@ -909,6 +1066,7 @@ def main() -> None:
909
1066
  wdk_evm_root = source_wdk_evm_root
910
1067
 
911
1068
  install_config_script = package_root / "scripts" / "install_openclaw_local_config.py"
1069
+ install_sealed_keys_script = package_root / "scripts" / "install_openclaw_sealed_keys.py"
912
1070
  if args.install_from_runtime:
913
1071
  default_source_env_path = source_package_root / ".env"
914
1072
  default_source_venv_path = source_package_root / ".venv"
@@ -923,7 +1081,7 @@ def main() -> None:
923
1081
  env_created = _ensure_env_file(env_path, env_example_path)
924
1082
  boot_key_file_env_updated = _ensure_runtime_boot_key_file_env(env_path)
925
1083
  flash_bridge_env = _ensure_flash_bridge_env(env_path, package_root)
926
- config_created = _ensure_openclaw_config(config_path)
1084
+ config_created = _ensure_openclaw_config(config_path) if args.configure_openclaw else False
927
1085
 
928
1086
  python_bin = Path(sys.executable)
929
1087
  venv_created = False
@@ -988,23 +1146,33 @@ def main() -> None:
988
1146
  pending_env = _pending_env_names() if backend_enabled else []
989
1147
  configured = False
990
1148
  configure_stdout = ""
1149
+ sealed_keys_result: dict[str, object] | None = None
991
1150
  solana_onboard_result: dict[str, object] | None = None
992
1151
  evm_onboard_result: dict[str, object] | None = None
1152
+ invite_binding_result: dict[str, object] | None = None
993
1153
  if backend_enabled and not pending_env and not args.dry_run:
994
- result = subprocess.run(
995
- _build_next_steps(
996
- python_bin,
997
- install_config_script,
998
- args,
999
- package_root=package_root,
1000
- extension_path=extension_path,
1001
- ),
1154
+ sealed_result = subprocess.run(
1155
+ [str(python_bin), str(install_sealed_keys_script)],
1002
1156
  capture_output=True,
1003
1157
  text=True,
1004
1158
  check=True,
1005
1159
  )
1006
- configured = True
1007
- configure_stdout = result.stdout
1160
+ sealed_keys_result = json.loads(sealed_result.stdout)
1161
+ if args.configure_openclaw:
1162
+ result = subprocess.run(
1163
+ _build_next_steps(
1164
+ python_bin,
1165
+ install_config_script,
1166
+ args,
1167
+ package_root=package_root,
1168
+ extension_path=extension_path,
1169
+ ),
1170
+ capture_output=True,
1171
+ text=True,
1172
+ check=True,
1173
+ )
1174
+ configured = True
1175
+ configure_stdout = result.stdout
1008
1176
  solana_onboard_result = _bootstrap_solana_wallet(
1009
1177
  python_bin,
1010
1178
  package_root,
@@ -1027,6 +1195,21 @@ def main() -> None:
1027
1195
  file=sys.stderr,
1028
1196
  )
1029
1197
 
1198
+ if args.invite.strip():
1199
+ if args.dry_run:
1200
+ invite_binding_result = {
1201
+ "ok": False,
1202
+ "status": "skipped_dry_run",
1203
+ "retryable": True,
1204
+ }
1205
+ else:
1206
+ invite_binding_result = _bind_invite_after_evm_onboard(
1207
+ args.invite,
1208
+ evm_onboard_result,
1209
+ )
1210
+ if invite_binding_result and not invite_binding_result.get("ok"):
1211
+ print(_invite_binding_warning(invite_binding_result), file=sys.stderr)
1212
+
1030
1213
  print(
1031
1214
  json.dumps(
1032
1215
  {
@@ -1037,6 +1220,7 @@ def main() -> None:
1037
1220
  "flash_bridge_env": flash_bridge_env,
1038
1221
  "config_path": str(config_path),
1039
1222
  "config_created": config_created,
1223
+ "configure_openclaw": bool(args.configure_openclaw),
1040
1224
  "package_root": str(package_root),
1041
1225
  "extension_path": str(extension_path),
1042
1226
  "wdk_btc_root": str(wdk_btc_root),
@@ -1051,8 +1235,10 @@ def main() -> None:
1051
1235
  "runtime_sync": runtime_sync,
1052
1236
  "configured": configured,
1053
1237
  "pending_env": pending_env,
1238
+ "sealed_keys": sealed_keys_result,
1054
1239
  "solana_wallet": solana_onboard_result,
1055
1240
  "evm_wallet": evm_onboard_result,
1241
+ "invite_binding": invite_binding_result,
1056
1242
  "next_configure_command": _build_next_steps(
1057
1243
  python_bin,
1058
1244
  install_config_script,
@@ -271,7 +271,10 @@ def _maybe_install_sealed_keys() -> str | None:
271
271
  updates["wdk_evm_wallet_password"] = secrets.token_urlsafe(24)
272
272
  if not updates:
273
273
  return None
274
- return str(seal_keys(boot_key, {**existing, **updates}))
274
+ merged = {**existing, **updates}
275
+ if merged == existing:
276
+ return str(sealed_path)
277
+ return str(seal_keys(boot_key, merged))
275
278
 
276
279
 
277
280
  def _require_hardened_runtime_secrets(backend: str) -> str | None:
@@ -94,15 +94,19 @@ def main() -> None:
94
94
  "and/or SOLANA_AGENT_PRIVATE_KEY in the environment."
95
95
  )
96
96
 
97
- path = seal_keys(boot_key, secrets)
97
+ changed = not sealed_path.exists() or secrets != existing
98
+ path = seal_keys(boot_key, secrets) if changed else sealed_path
98
99
  print(
99
100
  json.dumps(
100
101
  {
101
102
  "ok": True,
102
103
  "path": str(path),
103
104
  "stored_keys": sorted(secrets.keys()),
104
- "updated_keys": sorted(set(updates.keys()) | set(generated_keys)),
105
+ "updated_keys": (
106
+ sorted(set(updates.keys()) | set(generated_keys)) if changed else []
107
+ ),
105
108
  "replaced": bool(args.replace),
109
+ "changed": changed,
106
110
  },
107
111
  indent=2,
108
112
  )