@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.
- package/CHANGELOG.md +5 -0
- package/README.md +23 -8
- package/RELEASING.md +143 -144
- package/agent-wallet/README.md +18 -0
- package/agent-wallet/agent_wallet/config.py +11 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +74 -9
- package/agent-wallet/agent_wallet/providers/jupiter.py +171 -2
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/install_agent_wallet.py +1 -0
- package/bin/openclaw-agent-wallet.mjs +229 -5
- package/hermes/plugins/agent_wallet/README.md +54 -0
- package/hermes/plugins/agent_wallet/__init__.py +29 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +7 -0
- package/hermes/plugins/agent_wallet/schemas.py +134 -0
- package/hermes/plugins/agent_wallet/tools.py +433 -0
- package/package.json +4 -1
|
@@ -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.
|
|
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]
|
|
@@ -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
|
|
301
|
-
|
|
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
|
+
)
|