@agentlayer.tech/wallet 0.1.9 → 0.1.11
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/CHANGELOG.md +5 -0
- package/README.md +23 -8
- package/RELEASING.md +143 -144
- package/agent-wallet/README.md +18 -0
- package/agent-wallet/agent_wallet/config.py +11 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +74 -9
- package/agent-wallet/agent_wallet/providers/jupiter.py +171 -2
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/install_agent_wallet.py +1 -0
- package/bin/openclaw-agent-wallet.mjs +229 -5
- package/hermes/plugins/agent_wallet/README.md +54 -0
- package/hermes/plugins/agent_wallet/__init__.py +29 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +7 -0
- package/hermes/plugins/agent_wallet/schemas.py +134 -0
- package/hermes/plugins/agent_wallet/tools.py +433 -0
- package/package.json +4 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Tool schemas exposed to Hermes Agent."""
|
|
2
|
+
|
|
3
|
+
AGENT_WALLET_TOOLS = {
|
|
4
|
+
"name": "agent_wallet_tools",
|
|
5
|
+
"description": (
|
|
6
|
+
"List AgentLayer wallet capabilities available through the Hermes bridge. "
|
|
7
|
+
"Use this before agent_wallet_invoke when you need the exact underlying "
|
|
8
|
+
"wallet tool names, JSON schemas, or safety levels. This is read-only and "
|
|
9
|
+
"does not create, unlock, or modify wallets."
|
|
10
|
+
),
|
|
11
|
+
"parameters": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"backend": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["all", "solana_local", "wdk_btc_local", "wdk_evm_local"],
|
|
17
|
+
"description": "Optional backend filter. Defaults to all.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"additionalProperties": False,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
AGENT_WALLET_INVOKE = {
|
|
25
|
+
"name": "agent_wallet_invoke",
|
|
26
|
+
"description": (
|
|
27
|
+
"Invoke one existing AgentLayer/OpenClaw wallet tool through the local "
|
|
28
|
+
"Python wallet backend. Prefer read-only tools and preview modes first. "
|
|
29
|
+
"Execute modes require an approval_token from agent_wallet_approve bound "
|
|
30
|
+
"to the exact previewed operation after explicit user confirmation."
|
|
31
|
+
),
|
|
32
|
+
"parameters": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"tool_name": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Underlying wallet tool name, for example get_wallet_address or transfer_sol.",
|
|
38
|
+
},
|
|
39
|
+
"arguments": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"description": "JSON arguments for the underlying wallet tool.",
|
|
42
|
+
"additionalProperties": True,
|
|
43
|
+
},
|
|
44
|
+
"backend": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["solana_local", "wdk_btc_local", "wdk_evm_local"],
|
|
47
|
+
"description": "Optional backend override for this invocation.",
|
|
48
|
+
},
|
|
49
|
+
"network": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Optional network override, such as devnet, mainnet, bitcoin, ethereum, or base.",
|
|
52
|
+
},
|
|
53
|
+
"user_id": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Optional local wallet owner id. Defaults to AGENT_WALLET_USER_ID, USER, or hermes-local-user.",
|
|
56
|
+
},
|
|
57
|
+
"config": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"description": (
|
|
60
|
+
"Optional non-secret wallet config overrides. Do not include privateKey, "
|
|
61
|
+
"masterKey, or approvalSecret."
|
|
62
|
+
),
|
|
63
|
+
"additionalProperties": True,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
"required": ["tool_name"],
|
|
67
|
+
"additionalProperties": False,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
AGENT_WALLET_APPROVE = {
|
|
72
|
+
"name": "agent_wallet_approve",
|
|
73
|
+
"description": (
|
|
74
|
+
"Issue a short-lived AgentLayer/OpenClaw approval_token for one exact "
|
|
75
|
+
"wallet execute operation after the user explicitly confirms the previewed "
|
|
76
|
+
"confirmation_summary. Use only after agent_wallet_invoke preview/prepare "
|
|
77
|
+
"returns the exact confirmation_summary. Mainnet approvals require "
|
|
78
|
+
"mainnet_confirmed=true."
|
|
79
|
+
),
|
|
80
|
+
"parameters": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"properties": {
|
|
83
|
+
"tool_name": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "Underlying wallet tool name that will be executed.",
|
|
86
|
+
},
|
|
87
|
+
"confirmation_summary": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"description": (
|
|
90
|
+
"Exact confirmation_summary from the preview or prepare result. "
|
|
91
|
+
"Do not edit or summarize it."
|
|
92
|
+
),
|
|
93
|
+
"additionalProperties": True,
|
|
94
|
+
},
|
|
95
|
+
"user_confirmed": {
|
|
96
|
+
"type": "boolean",
|
|
97
|
+
"description": "Must be true only after the user explicitly approves this exact operation.",
|
|
98
|
+
},
|
|
99
|
+
"mainnet_confirmed": {
|
|
100
|
+
"type": "boolean",
|
|
101
|
+
"description": "Must be true for mainnet execute operations after explicit mainnet confirmation.",
|
|
102
|
+
},
|
|
103
|
+
"ttl_seconds": {
|
|
104
|
+
"type": "integer",
|
|
105
|
+
"minimum": 1,
|
|
106
|
+
"maximum": 3600,
|
|
107
|
+
"description": "Optional approval token lifetime in seconds.",
|
|
108
|
+
},
|
|
109
|
+
"backend": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"enum": ["solana_local", "wdk_btc_local", "wdk_evm_local"],
|
|
112
|
+
"description": "Optional backend override matching the planned execute invocation.",
|
|
113
|
+
},
|
|
114
|
+
"network": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"description": "Optional network override matching the planned execute invocation.",
|
|
117
|
+
},
|
|
118
|
+
"user_id": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "Optional local wallet owner id. Defaults to AGENT_WALLET_USER_ID, USER, or hermes-local-user.",
|
|
121
|
+
},
|
|
122
|
+
"config": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"description": (
|
|
125
|
+
"Optional non-secret wallet config overrides matching the planned execute invocation. "
|
|
126
|
+
"Do not include privateKey, masterKey, or approvalSecret."
|
|
127
|
+
),
|
|
128
|
+
"additionalProperties": True,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
"required": ["tool_name", "confirmation_summary", "user_confirmed"],
|
|
132
|
+
"additionalProperties": False,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Hermes Agent handlers that forward to the existing wallet CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import base64
|
|
8
|
+
import hashlib
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SECRET_CONFIG_KEYS = {"privateKey", "masterKey", "approvalSecret"}
|
|
17
|
+
BACKENDS = ("solana_local", "wdk_btc_local", "wdk_evm_local")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _json(data: dict[str, Any]) -> str:
|
|
21
|
+
return json.dumps(data, sort_keys=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _canonical_json_text(payload: dict[str, Any]) -> str:
|
|
25
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _preview_digest(preview: dict[str, Any]) -> str:
|
|
29
|
+
return hashlib.sha256(_canonical_json_text(preview).encode("utf-8")).hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _hermes_home() -> Path:
|
|
33
|
+
return Path(os.getenv("HERMES_HOME", "~/.hermes")).expanduser()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _preview_cache_path() -> Path:
|
|
37
|
+
return _hermes_home() / "agent_wallet_preview_cache.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_preview_cache() -> dict[str, Any]:
|
|
41
|
+
path = _preview_cache_path()
|
|
42
|
+
try:
|
|
43
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
44
|
+
except (OSError, json.JSONDecodeError):
|
|
45
|
+
return {"previews": {}}
|
|
46
|
+
if not isinstance(payload, dict):
|
|
47
|
+
return {"previews": {}}
|
|
48
|
+
previews = payload.get("previews")
|
|
49
|
+
if not isinstance(previews, dict):
|
|
50
|
+
payload["previews"] = {}
|
|
51
|
+
return payload
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _write_preview_cache(cache: dict[str, Any]) -> None:
|
|
55
|
+
path = _preview_cache_path()
|
|
56
|
+
try:
|
|
57
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
path.write_text(json.dumps(cache, sort_keys=True), encoding="utf-8")
|
|
59
|
+
path.chmod(0o600)
|
|
60
|
+
except OSError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _prune_preview_cache(cache: dict[str, Any]) -> dict[str, Any]:
|
|
65
|
+
now = time.time()
|
|
66
|
+
previews = cache.get("previews")
|
|
67
|
+
if not isinstance(previews, dict):
|
|
68
|
+
previews = {}
|
|
69
|
+
cache["previews"] = {
|
|
70
|
+
key: value
|
|
71
|
+
for key, value in previews.items()
|
|
72
|
+
if isinstance(value, dict) and float(value.get("expires_at") or 0) > now
|
|
73
|
+
}
|
|
74
|
+
return cache
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _cache_swap_preview(tool_name: str, result: dict[str, Any], ttl_seconds: int = 900) -> None:
|
|
78
|
+
if tool_name != "swap_solana_tokens" or result.get("ok") is not True:
|
|
79
|
+
return
|
|
80
|
+
preview = result.get("data")
|
|
81
|
+
if not isinstance(preview, dict):
|
|
82
|
+
return
|
|
83
|
+
if preview.get("mode") != "preview" or preview.get("asset_type") != "swap":
|
|
84
|
+
return
|
|
85
|
+
summary = preview.get("confirmation_summary")
|
|
86
|
+
if not isinstance(summary, dict):
|
|
87
|
+
return
|
|
88
|
+
digest = _preview_digest(preview)
|
|
89
|
+
cache = _prune_preview_cache(_read_preview_cache())
|
|
90
|
+
cache["previews"][digest] = {
|
|
91
|
+
"expires_at": time.time() + ttl_seconds,
|
|
92
|
+
"preview": preview,
|
|
93
|
+
"confirmation_summary": summary,
|
|
94
|
+
}
|
|
95
|
+
_write_preview_cache(cache)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _lookup_preview_for_summary(summary: dict[str, Any]) -> tuple[str, dict[str, Any]] | tuple[None, None]:
|
|
99
|
+
cache = _prune_preview_cache(_read_preview_cache())
|
|
100
|
+
for digest, entry in cache.get("previews", {}).items():
|
|
101
|
+
if not isinstance(entry, dict):
|
|
102
|
+
continue
|
|
103
|
+
if entry.get("confirmation_summary") == summary and isinstance(entry.get("preview"), dict):
|
|
104
|
+
_write_preview_cache(cache)
|
|
105
|
+
return str(digest), entry["preview"]
|
|
106
|
+
_write_preview_cache(cache)
|
|
107
|
+
return None, None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _approval_token_preview_digest(token: str) -> str:
|
|
111
|
+
if not isinstance(token, str) or "." not in token:
|
|
112
|
+
return ""
|
|
113
|
+
encoded_payload = token.split(".", 1)[0]
|
|
114
|
+
try:
|
|
115
|
+
padding = "=" * (-len(encoded_payload) % 4)
|
|
116
|
+
payload = json.loads(base64.urlsafe_b64decode(encoded_payload + padding).decode("utf-8"))
|
|
117
|
+
except Exception:
|
|
118
|
+
return ""
|
|
119
|
+
summary = payload.get("binding", {}).get("summary") if isinstance(payload, dict) else None
|
|
120
|
+
if not isinstance(summary, dict):
|
|
121
|
+
return ""
|
|
122
|
+
digest = summary.get("_preview_digest")
|
|
123
|
+
return str(digest).strip() if isinstance(digest, str) else ""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _lookup_preview_for_token(token: str) -> dict[str, Any] | None:
|
|
127
|
+
digest = _approval_token_preview_digest(token)
|
|
128
|
+
if not digest:
|
|
129
|
+
return None
|
|
130
|
+
cache = _prune_preview_cache(_read_preview_cache())
|
|
131
|
+
entry = cache.get("previews", {}).get(digest)
|
|
132
|
+
_write_preview_cache(cache)
|
|
133
|
+
if isinstance(entry, dict) and isinstance(entry.get("preview"), dict):
|
|
134
|
+
return entry["preview"]
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _repo_relative_package_root() -> Path:
|
|
139
|
+
return Path(__file__).resolve().parents[3] / "agent-wallet"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _resolve_package_root() -> Path:
|
|
143
|
+
candidates = [
|
|
144
|
+
os.getenv("AGENT_WALLET_PACKAGE_ROOT"),
|
|
145
|
+
os.getenv("OPENCLAW_AGENT_WALLET_PACKAGE_ROOT"),
|
|
146
|
+
str(_repo_relative_package_root()),
|
|
147
|
+
str(Path.cwd() / "agent-wallet"),
|
|
148
|
+
]
|
|
149
|
+
for candidate in candidates:
|
|
150
|
+
if not candidate:
|
|
151
|
+
continue
|
|
152
|
+
root = Path(candidate).expanduser().resolve()
|
|
153
|
+
if (root / "agent_wallet" / "__init__.py").exists():
|
|
154
|
+
return root
|
|
155
|
+
raise RuntimeError(
|
|
156
|
+
"Could not resolve agent-wallet package root. Set AGENT_WALLET_PACKAGE_ROOT."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _python_bin(package_root: Path) -> str:
|
|
161
|
+
for candidate in (
|
|
162
|
+
os.getenv("AGENT_WALLET_PYTHON"),
|
|
163
|
+
os.getenv("OPENCLAW_AGENT_WALLET_PYTHON"),
|
|
164
|
+
str(package_root / ".venv" / "bin" / "python"),
|
|
165
|
+
str(package_root / ".runtime-venv" / "bin" / "python"),
|
|
166
|
+
"python3",
|
|
167
|
+
):
|
|
168
|
+
if not candidate:
|
|
169
|
+
continue
|
|
170
|
+
resolved = Path(candidate).expanduser()
|
|
171
|
+
if resolved.is_absolute() and not resolved.exists():
|
|
172
|
+
continue
|
|
173
|
+
return str(resolved)
|
|
174
|
+
return "python3"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _user_id(args: dict[str, Any]) -> str:
|
|
178
|
+
value = (
|
|
179
|
+
args.get("user_id")
|
|
180
|
+
or os.getenv("AGENT_WALLET_USER_ID")
|
|
181
|
+
or os.getenv("OPENCLAW_AGENT_WALLET_USER_ID")
|
|
182
|
+
or os.getenv("USER")
|
|
183
|
+
or "hermes-local-user"
|
|
184
|
+
)
|
|
185
|
+
return str(value).strip() or "hermes-local-user"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _reject_secret_config(config: dict[str, Any]) -> None:
|
|
189
|
+
present = sorted(key for key in SECRET_CONFIG_KEYS if str(config.get(key) or "").strip())
|
|
190
|
+
if present:
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
"Sensitive keys are not allowed in Hermes wallet bridge config: "
|
|
193
|
+
+ ", ".join(present)
|
|
194
|
+
+ ". Use sealed_keys.json and protected environment injection."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _base_config(args: dict[str, Any]) -> dict[str, Any]:
|
|
199
|
+
raw = args.get("config") or {}
|
|
200
|
+
if not isinstance(raw, dict):
|
|
201
|
+
raise RuntimeError("config must be a JSON object when provided.")
|
|
202
|
+
config = dict(raw)
|
|
203
|
+
backend = args.get("backend") or os.getenv("AGENT_WALLET_BACKEND")
|
|
204
|
+
network = args.get("network") or os.getenv("AGENT_WALLET_NETWORK")
|
|
205
|
+
if backend:
|
|
206
|
+
config["backend"] = str(backend).strip()
|
|
207
|
+
if network:
|
|
208
|
+
config["network"] = str(network).strip()
|
|
209
|
+
_reject_secret_config(config)
|
|
210
|
+
return config
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _cli_env(package_root: Path) -> dict[str, str]:
|
|
214
|
+
env = dict(os.environ)
|
|
215
|
+
prior = env.get("PYTHONPATH", "")
|
|
216
|
+
env["PYTHONPATH"] = str(package_root) if not prior else f"{package_root}{os.pathsep}{prior}"
|
|
217
|
+
if not env.get("AGENT_WALLET_BOOT_KEY"):
|
|
218
|
+
key_file = env.get("AGENT_WALLET_BOOT_KEY_FILE", "").strip()
|
|
219
|
+
if key_file:
|
|
220
|
+
try:
|
|
221
|
+
boot_key = Path(key_file).expanduser().read_text(encoding="utf-8").strip()
|
|
222
|
+
except OSError:
|
|
223
|
+
boot_key = ""
|
|
224
|
+
if boot_key:
|
|
225
|
+
env["AGENT_WALLET_BOOT_KEY"] = boot_key
|
|
226
|
+
return env
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _call_wallet_cli(args: dict[str, Any]) -> dict[str, Any]:
|
|
230
|
+
package_root = _resolve_package_root()
|
|
231
|
+
config = _base_config(args)
|
|
232
|
+
tool_name = str(args.get("tool_name") or "").strip()
|
|
233
|
+
if not tool_name:
|
|
234
|
+
raise RuntimeError("tool_name is required.")
|
|
235
|
+
|
|
236
|
+
tool_args = args.get("arguments") or {}
|
|
237
|
+
if not isinstance(tool_args, dict):
|
|
238
|
+
raise RuntimeError("arguments must be a JSON object when provided.")
|
|
239
|
+
if tool_name == "swap_solana_tokens" and str(tool_args.get("mode") or "") == "execute":
|
|
240
|
+
approval_token = str(tool_args.get("approval_token") or "").strip()
|
|
241
|
+
cached_preview = _lookup_preview_for_token(approval_token)
|
|
242
|
+
if cached_preview is not None and "_approved_preview" not in tool_args:
|
|
243
|
+
tool_args = dict(tool_args)
|
|
244
|
+
tool_args["_approved_preview"] = cached_preview
|
|
245
|
+
|
|
246
|
+
command = [
|
|
247
|
+
_python_bin(package_root),
|
|
248
|
+
"-m",
|
|
249
|
+
"agent_wallet.openclaw_cli",
|
|
250
|
+
"invoke",
|
|
251
|
+
"--user-id",
|
|
252
|
+
_user_id(args),
|
|
253
|
+
"--tool",
|
|
254
|
+
tool_name,
|
|
255
|
+
"--arguments-json",
|
|
256
|
+
json.dumps(tool_args),
|
|
257
|
+
"--config-json",
|
|
258
|
+
json.dumps(config),
|
|
259
|
+
]
|
|
260
|
+
completed = subprocess.run(
|
|
261
|
+
command,
|
|
262
|
+
cwd=str(package_root),
|
|
263
|
+
env=_cli_env(package_root),
|
|
264
|
+
text=True,
|
|
265
|
+
capture_output=True,
|
|
266
|
+
timeout=float(os.getenv("AGENT_WALLET_HERMES_TIMEOUT", "120")),
|
|
267
|
+
check=False,
|
|
268
|
+
)
|
|
269
|
+
if completed.returncode != 0:
|
|
270
|
+
detail = completed.stderr.strip() or completed.stdout.strip()
|
|
271
|
+
return {"ok": False, "error": detail or f"wallet CLI exited {completed.returncode}"}
|
|
272
|
+
try:
|
|
273
|
+
result = json.loads(completed.stdout.strip() or "{}")
|
|
274
|
+
_cache_swap_preview(tool_name, result)
|
|
275
|
+
return result
|
|
276
|
+
except json.JSONDecodeError as exc:
|
|
277
|
+
return {"ok": False, "error": f"wallet CLI returned invalid JSON: {exc}"}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _call_issue_approval(args: dict[str, Any]) -> dict[str, Any]:
|
|
281
|
+
if args.get("user_confirmed") is not True:
|
|
282
|
+
raise RuntimeError(
|
|
283
|
+
"user_confirmed=true is required after explicit user approval of the exact confirmation_summary."
|
|
284
|
+
)
|
|
285
|
+
package_root = _resolve_package_root()
|
|
286
|
+
config = _base_config(args)
|
|
287
|
+
tool_name = str(args.get("tool_name") or "").strip()
|
|
288
|
+
if not tool_name:
|
|
289
|
+
raise RuntimeError("tool_name is required.")
|
|
290
|
+
|
|
291
|
+
summary = args.get("confirmation_summary")
|
|
292
|
+
if not isinstance(summary, dict) or not summary:
|
|
293
|
+
raise RuntimeError("confirmation_summary must be the non-empty object returned by preview/prepare.")
|
|
294
|
+
summary_for_token = dict(summary)
|
|
295
|
+
preview_digest, _preview = _lookup_preview_for_summary(summary)
|
|
296
|
+
if preview_digest:
|
|
297
|
+
summary_for_token["_preview_digest"] = preview_digest
|
|
298
|
+
|
|
299
|
+
command = [
|
|
300
|
+
_python_bin(package_root),
|
|
301
|
+
"-m",
|
|
302
|
+
"agent_wallet.openclaw_cli",
|
|
303
|
+
"issue-approval",
|
|
304
|
+
"--user-id",
|
|
305
|
+
_user_id(args),
|
|
306
|
+
"--tool",
|
|
307
|
+
tool_name,
|
|
308
|
+
"--summary-json",
|
|
309
|
+
json.dumps(summary_for_token),
|
|
310
|
+
"--config-json",
|
|
311
|
+
json.dumps(config),
|
|
312
|
+
]
|
|
313
|
+
if args.get("mainnet_confirmed") is True:
|
|
314
|
+
command.append("--mainnet-confirmed")
|
|
315
|
+
ttl_seconds = args.get("ttl_seconds")
|
|
316
|
+
if ttl_seconds is not None:
|
|
317
|
+
ttl = int(ttl_seconds)
|
|
318
|
+
if ttl <= 0 or ttl > 3600:
|
|
319
|
+
raise RuntimeError("ttl_seconds must be between 1 and 3600.")
|
|
320
|
+
command.extend(["--ttl-seconds", str(ttl)])
|
|
321
|
+
|
|
322
|
+
completed = subprocess.run(
|
|
323
|
+
command,
|
|
324
|
+
cwd=str(package_root),
|
|
325
|
+
env=_cli_env(package_root),
|
|
326
|
+
text=True,
|
|
327
|
+
capture_output=True,
|
|
328
|
+
timeout=float(os.getenv("AGENT_WALLET_HERMES_TIMEOUT", "120")),
|
|
329
|
+
check=False,
|
|
330
|
+
)
|
|
331
|
+
if completed.returncode != 0:
|
|
332
|
+
detail = completed.stderr.strip() or completed.stdout.strip()
|
|
333
|
+
return {"ok": False, "error": detail or f"wallet CLI exited {completed.returncode}"}
|
|
334
|
+
try:
|
|
335
|
+
return json.loads(completed.stdout.strip() or "{}")
|
|
336
|
+
except json.JSONDecodeError as exc:
|
|
337
|
+
return {"ok": False, "error": f"wallet CLI returned invalid JSON: {exc}"}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class _SchemaOnlyBackend:
|
|
341
|
+
def __init__(self, *, name: str, chain: str, network: str):
|
|
342
|
+
self.name = name
|
|
343
|
+
self.chain = chain
|
|
344
|
+
self.network = network
|
|
345
|
+
self.sign_only = True
|
|
346
|
+
|
|
347
|
+
def get_capabilities(self):
|
|
348
|
+
from agent_wallet.wallet_layer.base import WalletCapabilities
|
|
349
|
+
|
|
350
|
+
return WalletCapabilities(
|
|
351
|
+
backend=self.name,
|
|
352
|
+
chain=self.chain,
|
|
353
|
+
custody_model="local",
|
|
354
|
+
sign_only=True,
|
|
355
|
+
has_signer=False,
|
|
356
|
+
can_get_address=True,
|
|
357
|
+
can_get_balance=True,
|
|
358
|
+
external_dependencies=[],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
async def get_address(self):
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
async def get_balance(self, address=None):
|
|
365
|
+
return {}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _schema_backend(name: str) -> _SchemaOnlyBackend:
|
|
369
|
+
if name == "wdk_btc_local":
|
|
370
|
+
return _SchemaOnlyBackend(name=name, chain="bitcoin", network="bitcoin")
|
|
371
|
+
if name == "wdk_evm_local":
|
|
372
|
+
return _SchemaOnlyBackend(name=name, chain="evm", network="ethereum")
|
|
373
|
+
return _SchemaOnlyBackend(name="solana_local", chain="solana", network="mainnet")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _tool_specs(backend_name: str) -> list[dict[str, Any]]:
|
|
377
|
+
package_root = _resolve_package_root()
|
|
378
|
+
package_root_text = str(package_root)
|
|
379
|
+
inserted = package_root_text not in sys.path
|
|
380
|
+
if inserted:
|
|
381
|
+
sys.path.insert(0, package_root_text)
|
|
382
|
+
try:
|
|
383
|
+
from agent_wallet.openclaw_adapter import OpenClawWalletAdapter
|
|
384
|
+
|
|
385
|
+
adapter = OpenClawWalletAdapter(_schema_backend(backend_name))
|
|
386
|
+
return [tool.model_dump() for tool in adapter.list_tools()]
|
|
387
|
+
finally:
|
|
388
|
+
if inserted:
|
|
389
|
+
try:
|
|
390
|
+
sys.path.remove(package_root_text)
|
|
391
|
+
except ValueError:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def agent_wallet_tools(args: dict, **kwargs) -> str:
|
|
396
|
+
try:
|
|
397
|
+
requested = str((args or {}).get("backend") or "all").strip() or "all"
|
|
398
|
+
backend_names = BACKENDS if requested == "all" else (requested,)
|
|
399
|
+
invalid = [name for name in backend_names if name not in BACKENDS]
|
|
400
|
+
if invalid:
|
|
401
|
+
return _json({"ok": False, "error": f"Unknown backend: {', '.join(invalid)}"})
|
|
402
|
+
tools = {
|
|
403
|
+
backend_name: _tool_specs(backend_name)
|
|
404
|
+
for backend_name in backend_names
|
|
405
|
+
}
|
|
406
|
+
return _json(
|
|
407
|
+
{
|
|
408
|
+
"ok": True,
|
|
409
|
+
"bridge": "hermes-agent-wallet",
|
|
410
|
+
"backends": list(backend_names),
|
|
411
|
+
"tools": tools,
|
|
412
|
+
"usage": (
|
|
413
|
+
"Call agent_wallet_invoke with one of these tool names and JSON arguments. "
|
|
414
|
+
"Use preview before execute. Execute requires a host-issued approval_token."
|
|
415
|
+
),
|
|
416
|
+
}
|
|
417
|
+
)
|
|
418
|
+
except Exception as exc:
|
|
419
|
+
return _json({"ok": False, "error": str(exc)})
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def agent_wallet_invoke(args: dict, **kwargs) -> str:
|
|
423
|
+
try:
|
|
424
|
+
return _json(_call_wallet_cli(args or {}))
|
|
425
|
+
except Exception as exc:
|
|
426
|
+
return _json({"ok": False, "error": str(exc)})
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def agent_wallet_approve(args: dict, **kwargs) -> str:
|
|
430
|
+
try:
|
|
431
|
+
return _json(_call_issue_approval(args or {}))
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
return _json({"ok": False, "error": str(exc)})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentlayer.tech/wallet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"agent-wallet/pyproject.toml",
|
|
39
39
|
".openclaw/AGENTS.md",
|
|
40
40
|
".openclaw/extensions/agent-wallet/",
|
|
41
|
+
"hermes/plugins/agent_wallet/",
|
|
41
42
|
"wdk-btc-wallet/src/",
|
|
42
43
|
"wdk-btc-wallet/bootstrap.sh",
|
|
43
44
|
"wdk-btc-wallet/run-local.sh",
|
|
@@ -52,6 +53,8 @@
|
|
|
52
53
|
"wdk-evm-wallet/package-lock.json",
|
|
53
54
|
"!agent-wallet/**/__pycache__/**",
|
|
54
55
|
"!agent-wallet/**/*.pyc",
|
|
56
|
+
"!hermes/**/__pycache__/**",
|
|
57
|
+
"!hermes/**/*.pyc",
|
|
55
58
|
"!agent-wallet/.pytest_cache/**",
|
|
56
59
|
"!agent-wallet/.runtime-venv/**",
|
|
57
60
|
"!**/node_modules/**",
|