Skip to main content

Tamper-evident memory audit log for Hermes Agent. Hash-chained, HMAC-sealed. Plain plugin observer that forwards every memory mutation to the psy-core audit chain.

Project description

psy-core-hermes

Tamper-evident memory audit log for your Hermes agent. Every memory write — to MEMORY.md, USER.md, or skills — hash-chained and HMAC-sealed. One pip install.

PyPI Python License

psy-core-hermes is the Hermes Agent companion to psy-core. It registers as a plain Hermes plugin, observes every memory mutation, and forwards each one to a long-lived psy ingest subprocess that writes the canonical hash-chained, HMAC-sealed audit log to ~/.psy/audit.db.

Install

pip install psy-core-hermes
psy-core-hermes init --actor-id you@example.com

This adds a plugins.psy block to ~/.hermes/config.yaml. Then run Hermes as usual; the plugin loads via Hermes's hermes_agent.plugins entry-point group.

If psy is not on your PATH, the plugin falls back to npx -y psy-core@<exact-version> psy ingest. Most modern dev machines already have Node.js, so this Just Works; if you want to skip the npx round-trip, install psy-core globally with npm i -g psy-core.

Versioning note

psy-core-hermes has its own PyPI version. The initial adapter release line is 0.1.x. The psy_core_version config below is the npm psy-core version used for the Node psy ingest fallback, not the Python package version.

What gets captured

Every memory mutation Hermes exposes, mapped to psy-core's canonical operation vocabulary.

psy operation Hermes source Captured via
create memory tool with action: "add" pre_tool_call (intent) + filesystem watcher (result)
str_replace memory tool with action: "replace" pre_tool_call + filesystem watcher
delete memory tool with action: "remove" pre_tool_call + filesystem watcher
create skill_manage with action: "create" / "write_file" pre_tool_call + post_tool_call
str_replace skill_manage with action: "edit" / "patch" pre_tool_call + post_tool_call
delete skill_manage with action: "delete" / "remove_file" pre_tool_call + post_tool_call

Every mutation produces a paired intent + result row, the same shape as every other psy-core adapter.

Hermes memory surface — what's captured and what's not

Hermes has more than one kind of memory. The initial adapter scope deliberately covers the file-backed memory tool plus skill_manage, and explicitly stays out of the way of everything else. The boundary is pinned by tests at tests/test_real_hermes.py.

# Surface Hookable? Captured?
1 memory tool — add/replace/remove × {memory,user} writing MEMORY.md / USER.md ✅ via pre_tool_call + filesystem watcher Captured
2 skill_manage tool — SKILL.md + sub-files ✅ via pre_tool_call + post_tool_call Captured
3 MemoryProvider plugins — Honcho, Mem0, Hindsight, Byterover, Holographic, OpenViking, RetainDB, Supermemory; each exposes its own write tools (honcho_conclude, mem0_conclude, hindsight_retain, fact_store, viking_remember, retaindb_remember/ingest_file, supermemory_store, brv_curate, …) ✅ via pre_tool_call (verified at run_agent.py:9051's _invoke_tool block — the hook fires before memory_manager.handle_tool_call) Not captured in the initial scope. MemoryProvider write-tool capture is the largest likely future expansion; turn-on is one allowlist edit.
4 MemoryProvider lifecycle hooks (sync_turn, on_turn_start, on_session_end, on_pre_compress, on_memory_write, on_delegation) Subclass-only — MemoryManager.add_provider is single-select, so subclassing locks the user out of running Honcho/Mem0/Hindsight alongside psy Out of scope (architectural — would require psy-core-hermes to BE the user's MemoryProvider, which the plan explicitly rejected)
5 session_search (read-only SessionDB query) ✅ via pre_tool_call (in _AGENT_LOOP_TOOLS, no post) Not captured (read-only)
6 todo tool ✅ via pre_tool_call (in _AGENT_LOOP_TOOLS) Not captured (not memory)
7 SessionDB writes (cross-session summaries) ❌ no upstream hook Out of scope (would need an upstream PR)
8 Trajectory JSONL writes ❌ no upstream hook Out of scope (would need an upstream PR)
9 flush_memories() auxiliary writes ❌ no upstream hook Out of scope (would need an upstream PR)
10 Gateway transport events Separate gateway/hooks.py registry Separate adapter scope

Note on #3 ↔ #1: at run_agent.py:9098, when the file-backed memory tool runs, Hermes also calls memory_manager.on_memory_write(...) so any active external MemoryProvider can mirror the write semantically. That makes psy-core-hermes (audit) and Honcho/Mem0 (semantic recall) complementary observers of the same write, not competing writers — they're additive.

If you need Mem0/Letta/LangChain memory audited at the API level (rather than via Hermes's tool dispatch), psy-core ships dedicated adapters for those frameworks; see the adapter table in the root README.

Identity

actor_id is required unless allow_anonymous: true. Audit events must attribute the session to a principal. When actor_id is missing the plugin emits the F4 error template at session start and refuses to register hooks:

psy-core-hermes: actor_id is required.
  Why:    audit events must attribute the session to a principal.
  Where:  ~/.hermes/config.yaml -> plugins.psy.actor_id
  Example:
    plugins:
      psy:
        actor_id: alice@acme.com
  Bypass: set allow_anonymous: true (not recommended in production).
  Docs:   https://github.com/jethros-projects/psy-core/blob/main/python/psy-core-hermes/README.md#identity

Configuration

# ~/.hermes/config.yaml
plugins:
  enabled:
    - psy

  psy:
    enabled: true
    actor_id: alice@acme.com         # REQUIRED unless allow_anonymous: true
    tenant_id: acme                  # optional
    purpose: production-debug        # optional

    db_path: ~/.psy/audit.db         # optional; default <HERMES_HOME>/psy/audit.db
    seal_key_path: ~/.psy/seal-key   # optional
    memories_dir: ~/.hermes/memories # filesystem-watched dir

    psy_core_version: 0.4.0          # npm psy-core pin (used for npx fallback)
    psy_binary: null                 # optional override

    redactor: default                # default | none | "<dotted_path>"
    payload_capture: true            # capture memory content (with redaction)
    dry_run: false
    log_level: info
    allow_anonymous: false
    schema_version_pin: "1.0.0"

Console scripts

psy-core-hermes init [--actor-id NAME]   # idempotent config block insertion
psy-core-hermes doctor                   # config + paths + subprocess handshake test
psy-core-hermes status                   # one-line summary
psy-core-hermes dry-run < envelopes.jsonl  # emit envelopes locally; never spawn ingest
psy-core-hermes skill-stats              # skill-quality report from the audit chain

Skill-quality reporting (skill-stats)

The audit chain isn't just a write log — its tamper-evident ordering makes it a uniquely good signal for outcome attribution on skills. psy-core-hermes skill-stats reads the chain and reports per-skill churn, rapid-patch counts, and false-start patterns:

$ psy-core-hermes skill-stats
SKILL                            CREATE  PATCH  DEL  CHURN  RAPID  STATUS
deploy-runbook                        1      7    0   7.00      5  unstable
flaky-test-recovery                   1      4    0   4.00      4  unstable
release-checklist                     1      0    0   0.00      0  ok

legend:  unstable = churn>=2.0 or 3+ rapid patches  |  short-lived = create+delete within 1 day

Why this matters: Hermes's curator at agent/curator.py:283-285 explicitly tells the model "DO NOT use usage counters as a reason to skip consolidation … 'use=0' is not evidence." The team knows usage counters are insufficient. The audit chain provides the complementary signal — a skill patched 5 times within an hour of creation is probably unstable, no matter how often it's been called. That's a fact the chain can prove via its hash-linked ordering and timestamps.

Useful flags:

psy-core-hermes skill-stats --json                # machine-readable
psy-core-hermes skill-stats --since 7d            # last week only
psy-core-hermes skill-stats --actor alice@acme    # multi-tenant filter
psy-core-hermes skill-stats --top 10              # most-suspect 10 only
psy-core-hermes skill-stats --skill-md-only       # ignore attached files
psy-core-hermes skill-stats --db-path ~/.psy/audit.db  # explicit DB path

The metrics are also available as a Python library function for users who want to feed the signal into their own tools (Hermes curator integration, dashboards, Atropos quality filters):

from datetime import timedelta
from pathlib import Path
from psy_core.hermes.skill_stats import compute_skill_stats

metrics = compute_skill_stats(
    Path.home() / ".psy" / "audit.db",
    actor_id="alice@acme.com",
    since=timedelta(days=7),
)
unstable = [m for m in metrics if m.status == "unstable"]

The DB handle is opened read-only (SQLite mode=ro URI), so the read path provably cannot mutate the chain.

Architecture

┌─────────────────────────────────────────┐         ┌─────────────────────┐
│ Hermes process (Python)                 │         │ psy ingest (Node)   │
│                                         │         │                     │
│  psy_core.hermes.register(ctx)               │         │  Auditor.append()   │
│   ├─ register_hook(pre_tool_call,       │         │  Sealer.writeHead() │
│   │   filter: tool_name in              │  JSONL  │  ↓                  │
│   │   {"memory","skill_manage"})        │  ──────▶│  ~/.psy/audit.db    │
│   └─ register_hook(post_tool_call,      │  stdio  │  ~/.psy/head.json   │
│       filter: tool_name=="skill_manage")│  ◀─ACK  │  ~/.psy/seal-key    │
│                                         │         │                     │
│  Filesystem watcher (watchdog)          │         └─────────────────────┘
│   ├─ ~/.hermes/memories/MEMORY.md       │
│   ├─ ~/.hermes/memories/USER.md         │         Spawn flow:
│   └─ on change: emit confirmed-result   │           1. shutil.which("psy") → use directly
│                                         │           2. else npx -y psy-core@X psy ingest
│  IngestClient (thread-safe queue)       │           3. else error: install psy-core or Node
│   ├─ enqueue(envelope)                  │
│   ├─ background writer thread           │
│   └─ atexit + SIGTERM cleanup           │
└─────────────────────────────────────────┘

Distribution

  • Primary: PyPI as psy-core-hermes. Auto-discovered via hermes_agent.plugins entry-point.
  • Secondary: hermes plugins install jethros-projects/psy-core-hermes (Hermes's own installer; runs git clone --depth 1 against the in-repo path).

Verification

psy verify --all   # full chain integrity check + sealed-tail verify

The TS-side psy verify reads ~/.psy/audit.db, walks the hash chain, validates the HMAC seal, and exits non-zero on any tampering.

Verified against hermes-agent v0.11.0

The plugin contract was source-verified against NousResearch/hermes-agent at v0.11.0:

  • Entry-point group is hermes_agent.plugins; the loader does ep.load() and then getattr(module, "register"), so our entry-point value is the module path psy_core.hermes.register (not the module:attr form — that returns the function from ep.load() and breaks the getattr(module, "register") lookup).
  • _AGENT_LOOP_TOOLS = {"todo", "memory", "session_search", "delegate_task"} — confirmed memory bypasses post_tool_call; we use the filesystem watcher to confirm result envelopes for memory writes.
  • memory tool args: {action, target, content, old_text} where target ∈ {"memory", "user"} maps to MEMORY.md / USER.md.
  • skill_manage args: {action, name, content?, old_string?, new_string?, file_path?, file_content?, ...}. Skill key is name; file_path is the optional sub-path under the skill directory.
  • Hook callback signature: keyword-only (*, tool_name, args, task_id, session_id, tool_call_id, **_).
  • hermes_cli.config.load_config() returns the parsed YAML as a dict.

A live integration test that loads our plugin into a real Hermes PluginManager and asserts captured envelopes is part of the e2e workflow at .github/workflows/cross-lang-e2e.yml.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

psy_core_hermes-0.1.1.tar.gz (53.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

psy_core_hermes-0.1.1-py3-none-any.whl (36.3 kB view details)

Uploaded Python 3

File details

Details for the file psy_core_hermes-0.1.1.tar.gz.

File metadata

  • Download URL: psy_core_hermes-0.1.1.tar.gz
  • Upload date:
  • Size: 53.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for psy_core_hermes-0.1.1.tar.gz
Algorithm Hash digest
SHA256 0d9c6bda51ccc0a8a497116f2e99baf9cd9c181a43232fa8a45125834af362f8
MD5 9b70639fc38d296a3264fc0fbcd5c96e
BLAKE2b-256 5499816342b391b09c01d6598ffd02020cdbf44d586973d1143cdb534ef086c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for psy_core_hermes-0.1.1.tar.gz:

Publisher: publish-pypi.yml on jethros-projects/psy-core

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file psy_core_hermes-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for psy_core_hermes-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 144cc64db1533f177f0f12b7a3ec56c91db0e589b20e3051692af3090b9b254f
MD5 20e50bca2fe05a5f98e5995fc2eb82d5
BLAKE2b-256 4374381db3a0c1e01e05b4e805a4758215398a16fc8a2912cfb6c0a7d275f171

See more details on using hashes here.

Provenance

The following attestation bundles were made for psy_core_hermes-0.1.1-py3-none-any.whl:

Publisher: publish-pypi.yml on jethros-projects/psy-core

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page