@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.
@@ -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.9",
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/**",