mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 09:17:09 +08:00
Addresses follow-up review findings: - Cross-process persistence now uses locked read/modify/write helpers (wallet/file_state.py) instead of load-once/overwrite-whole-file writes. Wallet tx history and policy state refresh from disk and merge updates across CLI/gateway processes. - Hard-block policies now run before require_approval. User wallets can no longer bypass spending limits, blocklists, daily caps, or cooldowns just by requesting owner approval. - Duplicate wallets for the same chain/address are rejected on create/import. delete_wallet() now removes key material only when no remaining metadata references that address. - Wallet export remains explicit via cli_export requester. - Keystore docs/code now consistently describe SecretBox as XSalsa20-Poly1305. Regression coverage added for: - no insecure credential-store fallback - tx history merge across manager instances - policy state merge across engine instances - user-wallet hard-block precedence over require_approval - duplicate-wallet rejection and shared-key deletion safety Validation: 134 targeted tests passing
59 lines
1.6 KiB
Python
59 lines
1.6 KiB
Python
"""Small helpers for cross-process JSON state persistence.
|
|
|
|
Provides a minimal lock + read/modify/write utility for wallet state files.
|
|
Uses ``fcntl.flock`` on POSIX and gracefully degrades to best-effort writes on
|
|
platforms without flock support.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Callable, TypeVar
|
|
|
|
T = TypeVar("T")
|
|
|
|
try:
|
|
import fcntl # type: ignore
|
|
_HAS_FCNTL = True
|
|
except ImportError: # pragma: no cover - Windows fallback
|
|
_HAS_FCNTL = False
|
|
|
|
|
|
@contextmanager
|
|
def locked_file(path: Path):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(path, "a+b") as f:
|
|
if _HAS_FCNTL:
|
|
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
try:
|
|
yield f
|
|
finally:
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
if _HAS_FCNTL:
|
|
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
|
|
|
|
def read_json(path: Path, default):
|
|
if not path.exists():
|
|
return default
|
|
try:
|
|
return json.loads(path.read_text())
|
|
except Exception:
|
|
return default
|
|
|
|
|
|
def update_json(path: Path, default, merge_fn: Callable[[object], T]) -> T:
|
|
"""Lock, load current JSON, compute new state, atomically replace file."""
|
|
lock_path = path.with_suffix(path.suffix + ".lock")
|
|
with locked_file(lock_path):
|
|
current = read_json(path, default)
|
|
new_state = merge_fn(current)
|
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
tmp.write_text(json.dumps(new_state, indent=2))
|
|
os.replace(tmp, path)
|
|
return new_state
|