@agentlayer.tech/wallet 0.1.89 → 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.
- package/.openclaw/extensions/agent-wallet/dist/index.js +5 -5
- package/.openclaw/extensions/agent-wallet/index.ts +5 -5
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +46 -23
- package/VERSION +1 -1
- package/agent-wallet/README.md +26 -1
- package/agent-wallet/UPGRADE_COMPATIBILITY.md +2 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/autonomous_permissions.py +43 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +604 -375
- package/agent-wallet/agent_wallet/providers/x402.py +84 -0
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_agent_wallet.py +197 -11
- package/agent-wallet/scripts/install_openclaw_local_config.py +4 -1
- package/agent-wallet/scripts/install_openclaw_sealed_keys.py +6 -2
- package/bin/lib/host-detection.mjs +236 -0
- package/bin/lib/integrations.mjs +69 -5
- package/bin/openclaw-agent-wallet.mjs +398 -28
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/README.md +3 -1
- package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-approve.md +7 -8
- package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-revoke.md +2 -2
- package/claude-code/plugins/agent-wallet/commands/wallet-base.md +38 -15
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/README.md +11 -6
- package/codex/plugins/agent-wallet/skills/wallet-base/SKILL.md +56 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +8 -3
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
|
@@ -1600,6 +1600,90 @@ def _reusable_approved_preview(
|
|
|
1600
1600
|
return dict(approved_preview)
|
|
1601
1601
|
|
|
1602
1602
|
|
|
1603
|
+
#: USD amount below which an x402 payment never requires host/session/
|
|
1604
|
+
#: permission approval, regardless of network (including mainnet) -- the
|
|
1605
|
+
#: same rationale as in-person card payments skipping a signature/PIN below
|
|
1606
|
+
#: a floor limit. Only ever applies when the payment asset is confidently
|
|
1607
|
+
#: identified as USDC (see _looks_like_usdc); any other asset always
|
|
1608
|
+
#: requires approval, since its USD value can't be determined here.
|
|
1609
|
+
DE_MINIMIS_USD_THRESHOLD = 2.0
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
def de_minimis_usd_amount(preview: dict[str, Any]) -> float | None:
|
|
1613
|
+
"""Return *preview*'s payment amount in USD, or None if it isn't confidently USDC.
|
|
1614
|
+
|
|
1615
|
+
``x402_amount_display`` is only populated by normalize_payment_requirement
|
|
1616
|
+
when the asset is recognized as USDC (see _looks_like_usdc) -- for any
|
|
1617
|
+
other asset it's None, since its USD value is unknown here.
|
|
1618
|
+
"""
|
|
1619
|
+
amount_display = preview.get("x402_amount_display")
|
|
1620
|
+
if not isinstance(amount_display, str) or not amount_display.strip():
|
|
1621
|
+
return None
|
|
1622
|
+
try:
|
|
1623
|
+
return float(amount_display)
|
|
1624
|
+
except ValueError:
|
|
1625
|
+
return None
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
def is_de_minimis_payment(
|
|
1629
|
+
preview: dict[str, Any],
|
|
1630
|
+
*,
|
|
1631
|
+
threshold_usd: float | None = None,
|
|
1632
|
+
) -> bool:
|
|
1633
|
+
"""Whether *preview*'s payment is small enough to skip approval entirely.
|
|
1634
|
+
|
|
1635
|
+
threshold_usd defaults to the current DE_MINIMIS_USD_THRESHOLD, read at
|
|
1636
|
+
call time (not bound at import time) so tests can monkeypatch the module
|
|
1637
|
+
attribute directly.
|
|
1638
|
+
"""
|
|
1639
|
+
usd_amount = de_minimis_usd_amount(preview)
|
|
1640
|
+
if usd_amount is None:
|
|
1641
|
+
return False
|
|
1642
|
+
effective_threshold = DE_MINIMIS_USD_THRESHOLD if threshold_usd is None else threshold_usd
|
|
1643
|
+
return usd_amount < effective_threshold
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
async def resolve_payment_preview(
|
|
1647
|
+
*,
|
|
1648
|
+
backend: AgentWalletBackend,
|
|
1649
|
+
url: str,
|
|
1650
|
+
method: str = "GET",
|
|
1651
|
+
headers: dict[str, Any] | None = None,
|
|
1652
|
+
query: dict[str, Any] | None = None,
|
|
1653
|
+
json_body: Any | None = None,
|
|
1654
|
+
text_body: str | None = None,
|
|
1655
|
+
approved_preview: dict[str, Any] | None = None,
|
|
1656
|
+
) -> dict[str, Any]:
|
|
1657
|
+
"""Return the payment preview for this exact request.
|
|
1658
|
+
|
|
1659
|
+
Reuses *approved_preview* when it still matches (same fingerprint check
|
|
1660
|
+
``pay_and_fetch`` applies before deciding whether to skip a fresh probe),
|
|
1661
|
+
otherwise makes a fresh unpaid probe. Callers that need a summary to bind
|
|
1662
|
+
an approval token to -- before paying -- can call this instead of
|
|
1663
|
+
duplicating the reuse-or-probe logic ``pay_and_fetch`` already has.
|
|
1664
|
+
"""
|
|
1665
|
+
request = _build_request_metadata(
|
|
1666
|
+
url=url,
|
|
1667
|
+
method=method,
|
|
1668
|
+
headers=headers,
|
|
1669
|
+
query=query,
|
|
1670
|
+
json_body=json_body,
|
|
1671
|
+
text_body=text_body,
|
|
1672
|
+
)
|
|
1673
|
+
reused = _reusable_approved_preview(approved_preview, request=request)
|
|
1674
|
+
if reused is not None:
|
|
1675
|
+
return reused
|
|
1676
|
+
return await preview_request(
|
|
1677
|
+
backend=backend,
|
|
1678
|
+
url=url,
|
|
1679
|
+
method=method,
|
|
1680
|
+
headers=headers,
|
|
1681
|
+
query=query,
|
|
1682
|
+
json_body=json_body,
|
|
1683
|
+
text_body=text_body,
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
|
|
1603
1687
|
async def pay_and_fetch(
|
|
1604
1688
|
*,
|
|
1605
1689
|
backend: AgentWalletBackend,
|
|
@@ -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.91",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -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
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const HOST_NAMES = Object.freeze([
|
|
6
|
+
"openclaw",
|
|
7
|
+
"codex",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"hermes",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const HOST_ALIASES = Object.freeze({
|
|
13
|
+
openclaw: "openclaw",
|
|
14
|
+
codex: "codex",
|
|
15
|
+
claude: "claude-code",
|
|
16
|
+
"claude-code": "claude-code",
|
|
17
|
+
claudecode: "claude-code",
|
|
18
|
+
hermes: "hermes",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function expandHome(value, env) {
|
|
22
|
+
const home = env.HOME || os.homedir();
|
|
23
|
+
if (value === "~") return home;
|
|
24
|
+
if (value.startsWith("~/")) return path.join(home, value.slice(2));
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function existingEvidence(candidates, exists = fs.existsSync) {
|
|
29
|
+
return candidates.filter((candidate) => {
|
|
30
|
+
try {
|
|
31
|
+
return exists(candidate);
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectedHost(name, displayName, binary, configCandidates, commandPath, exists) {
|
|
39
|
+
const binaryPath = commandPath(binary);
|
|
40
|
+
const configPaths = existingEvidence(configCandidates, exists);
|
|
41
|
+
const evidence = [
|
|
42
|
+
...(binaryPath ? [{ type: "cli", path: binaryPath }] : []),
|
|
43
|
+
...configPaths.map((configPath) => ({ type: "config", path: configPath })),
|
|
44
|
+
];
|
|
45
|
+
return {
|
|
46
|
+
name,
|
|
47
|
+
display_name: displayName,
|
|
48
|
+
detected: evidence.length > 0,
|
|
49
|
+
confidence: evidence.length > 0 ? "high" : "none",
|
|
50
|
+
evidence,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function detectHosts({
|
|
55
|
+
env = process.env,
|
|
56
|
+
commandPath,
|
|
57
|
+
exists = fs.existsSync,
|
|
58
|
+
} = {}) {
|
|
59
|
+
if (typeof commandPath !== "function") {
|
|
60
|
+
throw new TypeError("detectHosts requires commandPath(name)");
|
|
61
|
+
}
|
|
62
|
+
const home = env.HOME || os.homedir();
|
|
63
|
+
const openclawHome = path.resolve(expandHome(env.OPENCLAW_HOME || "~/.openclaw", env));
|
|
64
|
+
const codexHome = path.resolve(expandHome(env.CODEX_HOME || "~/.codex", env));
|
|
65
|
+
const hermesHome = path.resolve(expandHome(env.HERMES_HOME || "~/.hermes", env));
|
|
66
|
+
const claudeHome = path.resolve(expandHome(env.CLAUDE_CONFIG_DIR || "~/.claude", env));
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
detectedHost(
|
|
70
|
+
"openclaw",
|
|
71
|
+
"OpenClaw",
|
|
72
|
+
"openclaw",
|
|
73
|
+
[path.join(openclawHome, "openclaw.json")],
|
|
74
|
+
commandPath,
|
|
75
|
+
exists,
|
|
76
|
+
),
|
|
77
|
+
detectedHost(
|
|
78
|
+
"codex",
|
|
79
|
+
"Codex",
|
|
80
|
+
"codex",
|
|
81
|
+
[path.join(codexHome, "config.toml")],
|
|
82
|
+
commandPath,
|
|
83
|
+
exists,
|
|
84
|
+
),
|
|
85
|
+
detectedHost(
|
|
86
|
+
"claude-code",
|
|
87
|
+
"Claude Code",
|
|
88
|
+
"claude",
|
|
89
|
+
[
|
|
90
|
+
path.join(claudeHome, "settings.json"),
|
|
91
|
+
path.join(home, ".claude.json"),
|
|
92
|
+
],
|
|
93
|
+
commandPath,
|
|
94
|
+
exists,
|
|
95
|
+
),
|
|
96
|
+
detectedHost(
|
|
97
|
+
"hermes",
|
|
98
|
+
"Hermes",
|
|
99
|
+
"hermes",
|
|
100
|
+
[
|
|
101
|
+
path.join(hermesHome, "config.yaml"),
|
|
102
|
+
path.join(hermesHome, "config.yml"),
|
|
103
|
+
path.join(hermesHome, "settings.json"),
|
|
104
|
+
],
|
|
105
|
+
commandPath,
|
|
106
|
+
exists,
|
|
107
|
+
),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function flagValues(args, name) {
|
|
112
|
+
const values = [];
|
|
113
|
+
const prefix = `${name}=`;
|
|
114
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
115
|
+
const value = args[index];
|
|
116
|
+
if (value === name) {
|
|
117
|
+
const next = args[index + 1] || "";
|
|
118
|
+
if (!next || next.startsWith("--")) {
|
|
119
|
+
throw new Error(`${name} requires a value.`);
|
|
120
|
+
}
|
|
121
|
+
values.push(next);
|
|
122
|
+
index += 1;
|
|
123
|
+
} else if (value.startsWith(prefix)) {
|
|
124
|
+
values.push(value.slice(prefix.length));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return values;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeHostToken(value) {
|
|
131
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
132
|
+
return HOST_ALIASES[normalized] || normalized;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseHostSet(values, { detected, managed }) {
|
|
136
|
+
const selected = new Set();
|
|
137
|
+
for (const rawValue of values) {
|
|
138
|
+
for (const rawToken of String(rawValue).split(",")) {
|
|
139
|
+
const token = normalizeHostToken(rawToken);
|
|
140
|
+
if (!token) continue;
|
|
141
|
+
if (token === "none" || token === "runtime-only") {
|
|
142
|
+
selected.clear();
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (token === "all") {
|
|
146
|
+
HOST_NAMES.forEach((name) => selected.add(name));
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (token === "detected") {
|
|
150
|
+
detected.forEach((name) => selected.add(name));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (token === "managed") {
|
|
154
|
+
managed.forEach((name) => selected.add(name));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!HOST_NAMES.includes(token)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Unknown host '${rawToken}'. Expected: ${HOST_NAMES.join(", ")}, detected, managed, all, or none.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
selected.add(token);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return selected;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function buildInstallPlan({
|
|
169
|
+
args,
|
|
170
|
+
detections,
|
|
171
|
+
managedHosts = [],
|
|
172
|
+
runtimeInstalled,
|
|
173
|
+
}) {
|
|
174
|
+
const detected = detections.filter((entry) => entry.detected).map((entry) => entry.name);
|
|
175
|
+
const managed = managedHosts.filter((name) => HOST_NAMES.includes(name));
|
|
176
|
+
const hostValues = flagValues(args, "--hosts");
|
|
177
|
+
const explicitHosts = hostValues.length > 0;
|
|
178
|
+
const runtimeOnly = args.includes("--runtime-only");
|
|
179
|
+
const managedOnly = args.includes("--managed-only");
|
|
180
|
+
|
|
181
|
+
let selected;
|
|
182
|
+
let selectionReason;
|
|
183
|
+
if (runtimeOnly) {
|
|
184
|
+
selected = new Set();
|
|
185
|
+
selectionReason = "runtime_only";
|
|
186
|
+
} else if (managedOnly) {
|
|
187
|
+
selected = new Set(managed);
|
|
188
|
+
selectionReason = "managed_only";
|
|
189
|
+
} else if (explicitHosts) {
|
|
190
|
+
selected = parseHostSet(hostValues, { detected, managed });
|
|
191
|
+
selectionReason = "explicit";
|
|
192
|
+
} else if (runtimeInstalled) {
|
|
193
|
+
selected = new Set(managed);
|
|
194
|
+
selectionReason = "existing_runtime_managed_only";
|
|
195
|
+
} else {
|
|
196
|
+
selected = new Set(detected);
|
|
197
|
+
selectionReason = "fresh_install_detected";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const excluded = parseHostSet(flagValues(args, "--exclude"), { detected, managed });
|
|
201
|
+
excluded.forEach((name) => selected.delete(name));
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
schema_version: 1,
|
|
205
|
+
runtime_installed_before: Boolean(runtimeInstalled),
|
|
206
|
+
selection_reason: selectionReason,
|
|
207
|
+
explicit_hosts: explicitHosts,
|
|
208
|
+
detected_hosts: detected,
|
|
209
|
+
managed_hosts: managed,
|
|
210
|
+
selected_hosts: HOST_NAMES.filter((name) => selected.has(name)),
|
|
211
|
+
excluded_hosts: HOST_NAMES.filter((name) => excluded.has(name)),
|
|
212
|
+
detections,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function stripUniversalInstallerArgs(args) {
|
|
217
|
+
const output = [];
|
|
218
|
+
const valueFlags = new Set(["--hosts", "--exclude"]);
|
|
219
|
+
const booleanFlags = new Set([
|
|
220
|
+
"--runtime-only",
|
|
221
|
+
"--managed-only",
|
|
222
|
+
"--no-prompt",
|
|
223
|
+
"--json",
|
|
224
|
+
]);
|
|
225
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
226
|
+
const value = args[index];
|
|
227
|
+
if (booleanFlags.has(value)) continue;
|
|
228
|
+
if (valueFlags.has(value)) {
|
|
229
|
+
index += 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if ([...valueFlags].some((name) => value.startsWith(`${name}=`))) continue;
|
|
233
|
+
output.push(value);
|
|
234
|
+
}
|
|
235
|
+
return output;
|
|
236
|
+
}
|