@agentlayer.tech/wallet 0.1.10 → 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,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.10",
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/**",