@agentlayer.tech/wallet 0.1.64 → 0.1.66
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/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/config.py +44 -2
- package/agent-wallet/agent_wallet/encrypted_storage.py +77 -9
- package/agent-wallet/agent_wallet/keystore.py +25 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +4 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +63 -4
- package/agent-wallet/agent_wallet/providers/x402.py +229 -148
- package/agent-wallet/agent_wallet/sealed_keys.py +58 -2
- package/agent-wallet/agent_wallet/user_wallets.py +93 -5
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/build_release_bundle.py +10 -2
- package/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/server.py +116 -47
- package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/install-from-github.sh +6 -0
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
|
@@ -52,13 +52,27 @@ def resolve_user_wallet_path(user_id: str, network: str | None = None) -> Path:
|
|
|
52
52
|
return user_dir / f"solana-{effective_network}-agent.json"
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def _user_wallet_metadata(
|
|
55
|
+
def _user_wallet_metadata(
|
|
56
|
+
user_id: str,
|
|
57
|
+
address: str,
|
|
58
|
+
network: str | None = None,
|
|
59
|
+
key_scope: str | None = None,
|
|
60
|
+
) -> dict[str, str]:
|
|
56
61
|
effective_network = normalize_solana_network(network or settings.solana_network)
|
|
57
|
-
|
|
62
|
+
metadata = {
|
|
58
63
|
"address": address,
|
|
59
64
|
"user_id": user_id,
|
|
60
65
|
"network": effective_network,
|
|
61
66
|
}
|
|
67
|
+
# Record which master-key scope encrypted this envelope so the loader can
|
|
68
|
+
# try the matching candidate first instead of paying a wasted KDF on the
|
|
69
|
+
# wrong one. Writes through _resolve_user_wallet_master_key follow
|
|
70
|
+
# use_per_user_key_derivation(), so that is the accurate default.
|
|
71
|
+
effective_scope = key_scope or (
|
|
72
|
+
"per-user-derived" if use_per_user_key_derivation() else "global-master"
|
|
73
|
+
)
|
|
74
|
+
metadata["key_scope"] = effective_scope
|
|
75
|
+
return metadata
|
|
62
76
|
|
|
63
77
|
|
|
64
78
|
def _resolve_effective_network(network: str | None = None) -> str:
|
|
@@ -126,12 +140,25 @@ def _load_user_wallet_secret_material(
|
|
|
126
140
|
if not is_encrypted_wallet_payload(raw_text):
|
|
127
141
|
return raw_text, "plaintext", None
|
|
128
142
|
|
|
129
|
-
|
|
130
|
-
for key_scope, candidate in _candidate_user_wallet_master_keys(
|
|
143
|
+
candidates = _candidate_user_wallet_master_keys(
|
|
131
144
|
user_id,
|
|
132
145
|
network,
|
|
133
146
|
raw_master_key=raw_master_key,
|
|
134
|
-
)
|
|
147
|
+
)
|
|
148
|
+
# Try the scope recorded at encryption time first: a mismatched candidate
|
|
149
|
+
# costs a full (failed) KDF before the right one is attempted. Files
|
|
150
|
+
# without the marker keep the legacy order.
|
|
151
|
+
try:
|
|
152
|
+
import json as _json
|
|
153
|
+
|
|
154
|
+
recorded_scope = (_json.loads(raw_text).get("metadata") or {}).get("key_scope")
|
|
155
|
+
except Exception:
|
|
156
|
+
recorded_scope = None
|
|
157
|
+
if isinstance(recorded_scope, str) and recorded_scope:
|
|
158
|
+
candidates.sort(key=lambda item: item[0] != recorded_scope)
|
|
159
|
+
|
|
160
|
+
last_exc: WalletBackendError | None = None
|
|
161
|
+
for key_scope, candidate in candidates:
|
|
135
162
|
try:
|
|
136
163
|
return (
|
|
137
164
|
decrypt_secret_material(raw_text, master_key=candidate),
|
|
@@ -147,6 +174,58 @@ def _load_user_wallet_secret_material(
|
|
|
147
174
|
)
|
|
148
175
|
|
|
149
176
|
|
|
177
|
+
def _maybe_migrate_wallet_envelope_kdf(
|
|
178
|
+
path: Path,
|
|
179
|
+
secret_material: str,
|
|
180
|
+
*,
|
|
181
|
+
user_id: str,
|
|
182
|
+
network: str,
|
|
183
|
+
key_scope: str | None,
|
|
184
|
+
address: str,
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Lazily rewrite an argon2id wallet envelope as hkdf-sha256.
|
|
187
|
+
|
|
188
|
+
Re-encrypts with the SAME key scope that just decrypted the file, so this
|
|
189
|
+
never performs a scope migration through the back door. Best-effort: any
|
|
190
|
+
failure leaves the (still readable) argon2id file untouched. Kill switch:
|
|
191
|
+
AGENT_WALLET_ENVELOPE_KDF_MIGRATION=0.
|
|
192
|
+
"""
|
|
193
|
+
from agent_wallet.config import envelope_kdf_migration_enabled
|
|
194
|
+
from agent_wallet.encrypted_storage import (
|
|
195
|
+
KDF_ARGON2ID,
|
|
196
|
+
_default_write_kdf,
|
|
197
|
+
envelope_kdf,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
if not envelope_kdf_migration_enabled():
|
|
202
|
+
return
|
|
203
|
+
if _default_write_kdf() == KDF_ARGON2ID:
|
|
204
|
+
return # forced-argon2id installs must not rewrite in place forever
|
|
205
|
+
if key_scope not in {"per-user-derived", "global-master"}:
|
|
206
|
+
return
|
|
207
|
+
if envelope_kdf(path.read_text(encoding="utf-8")) != KDF_ARGON2ID:
|
|
208
|
+
return
|
|
209
|
+
if key_scope == "per-user-derived":
|
|
210
|
+
master_key = _derive_user_scoped_key(
|
|
211
|
+
resolve_wallet_master_key(), user_id=user_id, network=network
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
master_key = resolve_wallet_master_key()
|
|
215
|
+
if not master_key.strip():
|
|
216
|
+
return
|
|
217
|
+
write_encrypted_wallet_file(
|
|
218
|
+
path,
|
|
219
|
+
secret_material,
|
|
220
|
+
master_key=master_key,
|
|
221
|
+
metadata=_user_wallet_metadata(
|
|
222
|
+
user_id, address, network=network, key_scope=key_scope
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
150
229
|
def ensure_user_solana_wallet(
|
|
151
230
|
user_id: str,
|
|
152
231
|
network: str | None = None,
|
|
@@ -206,6 +285,15 @@ def ensure_user_solana_wallet(
|
|
|
206
285
|
metadata=_user_wallet_metadata(user_id, signer.address, network=effective_network),
|
|
207
286
|
)
|
|
208
287
|
key_scope = "per-user-derived"
|
|
288
|
+
elif storage_format == "encrypted":
|
|
289
|
+
_maybe_migrate_wallet_envelope_kdf(
|
|
290
|
+
path,
|
|
291
|
+
secret_material,
|
|
292
|
+
user_id=user_id,
|
|
293
|
+
network=effective_network,
|
|
294
|
+
key_scope=key_scope,
|
|
295
|
+
address=signer.address,
|
|
296
|
+
)
|
|
209
297
|
return {
|
|
210
298
|
"user_id": user_id,
|
|
211
299
|
"address": signer.address,
|
|
@@ -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.66",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -19,15 +19,23 @@ INCLUDED_ROOT_FILES = [
|
|
|
19
19
|
"LICENSE",
|
|
20
20
|
"README.md",
|
|
21
21
|
"RELEASING.md",
|
|
22
|
+
"VERSION",
|
|
22
23
|
"install-from-github.sh",
|
|
23
24
|
"requirements.txt",
|
|
24
25
|
"setup.sh",
|
|
25
26
|
]
|
|
27
|
+
# Must cover every setup.sh require_path and mirror the npm package `files`
|
|
28
|
+
# allowlist (RELEASING.md "What Ships") — the bundle is the same runtime users
|
|
29
|
+
# get from npx, just delivered as a GitHub Release asset.
|
|
26
30
|
INCLUDED_TOP_LEVEL_DIRS = [
|
|
27
|
-
"
|
|
31
|
+
".claude-plugin",
|
|
28
32
|
".openclaw",
|
|
29
|
-
"agent-a2a-gateway",
|
|
30
33
|
"agent-wallet",
|
|
34
|
+
"bin",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"codex",
|
|
37
|
+
"hermes",
|
|
38
|
+
"scripts",
|
|
31
39
|
"wdk-btc-wallet",
|
|
32
40
|
"wdk-evm-wallet",
|
|
33
41
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wallet-operator
|
|
3
|
-
description: Use
|
|
3
|
+
description: Use whenever the user's own funds are involved, even if they never say "wallet": balances, portfolio, transfers, swaps, LI.FI cross-chain swaps, Jupiter swaps, Velora EVM swaps, BTC transfers, staking, Kamino lending, Bags launches, and wallet execution safety.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Wallet Operator
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-wallet",
|
|
3
3
|
"displayName": "Agent Wallet",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.66",
|
|
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"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Use
|
|
2
|
+
description: "Use whenever the user's own funds or wallet are involved — balances, portfolio, transfers, swaps, bridging, staking, lending, or x402 payments — even if the user never says \"wallet\". Trigger phrases include \"my balance\", \"how much SOL/ETH/BTC/USDC do I have\", \"send/transfer X to\", \"swap\", \"bridge\", \"stake\", \"my portfolio\", \"pay for this API\", \"x402\". Prefer agent-wallet MCP tools over shell commands, raw RPC, or crypto data servers. Preview writes first and execute only after explicit user confirmation."
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Agent Wallet Operator
|
|
@@ -78,6 +78,9 @@ APPROVAL_CONTEXT_MISSING_MESSAGE = (
|
|
|
78
78
|
"operation again, wait for explicit user confirmation, then retry execute. Do not ask the "
|
|
79
79
|
"user for a manual approval token."
|
|
80
80
|
)
|
|
81
|
+
# Tools served by the long-lived resident read worker instead of a cold CLI
|
|
82
|
+
# subprocess. Seeded with the legacy floor; _build_tool_definitions() extends
|
|
83
|
+
# it with every adapter-declared read-only tool at server start.
|
|
81
84
|
RESIDENT_READ_ONLY_TOOLS = {
|
|
82
85
|
"get_wallet_balance",
|
|
83
86
|
"get_wallet_portfolio",
|
|
@@ -270,6 +273,44 @@ def _cache_preview_for_approval(user_id: str, tool_name: str, payload: dict[str,
|
|
|
270
273
|
}
|
|
271
274
|
|
|
272
275
|
|
|
276
|
+
X402_PREVIEW_REUSE_TTL_SECONDS = 300
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _cache_x402_preview_payload(user_id: str, payload: dict[str, Any]) -> None:
|
|
280
|
+
"""Cache a paid-endpoint preview so x402_pay_request can skip its unpaid probe.
|
|
281
|
+
|
|
282
|
+
x402 previews run on the read-only resident-worker path, so they never reach
|
|
283
|
+
_cache_preview_for_approval (and preview-mode annotation strips
|
|
284
|
+
confirmation_summary anyway). Stored under the x402_preview_request key that
|
|
285
|
+
the pay alias resolves to. The wallet runtime revalidates the payload by
|
|
286
|
+
request fingerprint and falls back to a fresh probe on any mismatch, so a
|
|
287
|
+
stale or unrelated cached preview can never redirect a payment.
|
|
288
|
+
"""
|
|
289
|
+
if not isinstance(payload, dict) or payload.get("ok") is False:
|
|
290
|
+
return
|
|
291
|
+
data = payload.get("data")
|
|
292
|
+
if not isinstance(data, dict):
|
|
293
|
+
return
|
|
294
|
+
if not data.get("payment_required") or not str(data.get("request_fingerprint") or "").strip():
|
|
295
|
+
return
|
|
296
|
+
with _approval_cache_lock:
|
|
297
|
+
_prune_approval_preview_cache()
|
|
298
|
+
approval_preview_cache[_approval_cache_key(user_id, "x402_preview_request")] = {
|
|
299
|
+
"digest": _preview_digest(data),
|
|
300
|
+
"expires_at": time.time() + X402_PREVIEW_REUSE_TTL_SECONDS,
|
|
301
|
+
"preview": data,
|
|
302
|
+
"summary": None,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _attach_x402_approved_preview(tool_name: str, effective_params: dict[str, Any]) -> None:
|
|
307
|
+
if tool_name != "x402_pay_request" or "_approved_preview" in effective_params:
|
|
308
|
+
return
|
|
309
|
+
cached = _latest_cached_preview(_user_id(), tool_name)
|
|
310
|
+
if cached and isinstance(cached.get("preview"), dict):
|
|
311
|
+
effective_params["_approved_preview"] = cached["preview"]
|
|
312
|
+
|
|
313
|
+
|
|
273
314
|
def _latest_cached_preview(user_id: str, tool_name: str) -> dict[str, Any] | None:
|
|
274
315
|
with _approval_cache_lock:
|
|
275
316
|
_prune_approval_preview_cache()
|
|
@@ -622,20 +663,31 @@ def _call_wallet_cli(command: str, extra_args: list[str]) -> dict[str, Any]:
|
|
|
622
663
|
raise WalletCliError(f"agent-wallet CLI returned invalid JSON: {exc}") from exc
|
|
623
664
|
|
|
624
665
|
|
|
625
|
-
def _invoke_tool(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
666
|
+
def _invoke_tool(
|
|
667
|
+
tool_name: str,
|
|
668
|
+
arguments: dict[str, Any],
|
|
669
|
+
config: dict[str, Any],
|
|
670
|
+
*,
|
|
671
|
+
approval_summary: dict[str, Any] | None = None,
|
|
672
|
+
approval_mainnet_confirmed: bool = False,
|
|
673
|
+
) -> dict[str, Any]:
|
|
674
|
+
extra_args = [
|
|
675
|
+
"--user-id",
|
|
676
|
+
_user_id(),
|
|
677
|
+
"--tool",
|
|
678
|
+
tool_name,
|
|
679
|
+
"--arguments-json",
|
|
680
|
+
json.dumps(arguments),
|
|
681
|
+
"--config-json",
|
|
682
|
+
json.dumps(config),
|
|
683
|
+
]
|
|
684
|
+
if approval_summary is not None:
|
|
685
|
+
# The invoke subprocess mints the approval token itself, so an execute
|
|
686
|
+
# costs one cold start instead of two (issue-approval + invoke).
|
|
687
|
+
extra_args.extend(["--approval-summary-json", json.dumps(approval_summary)])
|
|
688
|
+
if approval_mainnet_confirmed:
|
|
689
|
+
extra_args.append("--approval-mainnet-confirmed")
|
|
690
|
+
return _call_wallet_cli("invoke", extra_args)
|
|
639
691
|
|
|
640
692
|
|
|
641
693
|
class _ResidentReadWorker:
|
|
@@ -943,33 +995,16 @@ def _prewarm_resident_read_worker() -> None:
|
|
|
943
995
|
).start()
|
|
944
996
|
|
|
945
997
|
|
|
946
|
-
def
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
) -> str:
|
|
998
|
+
def _approval_summary_for_preview(tool_name: str, preview_payload: dict[str, Any]) -> dict[str, Any]:
|
|
999
|
+
"""Build the digest-bound confirmation summary the invoke subprocess will
|
|
1000
|
+
mint an approval token from (in-process, replacing the old standalone
|
|
1001
|
+
issue-approval subprocess round trip)."""
|
|
951
1002
|
summary = preview_payload.get("confirmation_summary")
|
|
952
1003
|
if not isinstance(summary, dict):
|
|
953
1004
|
raise RuntimeError(f"No confirmation_summary available for {tool_name}.")
|
|
954
1005
|
summary_for_token = dict(summary)
|
|
955
1006
|
summary_for_token["_preview_digest"] = _preview_digest(preview_payload)
|
|
956
|
-
|
|
957
|
-
"--user-id",
|
|
958
|
-
_user_id(),
|
|
959
|
-
"--tool",
|
|
960
|
-
tool_name,
|
|
961
|
-
"--summary-json",
|
|
962
|
-
json.dumps(summary_for_token),
|
|
963
|
-
"--config-json",
|
|
964
|
-
json.dumps(config),
|
|
965
|
-
]
|
|
966
|
-
if preview_payload.get("is_mainnet") is True:
|
|
967
|
-
extra_args.append("--mainnet-confirmed")
|
|
968
|
-
payload = _call_wallet_cli("issue-approval", extra_args)
|
|
969
|
-
token = str(payload.get("approval_token") or "").strip()
|
|
970
|
-
if not token:
|
|
971
|
-
raise RuntimeError(f"issue-approval did not return an approval_token for {tool_name}.")
|
|
972
|
-
return token
|
|
1007
|
+
return summary_for_token
|
|
973
1008
|
|
|
974
1009
|
|
|
975
1010
|
def _invoke_resident_read_tool(tool_name: str, arguments: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1041,10 +1076,17 @@ def _attach_approval_for_execute(
|
|
|
1041
1076
|
tool_name: str,
|
|
1042
1077
|
config: dict[str, Any],
|
|
1043
1078
|
effective_params: dict[str, Any],
|
|
1044
|
-
) -> dict[str, Any] | None:
|
|
1079
|
+
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
1080
|
+
"""Prepare approval context for an execute call.
|
|
1081
|
+
|
|
1082
|
+
Returns ``(used_cache, approval_args)``: ``used_cache`` is the consumed
|
|
1083
|
+
bridge-managed preview (if any); ``approval_args`` carries the
|
|
1084
|
+
digest-bound summary the invoke subprocess mints its own token from,
|
|
1085
|
+
so no separate issue-approval subprocess is spawned.
|
|
1086
|
+
"""
|
|
1045
1087
|
mode = str(effective_params.get("mode") or "")
|
|
1046
1088
|
if mode not in {"execute", "intent_execute"}:
|
|
1047
|
-
return None
|
|
1089
|
+
return None, None
|
|
1048
1090
|
if tool_name == "swap_solana_tokens" and mode == "execute":
|
|
1049
1091
|
raise RuntimeError(
|
|
1050
1092
|
"Legacy exact-preview execute is disabled for Solana Jupiter swaps in Codex. "
|
|
@@ -1053,19 +1095,22 @@ def _attach_approval_for_execute(
|
|
|
1053
1095
|
cached = _latest_cached_preview(_user_id(), tool_name)
|
|
1054
1096
|
if cached and isinstance(cached.get("preview"), dict):
|
|
1055
1097
|
preview = cached["preview"]
|
|
1056
|
-
|
|
1098
|
+
approval_args = {
|
|
1099
|
+
"summary": _approval_summary_for_preview(tool_name, preview),
|
|
1100
|
+
"mainnet_confirmed": preview.get("is_mainnet") is True,
|
|
1101
|
+
}
|
|
1057
1102
|
if _requires_approved_preview_payload(tool_name, effective_params):
|
|
1058
1103
|
effective_params["_approved_preview"] = preview
|
|
1059
|
-
return cached
|
|
1104
|
+
return cached, approval_args
|
|
1060
1105
|
approval_token = str(effective_params.get("approval_token") or "").strip()
|
|
1061
1106
|
if approval_token and _requires_approved_preview_payload(tool_name, effective_params):
|
|
1062
1107
|
cached_preview = _cached_preview_for_token(_user_id(), tool_name, approval_token)
|
|
1063
1108
|
if cached_preview is not None and "_approved_preview" not in effective_params:
|
|
1064
1109
|
effective_params["_approved_preview"] = cached_preview
|
|
1065
1110
|
if effective_params.get("approval_token"):
|
|
1066
|
-
return None
|
|
1111
|
+
return None, None
|
|
1067
1112
|
if _should_let_backend_authorize_autonomous_execution(tool_name, effective_params, config):
|
|
1068
|
-
return None
|
|
1113
|
+
return None, None
|
|
1069
1114
|
raise RuntimeError(APPROVAL_CONTEXT_MISSING_MESSAGE)
|
|
1070
1115
|
|
|
1071
1116
|
|
|
@@ -1242,6 +1287,13 @@ def _build_tool_definitions() -> list[dict[str, Any]]:
|
|
|
1242
1287
|
merged.setdefault(spec["name"], spec)
|
|
1243
1288
|
for spec in merged.values():
|
|
1244
1289
|
spec["input_schema"] = _sanitize_schema(spec["input_schema"])
|
|
1290
|
+
if spec.get("read_only") is True:
|
|
1291
|
+
# Route every adapter-declared read-only tool through the resident
|
|
1292
|
+
# read worker: reads then cost one warm request instead of a cold
|
|
1293
|
+
# interpreter boot + onboarding per call. The worker enforces the
|
|
1294
|
+
# same read_only contract on its side, and transport failures fall
|
|
1295
|
+
# back to the cold subprocess path.
|
|
1296
|
+
RESIDENT_READ_ONLY_TOOLS.add(spec["name"])
|
|
1245
1297
|
if spec.get("read_only") is False:
|
|
1246
1298
|
spec["description"] = (
|
|
1247
1299
|
f"{spec['description']} Preview first when supported. Execute reuses cached "
|
|
@@ -1388,11 +1440,22 @@ def _invoke_wallet_tool_blocking(
|
|
|
1388
1440
|
call never freezes the MCP server (tools/list, read-only calls, and
|
|
1389
1441
|
cancellation stay responsive).
|
|
1390
1442
|
"""
|
|
1391
|
-
used_cache = _attach_approval_for_execute(tool_name, config, effective_params)
|
|
1443
|
+
used_cache, approval_args = _attach_approval_for_execute(tool_name, config, effective_params)
|
|
1444
|
+
_attach_x402_approved_preview(tool_name, effective_params)
|
|
1392
1445
|
try:
|
|
1393
|
-
payload = _invoke_tool(
|
|
1446
|
+
payload = _invoke_tool(
|
|
1447
|
+
tool_name,
|
|
1448
|
+
effective_params,
|
|
1449
|
+
config,
|
|
1450
|
+
approval_summary=(approval_args or {}).get("summary"),
|
|
1451
|
+
approval_mainnet_confirmed=bool((approval_args or {}).get("mainnet_confirmed")),
|
|
1452
|
+
)
|
|
1394
1453
|
except Exception as exc:
|
|
1395
1454
|
raise _normalize_approval_context_error(exc) from exc
|
|
1455
|
+
if tool_name == "x402_pay_request":
|
|
1456
|
+
# The cached x402 preview quote is single-use: whether the payment
|
|
1457
|
+
# succeeded or the endpoint re-priced, the next pay must re-preview.
|
|
1458
|
+
_consume_cached_preview(_user_id(), tool_name)
|
|
1396
1459
|
_cache_preview_for_approval(_user_id(), tool_name, payload)
|
|
1397
1460
|
# A bridge-managed preview that was just executed successfully is single-use:
|
|
1398
1461
|
# drop it so a duplicate execute cannot silently re-run the operation.
|
|
@@ -1431,15 +1494,21 @@ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[st
|
|
|
1431
1494
|
_invoke_wallet_tool_blocking, tool_name, config, effective_params
|
|
1432
1495
|
)
|
|
1433
1496
|
|
|
1497
|
+
if tool_name == "x402_preview_request":
|
|
1498
|
+
_cache_x402_preview_payload(_user_id(), payload)
|
|
1434
1499
|
if payload.get("ok") is False:
|
|
1435
1500
|
raise RuntimeError(str(payload.get("error") or f"{tool_name} failed"))
|
|
1436
1501
|
return payload.get("data", {})
|
|
1437
1502
|
|
|
1438
1503
|
|
|
1439
1504
|
BASE_INSTRUCTIONS = (
|
|
1440
|
-
"
|
|
1441
|
-
"
|
|
1442
|
-
"
|
|
1505
|
+
"This server is the user's own local AgentLayer wallet (Solana, EVM, Bitcoin). Use its "
|
|
1506
|
+
"tools whenever the user asks about their balances, portfolio, addresses, transfers, "
|
|
1507
|
+
"swaps, bridging, staking, lending, or x402 payments — even when they never say the "
|
|
1508
|
+
"word 'wallet'. For anything touching the user's own funds, prefer these tools over "
|
|
1509
|
+
"shell commands, raw RPC calls, or other crypto data servers (those are for arbitrary "
|
|
1510
|
+
"addresses and market data, not the user's wallet). Keep wallet secrets local. Preview "
|
|
1511
|
+
"writes first when supported, and execute only after explicit user confirmation."
|
|
1443
1512
|
)
|
|
1444
1513
|
|
|
1445
1514
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "wallet-operator"
|
|
3
|
-
description: "Use
|
|
3
|
+
description: "Use whenever the user's own funds or wallet are involved — balances, portfolio, transfers, swaps, bridging, staking, lending, or x402 payments — even if the user never says \"wallet\". Trigger phrases include \"my balance\", \"how much SOL/ETH/BTC/USDC do I have\", \"send/transfer X to\", \"swap\", \"bridge\", \"stake\", \"my portfolio\". Prefer wallet tools over shell commands, preview writes first, and keep approval/signing semantics intact."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Agent Wallet Operator
|
package/install-from-github.sh
CHANGED
|
@@ -120,9 +120,15 @@ if [ -z "$SOURCE_ROOT" ] || [ ! -d "$SOURCE_ROOT" ]; then
|
|
|
120
120
|
exit 1
|
|
121
121
|
fi
|
|
122
122
|
|
|
123
|
+
# Pre-flight must be a superset of setup.sh's require_path list: everything is
|
|
124
|
+
# checked on the EXTRACTED temp tree, before the destructive swap below. A
|
|
125
|
+
# bundle that would fail setup.sh must never replace the user's runtime.
|
|
123
126
|
require_path "$SOURCE_ROOT/setup.sh" "local setup entrypoint"
|
|
124
127
|
require_path "$SOURCE_ROOT/agent-wallet" "agent-wallet package"
|
|
128
|
+
require_path "$SOURCE_ROOT/agent-wallet/scripts/install_agent_wallet.py" "Python installer"
|
|
125
129
|
require_path "$SOURCE_ROOT/.openclaw/extensions/agent-wallet" "OpenClaw extension"
|
|
130
|
+
require_path "$SOURCE_ROOT/codex/plugins/agent-wallet/.codex-plugin/plugin.json" "Codex plugin"
|
|
131
|
+
require_path "$SOURCE_ROOT/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json" "Claude Code plugin"
|
|
126
132
|
require_path "$SOURCE_ROOT/wdk-btc-wallet/package.json" "wdk-btc-wallet runtime"
|
|
127
133
|
require_path "$SOURCE_ROOT/wdk-evm-wallet/package.json" "wdk-evm-wallet runtime"
|
|
128
134
|
|
package/package.json
CHANGED