@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.
@@ -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
@@ -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
+ }