@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.
@@ -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.9"
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
  ]
@@ -19,6 +19,7 @@ function printHelp() {
19
19
 
20
20
  Usage:
21
21
  openclaw-agent-wallet install [options]
22
+ openclaw-agent-wallet hermes install [options]
22
23
  openclaw-agent-wallet update [options]
23
24
  openclaw-agent-wallet status
24
25
  openclaw-agent-wallet rollback [--to <version>]
@@ -33,6 +34,7 @@ Common install options:
33
34
 
34
35
  Examples:
35
36
  npx @agentlayer.tech/wallet install --yes
37
+ npx @agentlayer.tech/wallet hermes install --yes
36
38
  npx @agentlayer.tech/wallet install --backend none
37
39
  npx @agentlayer.tech/wallet update --yes
38
40
  npx @agentlayer.tech/wallet status
@@ -65,6 +67,10 @@ function resolveRuntimeBase(env = process.env) {
65
67
  return path.join(resolveOpenclawHome(env), "agent-wallet-runtime");
66
68
  }
67
69
 
70
+ function resolveHermesHome(env = process.env) {
71
+ return path.resolve(expandHome(env.HERMES_HOME || "~/.hermes"));
72
+ }
73
+
68
74
  function releaseRootFor(version, env = process.env) {
69
75
  return path.join(resolveRuntimeBase(env), "releases", version);
70
76
  }
@@ -73,6 +79,21 @@ function currentRuntimePath(env = process.env) {
73
79
  return path.join(resolveRuntimeBase(env), "current");
74
80
  }
75
81
 
82
+ function resolvedCurrentRuntimeRoot(env = process.env) {
83
+ const currentPath = currentRuntimePath(env);
84
+ const currentTarget = readLinkOrNull(currentPath);
85
+ if (currentTarget) {
86
+ return path.resolve(path.dirname(currentPath), currentTarget);
87
+ }
88
+ try {
89
+ const stat = fs.statSync(currentPath);
90
+ if (stat.isDirectory()) return currentPath;
91
+ } catch (error) {
92
+ if (error?.code !== "ENOENT") throw error;
93
+ }
94
+ return "";
95
+ }
96
+
76
97
  function previousRuntimePath(env = process.env) {
77
98
  return path.join(resolveRuntimeBase(env), "previous");
78
99
  }
@@ -282,6 +303,31 @@ function envFileSet(pathname, updates) {
282
303
  }
283
304
  }
284
305
 
306
+ function envFileUnset(pathname, keys) {
307
+ let lines = [];
308
+ try {
309
+ lines = fs.readFileSync(pathname, "utf8").split(/\r?\n/);
310
+ } catch (error) {
311
+ if (error?.code === "ENOENT") return;
312
+ throw error;
313
+ }
314
+ const blocked = new Set(keys);
315
+ const next = [];
316
+ for (const line of lines) {
317
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
318
+ if (match && blocked.has(match[1])) {
319
+ continue;
320
+ }
321
+ if (line.length > 0) next.push(line);
322
+ }
323
+ fs.writeFileSync(pathname, `${next.join("\n")}\n`, { mode: 0o600 });
324
+ try {
325
+ fs.chmodSync(pathname, 0o600);
326
+ } catch {
327
+ // ignored
328
+ }
329
+ }
330
+
285
331
  function readEnvFile(pathname) {
286
332
  try {
287
333
  const result = {};
@@ -297,13 +343,55 @@ function readEnvFile(pathname) {
297
343
  }
298
344
 
299
345
  function currentBootKey(env = process.env) {
300
- const currentPath = currentRuntimePath(env);
301
- const currentTarget = readLinkOrNull(currentPath);
302
- if (!currentTarget) return "";
303
- const currentRoot = path.resolve(path.dirname(currentPath), currentTarget);
346
+ const currentRoot = resolvedCurrentRuntimeRoot(env);
347
+ if (!currentRoot) return "";
304
348
  return readEnvFile(path.join(currentRoot, "agent-wallet", ".env")).AGENT_WALLET_BOOT_KEY || "";
305
349
  }
306
350
 
351
+ function readTextIfExists(pathname) {
352
+ try {
353
+ return fs.readFileSync(pathname, "utf8");
354
+ } catch (error) {
355
+ if (error?.code === "ENOENT") return "";
356
+ throw error;
357
+ }
358
+ }
359
+
360
+ function writeSecretFile(pathname, value) {
361
+ fs.mkdirSync(path.dirname(pathname), { recursive: true });
362
+ fs.writeFileSync(pathname, `${String(value || "").trim()}\n`, { mode: 0o600 });
363
+ try {
364
+ fs.chmodSync(pathname, 0o600);
365
+ } catch {
366
+ // ignored
367
+ }
368
+ }
369
+
370
+ function resolveBootKeyFromFile(env = process.env) {
371
+ const keyFile = String(env.AGENT_WALLET_BOOT_KEY_FILE || "").trim();
372
+ if (!keyFile) return "";
373
+ return readTextIfExists(path.resolve(expandHome(keyFile))).trim();
374
+ }
375
+
376
+ function defaultBootKeyFile(env = process.env) {
377
+ return path.join(resolveRuntimeBase(env), "boot-key");
378
+ }
379
+
380
+ function ensureBootKeyFile(env = process.env) {
381
+ const configuredFile = String(env.AGENT_WALLET_BOOT_KEY_FILE || "").trim();
382
+ const keyFile = configuredFile ? path.resolve(expandHome(configuredFile)) : defaultBootKeyFile(env);
383
+ const existing = readTextIfExists(keyFile).trim();
384
+ if (existing) {
385
+ return { path: keyFile, status: "existing" };
386
+ }
387
+ const bootKey = String(env.AGENT_WALLET_BOOT_KEY || "").trim() || resolveBootKeyFromFile(env) || currentBootKey(env);
388
+ if (!bootKey) {
389
+ return { path: keyFile, status: "missing" };
390
+ }
391
+ writeSecretFile(keyFile, bootKey);
392
+ return { path: keyFile, status: "created" };
393
+ }
394
+
307
395
  function runDoctor() {
308
396
  const requiredPaths = [
309
397
  ["setup.sh", setupPath],
@@ -380,7 +468,7 @@ function buildInstallerEnv(args) {
380
468
  const sealedKeysPath = path.join(resolveOpenclawHome(env), "sealed_keys.json");
381
469
  const sealedKeysExist = fs.existsSync(sealedKeysPath);
382
470
  if (!env.AGENT_WALLET_BOOT_KEY) {
383
- const existingBootKey = currentBootKey(env);
471
+ const existingBootKey = resolveBootKeyFromFile(env) || currentBootKey(env);
384
472
  if (existingBootKey) {
385
473
  env.AGENT_WALLET_BOOT_KEY = existingBootKey;
386
474
  }
@@ -523,6 +611,132 @@ function runRollback(args) {
523
611
  return 0;
524
612
  }
525
613
 
614
+ function resolveHermesPluginSource() {
615
+ const currentRoot = resolvedCurrentRuntimeRoot();
616
+ const candidates = [];
617
+ if (currentRoot) {
618
+ candidates.push(path.join(currentRoot, "hermes", "plugins", "agent_wallet"));
619
+ }
620
+ candidates.push(path.join(packageRoot, "hermes", "plugins", "agent_wallet"));
621
+ for (const source of candidates) {
622
+ if (fs.existsSync(path.join(source, "plugin.yaml"))) {
623
+ return source;
624
+ }
625
+ }
626
+ throw new Error(`Missing Hermes plugin bundle. Checked: ${candidates.join(", ")}`);
627
+ }
628
+
629
+ function resolveAgentWalletPackageRoot(env = process.env) {
630
+ const currentRoot = resolvedCurrentRuntimeRoot(env);
631
+ if (currentRoot) {
632
+ const runtimePackage = path.join(currentRoot, "agent-wallet");
633
+ if (fs.existsSync(path.join(runtimePackage, "agent_wallet", "__init__.py"))) {
634
+ return runtimePackage;
635
+ }
636
+ }
637
+ return path.join(packageRoot, "agent-wallet");
638
+ }
639
+
640
+ function resolveAgentWalletPython(packageRootPath) {
641
+ for (const candidate of [
642
+ process.env.AGENT_WALLET_PYTHON,
643
+ process.env.OPENCLAW_AGENT_WALLET_PYTHON,
644
+ path.join(packageRootPath, ".venv", "bin", "python"),
645
+ path.join(packageRootPath, ".runtime-venv", "bin", "python"),
646
+ commandPath("python3"),
647
+ ]) {
648
+ if (!candidate) continue;
649
+ if (path.isAbsolute(candidate) && !fs.existsSync(candidate)) continue;
650
+ return candidate;
651
+ }
652
+ return "python3";
653
+ }
654
+
655
+ function runHermesInstall(args) {
656
+ const hermesHome = resolveHermesHome();
657
+ const userPluginsDir = path.join(hermesHome, "plugins");
658
+ const pluginSource = resolveHermesPluginSource();
659
+ const pluginTarget = path.join(userPluginsDir, "agent_wallet");
660
+ const force = hasFlag(args, "--force");
661
+ const skipEnable = hasFlag(args, "--skip-enable");
662
+ const hermesBin = commandPath("hermes");
663
+ const agentWalletPackageRoot = resolveAgentWalletPackageRoot();
664
+ const agentWalletPython = resolveAgentWalletPython(agentWalletPackageRoot);
665
+ const hermesEnvPath = path.join(hermesHome, ".env");
666
+ const existingHermesEnv = readEnvFile(hermesEnvPath);
667
+ const bootKeyFile = ensureBootKeyFile({ ...process.env, ...existingHermesEnv });
668
+
669
+ fs.mkdirSync(userPluginsDir, { recursive: true });
670
+ try {
671
+ const existing = fs.lstatSync(pluginTarget);
672
+ if (!existing.isSymbolicLink()) {
673
+ if (!force) {
674
+ throw new Error(`${pluginTarget} exists and is not a symlink. Pass --force to replace it.`);
675
+ }
676
+ fs.rmSync(pluginTarget, { recursive: true, force: true });
677
+ } else {
678
+ fs.unlinkSync(pluginTarget);
679
+ }
680
+ } catch (error) {
681
+ if (error?.code !== "ENOENT") throw error;
682
+ }
683
+ fs.symlinkSync(pluginSource, pluginTarget, "dir");
684
+
685
+ envFileSet(hermesEnvPath, {
686
+ AGENT_WALLET_PACKAGE_ROOT: agentWalletPackageRoot,
687
+ AGENT_WALLET_PYTHON: agentWalletPython,
688
+ AGENT_WALLET_BOOT_KEY_FILE: bootKeyFile.path,
689
+ });
690
+ if (bootKeyFile.status !== "missing") {
691
+ envFileUnset(hermesEnvPath, ["AGENT_WALLET_BOOT_KEY"]);
692
+ }
693
+
694
+ let enable = { attempted: false, ok: false, skipped: skipEnable, error: "" };
695
+ if (!skipEnable) {
696
+ if (!hermesBin) {
697
+ enable = {
698
+ attempted: false,
699
+ ok: false,
700
+ skipped: false,
701
+ error: "Hermes CLI was not found on PATH. Run `hermes plugins enable agent-wallet` after installing Hermes.",
702
+ };
703
+ } else {
704
+ const result = spawnSync(hermesBin, ["plugins", "enable", "agent-wallet"], {
705
+ cwd: packageRoot,
706
+ encoding: "utf8",
707
+ env: { ...process.env, HERMES_HOME: hermesHome },
708
+ });
709
+ enable = {
710
+ attempted: true,
711
+ ok: result.status === 0,
712
+ skipped: false,
713
+ error: result.status === 0 ? "" : (result.stderr || result.stdout || "").trim(),
714
+ };
715
+ }
716
+ }
717
+
718
+ console.log(
719
+ JSON.stringify(
720
+ {
721
+ ok: enable.skipped || enable.ok,
722
+ hermes_home: hermesHome,
723
+ plugin_source: pluginSource,
724
+ plugin_target: pluginTarget,
725
+ env_path: hermesEnvPath,
726
+ agent_wallet_package_root: agentWalletPackageRoot,
727
+ agent_wallet_python: agentWalletPython,
728
+ boot_key_file: bootKeyFile.path,
729
+ boot_key_file_status: bootKeyFile.status,
730
+ hermes_enable: enable,
731
+ restart_required: true,
732
+ },
733
+ null,
734
+ 2,
735
+ ),
736
+ );
737
+ return enable.skipped || enable.ok ? 0 : 1;
738
+ }
739
+
526
740
  const args = process.argv.slice(2);
527
741
  const command = args[0] || "install";
528
742
 
@@ -556,6 +770,16 @@ if (command === "rollback") {
556
770
  process.exit(runRollback(args.slice(1)));
557
771
  }
558
772
 
773
+ if (command === "hermes") {
774
+ const subcommand = args[1] || "install";
775
+ if (subcommand === "install" || subcommand === "setup") {
776
+ process.exit(runHermesInstall(args.slice(2)));
777
+ }
778
+ console.error(`Unknown hermes command: ${subcommand}`);
779
+ console.error("Run `openclaw-agent-wallet hermes install --yes` to connect Hermes Agent.");
780
+ process.exit(2);
781
+ }
782
+
559
783
  if (command.startsWith("-")) {
560
784
  process.exit(runInstall(args, { commandName: "install" }));
561
785
  }
@@ -0,0 +1,54 @@
1
+ # AgentLayer Wallet Hermes Plugin
2
+
3
+ This is a thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend.
4
+
5
+ It intentionally does not copy the OpenClaw TypeScript extension or reimplement wallet policy. Hermes gets three tools:
6
+
7
+ - `agent_wallet_tools` - lists the underlying wallet tools and schemas from the Python adapter without creating or unlocking a wallet.
8
+ - `agent_wallet_invoke` - forwards one tool call to `python -m agent_wallet.openclaw_cli invoke`.
9
+ - `agent_wallet_approve` - issues a short-lived approval token through `python -m agent_wallet.openclaw_cli issue-approval` after explicit user confirmation of the exact preview summary.
10
+
11
+ OpenClaw remains the primary local environment. This plugin only expands the same backend into Hermes.
12
+
13
+ ## Integration Plan
14
+
15
+ 1. Keep wallet behavior and safety policy in `agent-wallet/`.
16
+ 2. Keep OpenClaw as the primary environment and leave `.openclaw/extensions/agent-wallet` unchanged.
17
+ 3. Register a small Hermes bridge instead of one Hermes tool per wallet operation.
18
+ 4. Use discovery from `OpenClawWalletAdapter.list_tools()` so Hermes sees the current backend schemas without duplicated metadata.
19
+ 5. Forward execution to `agent_wallet.openclaw_cli invoke` so config validation, sealed secrets, approval-token checks, and backend dispatch stay authoritative in Python.
20
+ 6. Forward token issuance to `agent_wallet.openclaw_cli issue-approval` so Hermes can complete preview/approve/execute without learning sealed secrets.
21
+ 7. Cache successful Solana swap previews briefly in Hermes and bind the cached payload digest into the approval token. Execute can then reuse the exact Jupiter preview the user approved instead of re-quoting volatile markets.
22
+ 8. Add broader Hermes ergonomics later only where it improves safety, such as an installer wrapper or read-only status command.
23
+
24
+ ## Install
25
+
26
+ Copy or symlink this directory into a Hermes plugin path:
27
+
28
+ ```bash
29
+ mkdir -p ~/.hermes/plugins
30
+ ln -s /absolute/path/to/openclaw_skill/hermes/plugins/agent_wallet ~/.hermes/plugins/agent_wallet
31
+ ```
32
+
33
+ Set the wallet package root if Hermes is not launched from this repository:
34
+
35
+ ```bash
36
+ export AGENT_WALLET_PACKAGE_ROOT=/absolute/path/to/openclaw_skill/agent-wallet
37
+ export AGENT_WALLET_PYTHON=python3
38
+ ```
39
+
40
+ Then enable or reload plugins in Hermes:
41
+
42
+ ```bash
43
+ hermes plugins
44
+ hermes chat
45
+ ```
46
+
47
+ ## Runtime Notes
48
+
49
+ - Secrets must stay in the existing protected runtime path, especially `~/.openclaw/sealed_keys.json`.
50
+ - Do not pass `privateKey`, `masterKey`, or `approvalSecret` through Hermes tool config.
51
+ - Write-capable wallet tools still require preview first and an `approval_token` bound to the exact `confirmation_summary`.
52
+ - Use `agent_wallet_approve` only after the user explicitly confirms the exact operation. Mainnet execute requires `mainnet_confirmed=true`.
53
+ - Use `agent_wallet_tools` before invoking unfamiliar tool names.
54
+ - Solana swap previews are cached at `~/.hermes/agent_wallet_preview_cache.json` with `0600` permissions for a short window. The cache contains unsigned preview payloads only; signing and approval checks remain in `agent-wallet/`.
@@ -0,0 +1,29 @@
1
+ """Hermes Agent plugin bridge for AgentLayer wallet tools."""
2
+
3
+ from .schemas import AGENT_WALLET_APPROVE, AGENT_WALLET_INVOKE, AGENT_WALLET_TOOLS
4
+ from .tools import agent_wallet_approve, agent_wallet_invoke, agent_wallet_tools
5
+
6
+
7
+ def register(ctx):
8
+ """Register a narrow dispatcher instead of duplicating wallet tools."""
9
+ ctx.register_tool(
10
+ name=AGENT_WALLET_TOOLS["name"],
11
+ toolset="agent_wallet",
12
+ schema=AGENT_WALLET_TOOLS,
13
+ handler=agent_wallet_tools,
14
+ description=AGENT_WALLET_TOOLS["description"],
15
+ )
16
+ ctx.register_tool(
17
+ name=AGENT_WALLET_INVOKE["name"],
18
+ toolset="agent_wallet",
19
+ schema=AGENT_WALLET_INVOKE,
20
+ handler=agent_wallet_invoke,
21
+ description=AGENT_WALLET_INVOKE["description"],
22
+ )
23
+ ctx.register_tool(
24
+ name=AGENT_WALLET_APPROVE["name"],
25
+ toolset="agent_wallet",
26
+ schema=AGENT_WALLET_APPROVE,
27
+ handler=agent_wallet_approve,
28
+ description=AGENT_WALLET_APPROVE["description"],
29
+ )
@@ -0,0 +1,7 @@
1
+ name: agent-wallet
2
+ version: 0.1.0
3
+ description: Thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend
4
+ provides_tools:
5
+ - agent_wallet_tools
6
+ - agent_wallet_invoke
7
+ - agent_wallet_approve