mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 01:07:31 +08:00
Addresses review findings: - Remove insecure automatic encrypted-file credential-store fallback. now only uses real OS/keyctl-backed stores, or remains unavailable. Headless users must use explicit HERMES_KEYSTORE_PASSPHRASE if desired. - Add shared wallet runtime so tools/CLI/approval use the same configured providers and persisted policy state. - Inject keystore-backed secrets into gateway/headless startup too, so migrated .env stubs don't break messaging deployments. - Persist wallet policy state (freeze, daily totals, rate-limit timestamps, cooldown timestamps) across invocations. - Persist transaction history to disk across invocations. - Make owner-approved sends execute through the same runtime/policy path and record policy state after successful approved sends. - Fix wallet export by allowing explicit CLI export reads of sealed keys via dedicated requester path () instead of generic CLI reads. - Make CLI wallet sends evaluate policy before execution and honor freeze. - Align docs with actual crypto primitive (XSalsa20-Poly1305 via SecretBox) and current policy-config scope. Validation: - 129 tests passing - freeze persistence verified manually - wallet export verified manually
98 lines
3.0 KiB
Python
98 lines
3.0 KiB
Python
"""Shared wallet runtime.
|
|
|
|
Provides a single configured WalletManager + PolicyEngine per process so all
|
|
entry points (CLI, tools, approvals, gateway) share the same provider setup,
|
|
policy configuration, and persisted state.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional, Tuple
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_runtime: Optional[tuple] = None
|
|
|
|
|
|
def _hermes_home() -> Path:
|
|
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
|
|
|
|
|
def _wallet_state_dir() -> Path:
|
|
p = _hermes_home() / "wallet"
|
|
p.mkdir(parents=True, exist_ok=True)
|
|
return p
|
|
|
|
|
|
def _load_wallet_config() -> dict:
|
|
try:
|
|
from hermes_cli.config import load_config
|
|
cfg = load_config() or {}
|
|
return cfg.get("wallet", {}) or {}
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def _policy_overrides_from_config(wallet_cfg: dict) -> dict:
|
|
# Documented config is not yet fully per-wallet. Support a minimal global map now.
|
|
overrides = wallet_cfg.get("policies", {}) or {}
|
|
agent_cfg = wallet_cfg.get("agent_wallet", {}) or {}
|
|
# Map a few friendly config keys into policy override shape.
|
|
mapped = dict(overrides)
|
|
if agent_cfg.get("max_per_tx_native") is not None:
|
|
mapped.setdefault("spending_limit", {})["max_native"] = str(agent_cfg["max_per_tx_native"])
|
|
if agent_cfg.get("daily_limit_native") is not None:
|
|
mapped.setdefault("daily_limit", {})["max_native"] = str(agent_cfg["daily_limit_native"])
|
|
if agent_cfg.get("auto_approve_below_native") is not None:
|
|
mapped.setdefault("require_approval", {})["above_native"] = str(agent_cfg["auto_approve_below_native"])
|
|
return mapped
|
|
|
|
|
|
def get_runtime():
|
|
global _runtime
|
|
if _runtime is not None:
|
|
return _runtime
|
|
|
|
from keystore.client import get_keystore
|
|
from wallet.manager import WalletManager
|
|
from wallet.policy import PolicyEngine
|
|
|
|
ks = get_keystore()
|
|
if not ks.is_unlocked:
|
|
return None, None
|
|
|
|
mgr = WalletManager(ks, state_dir=_wallet_state_dir())
|
|
wallet_cfg = _load_wallet_config()
|
|
policy = PolicyEngine(
|
|
policies=_policy_overrides_from_config(wallet_cfg),
|
|
state_path=_wallet_state_dir() / "policy_state.json",
|
|
)
|
|
|
|
rpc_overrides = wallet_cfg.get("rpc_endpoints", {}) or {}
|
|
|
|
try:
|
|
from wallet.chains.evm import EVMProvider, EVM_CHAINS
|
|
for chain_id, config in EVM_CHAINS.items():
|
|
mgr.register_provider(chain_id, EVMProvider(config, rpc_url_override=rpc_overrides.get(chain_id, "")))
|
|
except ImportError:
|
|
pass
|
|
|
|
try:
|
|
from wallet.chains.solana import SolanaProvider, SOLANA_CHAINS
|
|
for chain_id, config in SOLANA_CHAINS.items():
|
|
mgr.register_provider(chain_id, SolanaProvider(config, rpc_url_override=rpc_overrides.get(chain_id, "")))
|
|
except ImportError:
|
|
pass
|
|
|
|
_runtime = (mgr, policy)
|
|
return _runtime
|
|
|
|
|
|
def reset_runtime() -> None:
|
|
global _runtime
|
|
_runtime = None
|