@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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Added an optional Hermes Agent bridge plugin under `hermes/plugins/agent_wallet`
6
+ that forwards into the existing Python wallet CLI instead of duplicating
7
+ OpenClaw wallet tools or policy.
8
+ - Added `wallet hermes install --yes` and `AGENT_WALLET_BOOT_KEY_FILE` support
9
+ for smoother Hermes onboarding without manual `.env` editing.
5
10
  - Replaced the repo license with `PolyForm Small Business 1.0.0`.
6
11
  - Clarified in `README.md` that individuals can audit, fork, run, and modify
7
12
  the code for themselves, and that company use follows the PolyForm small
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  ![AgentLayer](logo+name.png)
2
-
3
2
  # AgentLayer
4
3
 
4
+ ```bash
5
+ npx @agentlayer.tech/wallet install --yes
6
+ ```
5
7
  AgentLayer is a beta local-first wallet and finance stack for agents.
6
8
 
7
9
  The repository includes:
8
10
 
9
11
  - `agent-wallet/` - the main wallet backend for AgentLayer
10
12
  - `.openclaw/` - the local AgentLayer bridge layer
13
+ - `hermes/` - optional Hermes Agent plugin bridge for the same wallet backend
11
14
  - `wdk-btc-wallet/` - the local Bitcoin wallet service
12
15
  - `wdk-evm-wallet/` - the local EVM wallet service
13
16
  - `provider-gateway/` - shared provider access for Solana RPC, Bags, and related finance reads
@@ -33,12 +36,6 @@ System prerequisites:
33
36
  - `node`
34
37
  - `npm`
35
38
 
36
- Install from the latest GitHub release bundle:
37
-
38
- ```bash
39
- curl -fsSL https://raw.githubusercontent.com/lopushok9/Agent-Layer/main/install-from-github.sh | sh
40
- ```
41
-
42
39
  Install through npm:
43
40
 
44
41
  ```bash
@@ -66,6 +63,7 @@ Useful npm CLI commands:
66
63
  ```bash
67
64
  wallet status
68
65
  wallet doctor
66
+ wallet hermes install --yes
69
67
  wallet update --yes
70
68
  wallet rollback
71
69
  ```
@@ -143,9 +141,25 @@ Without those secrets, the installer still lays down the runtime and installs de
143
141
  }
144
142
  ```
145
143
 
144
+ ## Connect Hermes Agent
145
+
146
+ OpenClaw remains the primary local environment, but the repo also ships an optional Hermes Agent bridge at:
147
+
148
+ ```bash
149
+ hermes/plugins/agent_wallet
150
+ ```
151
+
152
+ It exposes only two Hermes tools: `agent_wallet_tools` for discovery and `agent_wallet_invoke` for forwarding a single call into the existing Python wallet CLI. Install it by symlinking the plugin directory into Hermes:
153
+
154
+ ```bash
155
+ npx @agentlayer.tech/wallet hermes install --yes
156
+ ```
157
+
158
+ That command installs the Hermes plugin, runs `hermes plugins enable agent-wallet`, writes non-secret runtime paths into `~/.hermes/.env`, and points Hermes at a local boot-key file. Secrets stay in the existing protected OpenClaw runtime paths, especially `~/.openclaw/sealed_keys.json`; do not put wallet secrets into Hermes tool config.
159
+
146
160
  ## What you get after install
147
161
 
148
- If you install from GitHub release, the bundle is extracted under:
162
+ If you install through npm, the runtime is extracted under:
149
163
 
150
164
  ```bash
151
165
  ~/.openclaw/agent-wallet-runtime/current
@@ -159,6 +173,7 @@ The installer then does the following:
159
173
  - creates a minimal `~/.openclaw/openclaw.json` if one does not exist
160
174
  - if the required secrets are already present, writes or updates `~/.openclaw/sealed_keys.json`
161
175
  - if the required secrets are already present, patches `~/.openclaw/openclaw.json` to load the `agent-wallet` extension and point it at the installed runtime
176
+ - `wallet hermes install --yes` additionally connects Hermes Agent to the same runtime without copying wallet tools or policy
162
177
 
163
178
  When the installer reaches the final config step, the default plugin config is:
164
179
 
@@ -18,6 +18,7 @@ The package now includes a thin adapter for agent runtimes:
18
18
  - `agent_wallet.plugin_bundle.build_openclaw_plugin_bundle`
19
19
  - `agent_wallet.openclaw_runtime.onboard_openclaw_user_wallet`
20
20
  - `agent_wallet.openclaw_cli` for the official OpenClaw TypeScript plugin bridge
21
+ - `hermes/plugins/agent_wallet` as an optional Hermes Agent bridge to the same CLI
21
22
 
22
23
  It provides:
23
24
 
@@ -27,6 +28,23 @@ It provides:
27
28
  - OpenClaw-style plugin manifest and skill bundle
28
29
  - explicit network-aware results so the host and agent can see `devnet` vs `mainnet`
29
30
 
31
+ ## Hermes integration
32
+
33
+ The optional Hermes plugin is intentionally a bridge, not a port of the OpenClaw plugin. It registers:
34
+
35
+ - `agent_wallet_tools` - read-only discovery for the underlying Python adapter tool specs.
36
+ - `agent_wallet_invoke` - a dispatcher that calls `python -m agent_wallet.openclaw_cli invoke`.
37
+
38
+ Install it with:
39
+
40
+ ```bash
41
+ npx @agentlayer.tech/wallet hermes install --yes
42
+ ```
43
+
44
+ That command symlinks `hermes/plugins/agent_wallet` into `~/.hermes/plugins/agent_wallet`, enables the plugin with `hermes plugins enable agent-wallet`, and writes `AGENT_WALLET_PACKAGE_ROOT`, `AGENT_WALLET_PYTHON`, and `AGENT_WALLET_BOOT_KEY_FILE` into `~/.hermes/.env`. OpenClaw remains the canonical host integration and wallet safety policy remains in Python.
45
+
46
+ Hermes tool config must not contain wallet secrets. Use the existing sealed runtime path and host-issued approval tokens for execute flows. `AGENT_WALLET_BOOT_KEY_FILE` lets OpenClaw and Hermes reference one local boot-key file instead of duplicating the boot key across multiple env files.
47
+
30
48
  Current safe tools:
31
49
 
32
50
  - `get_wallet_capabilities`
@@ -13,6 +13,7 @@ class Settings(BaseSettings):
13
13
  agent_wallet_backend: str = "none"
14
14
  agent_wallet_sign_only: bool = False
15
15
  agent_wallet_boot_key: str = ""
16
+ agent_wallet_boot_key_file: str = ""
16
17
  agent_wallet_approval_ttl_seconds: int = 600
17
18
  agent_wallet_per_user_key_derivation: bool = True
18
19
  agent_wallet_encrypt_user_wallets: bool = True
@@ -297,7 +298,16 @@ def _env_bool(name: str, default: bool) -> bool:
297
298
 
298
299
  def resolve_boot_key() -> str:
299
300
  """Resolve the boot key used to unlock sealed secrets from disk."""
300
- return os.getenv("AGENT_WALLET_BOOT_KEY", settings.agent_wallet_boot_key).strip()
301
+ direct = os.getenv("AGENT_WALLET_BOOT_KEY", settings.agent_wallet_boot_key).strip()
302
+ if direct:
303
+ return direct
304
+ key_file = os.getenv("AGENT_WALLET_BOOT_KEY_FILE", settings.agent_wallet_boot_key_file).strip()
305
+ if not key_file:
306
+ return ""
307
+ try:
308
+ return Path(key_file).expanduser().read_text(encoding="utf-8").strip()
309
+ except OSError:
310
+ return ""
301
311
 
302
312
 
303
313
  def _reject_legacy_runtime_secret_env(var_name: str) -> None:
@@ -11,6 +11,14 @@ from agent_wallet.models import AgentToolResult, AgentToolSpec
11
11
  from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendError
12
12
 
13
13
 
14
+ def _canonical_json_text(payload: dict[str, Any]) -> str:
15
+ return json.dumps(payload, sort_keys=True, separators=(",", ":"))
16
+
17
+
18
+ def preview_payload_digest(preview: dict[str, Any]) -> str:
19
+ return hashlib.sha256(_canonical_json_text(preview).encode("utf-8")).hexdigest()
20
+
21
+
14
22
  WALLET_RUNTIME_INSTRUCTIONS = """
15
23
  Use wallet tools only when the user explicitly asks for wallet-related actions.
16
24
  Treat any signing request as sensitive.
@@ -4398,19 +4406,76 @@ class OpenClawWalletAdapter:
4398
4406
  ),
4399
4407
  )
4400
4408
 
4401
- execute_preview = await self.backend.preview_swap(
4402
- input_mint=input_mint.strip(),
4403
- output_mint=output_mint.strip(),
4404
- amount_ui=float(amount),
4405
- slippage_bps=slippage_bps,
4409
+ approval_payload = inspect_approval_token(
4410
+ approval_token,
4411
+ tool_name=tool_name,
4412
+ network=str(getattr(self.backend, "network", "unknown")),
4413
+ require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
4406
4414
  )
4415
+ approval_summary = approval_payload.get("binding", {}).get("summary")
4416
+ if not isinstance(approval_summary, dict):
4417
+ raise WalletBackendError(
4418
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4419
+ )
4420
+ expected_summary = {
4421
+ "operation": "Swap",
4422
+ "network": str(getattr(self.backend, "network", "unknown")),
4423
+ "input_mint": input_mint.strip(),
4424
+ "output_mint": output_mint.strip(),
4425
+ "slippage_bps": slippage_bps,
4426
+ }
4427
+ for key, expected_value in expected_summary.items():
4428
+ if approval_summary.get(key) != expected_value:
4429
+ raise WalletBackendError(
4430
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4431
+ )
4432
+ try:
4433
+ approved_amount = float(approval_summary.get("input_amount_ui"))
4434
+ except (TypeError, ValueError):
4435
+ raise WalletBackendError(
4436
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4437
+ )
4438
+ if approved_amount != float(amount):
4439
+ raise WalletBackendError(
4440
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4441
+ )
4442
+
4443
+ approval_summary_copy = dict(approval_summary)
4444
+ approved_preview = args.get("_approved_preview")
4445
+ if isinstance(approval_summary_copy.get("_preview_digest"), str):
4446
+ if not isinstance(approved_preview, dict):
4447
+ raise WalletBackendError(
4448
+ "Approved swap preview payload is required for execute mode. Generate a new preview and approval before execute."
4449
+ )
4450
+ if preview_payload_digest(approved_preview) != approval_summary_copy["_preview_digest"]:
4451
+ raise WalletBackendError(
4452
+ "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
4453
+ )
4454
+ preview_summary = self._build_confirmation_summary(
4455
+ action_label="Swap",
4456
+ payload=approved_preview,
4457
+ )
4458
+ summary_without_digest = {
4459
+ key: value
4460
+ for key, value in approval_summary_copy.items()
4461
+ if key != "_preview_digest"
4462
+ }
4463
+ if preview_summary != summary_without_digest:
4464
+ raise WalletBackendError(
4465
+ "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
4466
+ )
4467
+ execute_preview = dict(approved_preview)
4468
+ else:
4469
+ execute_preview = await self.backend.preview_swap(
4470
+ input_mint=input_mint.strip(),
4471
+ output_mint=output_mint.strip(),
4472
+ amount_ui=float(amount),
4473
+ slippage_bps=slippage_bps,
4474
+ )
4407
4475
  self._require_execute_approval(
4408
4476
  approval_token=approval_token,
4409
4477
  tool_name=tool_name,
4410
- summary=self._build_confirmation_summary(
4411
- action_label="Swap",
4412
- payload=execute_preview,
4413
- ),
4478
+ summary=approval_summary_copy,
4414
4479
  action_label="Swap",
4415
4480
  )
4416
4481
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import os
6
7
  from typing import Any
7
8
 
@@ -54,6 +55,38 @@ def _gateway_route_missing(status_code: int, payload: Any) -> bool:
54
55
  return False
55
56
 
56
57
 
58
+ async def _gateway_get(path_suffix: str, *, params: dict[str, Any] | None = None) -> tuple[int, Any]:
59
+ """Make a GET request through provider gateway."""
60
+ client = get_client()
61
+ response = await client.get(
62
+ f"{_gateway_base_url()}/v1/jupiter/swap/{path_suffix}",
63
+ params=params,
64
+ headers=_gateway_headers(),
65
+ )
66
+ if not response.content:
67
+ return response.status_code, {}
68
+ try:
69
+ return response.status_code, response.json()
70
+ except ValueError:
71
+ return response.status_code, response.text[:500]
72
+
73
+
74
+ async def _gateway_post(path_suffix: str, *, body: dict[str, Any]) -> tuple[int, Any]:
75
+ """Make a POST request through provider gateway."""
76
+ client = get_client()
77
+ response = await client.post(
78
+ f"{_gateway_base_url()}/v1/jupiter/swap/{path_suffix}",
79
+ json=body,
80
+ headers={**_gateway_headers(), "Content-Type": "application/json"},
81
+ )
82
+ if not response.content:
83
+ return response.status_code, {}
84
+ try:
85
+ return response.status_code, response.json()
86
+ except ValueError:
87
+ return response.status_code, response.text[:500]
88
+
89
+
57
90
  def _direct_jupiter_enabled() -> bool:
58
91
  return bool(settings.jupiter_api_key.strip())
59
92
 
@@ -187,7 +220,58 @@ async def fetch_quote(
187
220
  only_direct_routes: bool = False,
188
221
  swap_mode: str = "ExactIn",
189
222
  ) -> dict[str, Any]:
190
- """Fetch a Jupiter quote for an exact-in swap."""
223
+ """Fetch a Jupiter quote for an exact-in swap.
224
+
225
+ Tries direct Jupiter API first. On free-tier errors (TOKEN_NOT_TRADABLE,
226
+ NOT_SUPPORTED) falls back to provider gateway when configured.
227
+ """
228
+ # Try direct first
229
+ try:
230
+ return await _fetch_quote_direct(
231
+ input_mint=input_mint,
232
+ output_mint=output_mint,
233
+ amount_raw=amount_raw,
234
+ slippage_bps=slippage_bps,
235
+ restrict_intermediate_tokens=restrict_intermediate_tokens,
236
+ only_direct_routes=only_direct_routes,
237
+ swap_mode=swap_mode,
238
+ )
239
+ except ProviderError as exc:
240
+ error_msg = str(exc).lower()
241
+ # Only fall back for known free-tier limitations
242
+ gateway_fallback_errors = (
243
+ "not tradable",
244
+ "token_not_tradable",
245
+ "not supported",
246
+ "restrict_intermediate_tokens",
247
+ )
248
+ if not any(phrase in error_msg for phrase in gateway_fallback_errors):
249
+ raise
250
+ if not _gateway_enabled():
251
+ raise
252
+ # Retry via gateway with relaxed restrictions
253
+ return await _fetch_quote_via_gateway(
254
+ input_mint=input_mint,
255
+ output_mint=output_mint,
256
+ amount_raw=amount_raw,
257
+ slippage_bps=slippage_bps,
258
+ restrict_intermediate_tokens=False,
259
+ only_direct_routes=only_direct_routes,
260
+ swap_mode=swap_mode,
261
+ )
262
+
263
+
264
+ async def _fetch_quote_direct(
265
+ *,
266
+ input_mint: str,
267
+ output_mint: str,
268
+ amount_raw: int,
269
+ slippage_bps: int = 50,
270
+ restrict_intermediate_tokens: bool = True,
271
+ only_direct_routes: bool = False,
272
+ swap_mode: str = "ExactIn",
273
+ ) -> dict[str, Any]:
274
+ """Fetch a Jupiter quote directly from Jupiter API."""
191
275
  client = get_client()
192
276
  params = {
193
277
  "inputMint": input_mint,
@@ -211,6 +295,38 @@ async def fetch_quote(
211
295
  return data
212
296
 
213
297
 
298
+ async def _fetch_quote_via_gateway(
299
+ *,
300
+ input_mint: str,
301
+ output_mint: str,
302
+ amount_raw: int,
303
+ slippage_bps: int = 50,
304
+ restrict_intermediate_tokens: bool = False,
305
+ only_direct_routes: bool = False,
306
+ swap_mode: str = "ExactIn",
307
+ ) -> dict[str, Any]:
308
+ """Fetch a Jupiter quote via provider gateway (uses API key)."""
309
+ params: dict[str, Any] = {
310
+ "inputMint": input_mint,
311
+ "outputMint": output_mint,
312
+ "amount": str(amount_raw),
313
+ "slippageBps": str(slippage_bps),
314
+ "swapMode": swap_mode,
315
+ }
316
+ if only_direct_routes:
317
+ params["onlyDirectRoutes"] = "true"
318
+
319
+ status_code, payload = await _gateway_get("quote", params=params)
320
+ if status_code != 200:
321
+ error_msg = payload if isinstance(payload, str) else json.dumps(payload)
322
+ raise ProviderError("jupiter-gateway", f"HTTP {status_code}: {error_msg}")
323
+ if isinstance(payload, dict) and payload.get("errorCode"):
324
+ raise ProviderError("jupiter-gateway", str(payload.get("error") or payload.get("errorCode")))
325
+ if not isinstance(payload, dict) or "outAmount" not in payload:
326
+ raise ProviderError("jupiter-gateway", "Unexpected quote response from gateway.")
327
+ return payload
328
+
329
+
214
330
  async def fetch_ultra_order(
215
331
  *,
216
332
  input_mint: str,
@@ -257,7 +373,35 @@ async def build_swap_transaction(
257
373
  quote_response: dict[str, Any],
258
374
  wrap_and_unwrap_sol: bool = True,
259
375
  ) -> dict[str, Any]:
260
- """Build a serialized swap transaction from a Jupiter quote."""
376
+ """Build a serialized swap transaction from a Jupiter quote.
377
+
378
+ Tries direct Jupiter API first. Falls back to provider gateway on error.
379
+ """
380
+ # Try direct first
381
+ try:
382
+ return await _build_swap_direct(
383
+ user_public_key=user_public_key,
384
+ quote_response=quote_response,
385
+ wrap_and_unwrap_sol=wrap_and_unwrap_sol,
386
+ )
387
+ except ProviderError as exc:
388
+ if not _gateway_enabled():
389
+ raise
390
+ # Fall back to gateway
391
+ return await _build_swap_via_gateway(
392
+ user_public_key=user_public_key,
393
+ quote_response=quote_response,
394
+ wrap_and_unwrap_sol=wrap_and_unwrap_sol,
395
+ )
396
+
397
+
398
+ async def _build_swap_direct(
399
+ *,
400
+ user_public_key: str,
401
+ quote_response: dict[str, Any],
402
+ wrap_and_unwrap_sol: bool = True,
403
+ ) -> dict[str, Any]:
404
+ """Build a swap transaction directly via Jupiter API."""
261
405
  client = get_client()
262
406
  body = {
263
407
  "userPublicKey": user_public_key,
@@ -279,6 +423,31 @@ async def build_swap_transaction(
279
423
  return data
280
424
 
281
425
 
426
+ async def _build_swap_via_gateway(
427
+ *,
428
+ user_public_key: str,
429
+ quote_response: dict[str, Any],
430
+ wrap_and_unwrap_sol: bool = True,
431
+ ) -> dict[str, Any]:
432
+ """Build a swap transaction via provider gateway (uses API key)."""
433
+ body = {
434
+ "userPublicKey": user_public_key,
435
+ "quoteResponse": quote_response,
436
+ "wrapAndUnwrapSol": wrap_and_unwrap_sol,
437
+ "dynamicComputeUnitLimit": True,
438
+ "prioritizationFeeLamports": "auto",
439
+ }
440
+ status_code, payload = await _gateway_post("swap", body=body)
441
+ if status_code != 200:
442
+ error_msg = payload if isinstance(payload, str) else json.dumps(payload)
443
+ raise ProviderError("jupiter-gateway", f"HTTP {status_code}: {error_msg}")
444
+ if isinstance(payload, dict) and payload.get("errorCode"):
445
+ raise ProviderError("jupiter-gateway", str(payload.get("error") or payload.get("errorCode")))
446
+ if not isinstance(payload, dict) or "swapTransaction" not in payload:
447
+ raise ProviderError("jupiter-gateway", "Unexpected swap response from gateway.")
448
+ return payload
449
+
450
+
282
451
  async def execute_ultra_order(
283
452
  *,
284
453
  signed_transaction_base64: str,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.10"
7
+ version = "0.1.11"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -14,6 +14,7 @@ dependencies = [
14
14
  "pynacl>=1.5.0",
15
15
  "python-dotenv>=1.0.0",
16
16
  "solana>=0.36.0",
17
+ "solders>=0.27.0",
17
18
  ]
18
19
 
19
20
  [project.optional-dependencies]
@@ -26,6 +26,7 @@ INCLUDED_RUNTIME_TOP_LEVEL_DIRS = [
26
26
  ".openclaw",
27
27
  "agent-wallet",
28
28
  "agent-a2a-gateway",
29
+ "hermes",
29
30
  "wdk-btc-wallet",
30
31
  "wdk-evm-wallet",
31
32
  ]