Tenuo Hermes Agent Integration

Overview

hermes-tenuo is the official Tenuo integration for Hermes Agent. Hermes routes agent-loop tool calls through the plugin before the handler runs, where Tenuo checks the tool name and every argument against a signed, expiring warrant. When a call falls outside the warrant, the handler never runs, and the model gets the reason back as the tool result.

ALLOW  read_file  path=/data/reports/q3.csv
DENY   read_file  path=/opt/private/payroll.csv
       Constraint 'path' not satisfied: value does not match constraint
DENY   terminal   command=ls
       Tool 'terminal' is not authorized

The plugin is listed in the Hermes plugin catalog as hermes-tenuo. Keys and decisions stay on your machine unless you connect a control plane.

Hermes feature What the warrant gives you
Cron and scheduled jobs A TTL that matches the job window. The job cannot act after it should be done.
delegate_task subagents The child gets only what the parent granted, verified as a chain.
Multi-user gateways One warrant per session, cleared when the session ends.
Kanban workers A per-task warrant. A denial blocks the task on the board.
Fleets Pin the warrant and trust anchor in /etc/hermes/config.yaml so users cannot loosen them.

See it first

No Hermes install, no API key:

uvx hermes-tenuo demo

It prints the allow and deny decisions for a cron job, a subagent handoff, and two gateway users.


Installation

Requires Hermes Agent 0.20 or newer.

hermes plugins install hermes-tenuo
hermes plugins enable hermes-tenuo

This is the supported installation path. install shows the catalog entry and its disclosure, clones the reviewed commit, and asks for TENUO_WARRANT and TENUO_SIGNING_KEY. You create them in the next step, so leave both empty and continue. enable activates the plugin and installs its tenuo dependency into the Hermes runtime.

For direct package installation, custom Hermes environments, or unreleased builds, follow the installation guidance in the hermes-tenuo repository.


Quick Start

1. Mint a warrant

uvx hermes-tenuo mint --ttl 1h \
  --allow read_file:path=/data \
  --allow web_search

This generates a key pair and a warrant, and prints the config block to paste:

# ~/.hermes/config.yaml
plugins:
  enabled:
    - hermes-tenuo
  entries:
    hermes-tenuo:
      warrant: <base64>            # or a path to a .warrant file
      trusted_root: <base64>       # the public key that signed it
      signing_key_env: TENUO_SIGNING_KEY

Put the printed TENUO_SIGNING_KEY in ~/.hermes/.env, or export it before you start Hermes. Keep it out of config.yaml.

2. Check the wiring

hermes plugins doctor hermes-tenuo   # Hermes loads the plugin and its hooks
uvx hermes-tenuo doctor              # config, warrant, expiry, signing key

The second command reads the signing key from your shell, so export it first if it only lives in ~/.hermes/.env.

3. Run Hermes

hermes

Ask the agent to read a file outside /data. The call is denied, and the agent tells you why.


Scoping arguments

Each --allow names a tool and, optionally, constraints on its arguments. A tool with no constraints is allowed with any arguments. A tool not named in the warrant is denied.

Syntax Meaning Example
tool any arguments --allow web_search
tool:arg=/path that path or under it (traversal-safe) --allow read_file:path=/data
tool:arg=glob* matches the glob --allow web_search:query=acme*
tool:arg=a\|b\|c one of the choices --allow git:action=status\|diff\|log
tool:arg=value exact match --allow write_file:mode=w
tool:a=..,b=.. several constraints on one tool --allow write_file:path=/tmp/out,mode=w

For numeric ranges and the rest of the constraint types, mint in Python:

from tenuo import SigningKey, Warrant, Subpath, Range

control_key = SigningKey.generate()   # its public key is trusted_root
agent_key = SigningKey.generate()     # its secret is TENUO_SIGNING_KEY

warrant = (
    Warrant.mint_builder()
    .holder(agent_key.public_key)
    .capability("read_file", path=Subpath("/data"))
    .capability("scale_cluster", replicas=Range.max_value(10))
    .ttl(3600)
    .mint(control_key)
)

The hermes-tenuo README shows how to write that warrant to a file and wire it in.


Audit log

Every decision is appended to $HERMES_HOME/tenuo/audit.jsonl. No account needed.

uvx hermes-tenuo audit --last 20
uvx hermes-tenuo audit --denied

Not sure what to allow yet? Set on_denial: log under plugins.entries.hermes-tenuo. Every call is still checked and recorded, but nothing is blocked. Run the agent, read the denied lines, tighten the warrant, then remove the setting.


How it works

Hermes hook What the plugin does
pre_tool_call Verifies the tool name and arguments against the session’s warrant. On denial it blocks the call and returns the reason as the tool result.
post_tool_call Records timing and writes the audit record.
subagent_start Hands the child warrant to the new delegate_task session.
on_session_end Clears the session’s warrant, so gateway users never share one.

The check runs in Tenuo’s Rust core: signature, expiry, holder proof-of-possession, and every argument constraint. The plugin trusts the issuer’s public key; the issuer’s private key never enters Hermes. The holder signing key remains local to Hermes for proof-of-possession and delegated warrants.

A plugin with a configured warrant that is missing, empty, or fails to load blocks every call. It does not fall back to allowing them.

Coverage

Hermes routes agent-loop tool calls through pre_tool_call, including tools handled before the tool registry (todo, memory, session_search, delegate_task) and tool calls made from inside execute_code scripts.

Direct dispatch by another plugin through ctx.dispatch_tool() is outside this hook, so install trusted plugins alongside it. Code that an execute_code script runs on its own, such as a subprocess, belongs to the terminal sandbox boundary; use a container backend (Docker, Modal, Daytona) to isolate those effects.


Configuration reference

All keys live under plugins.entries.hermes-tenuo in ~/.hermes/config.yaml.

Key Env Meaning
warrant TENUO_WARRANT Base64 warrant, or a path to a file containing one. Required for enforcement.
trusted_root TENUO_TRUSTED_ROOT Base64 public key of the issuer. Warrants signed by anything else are rejected.
signing_key_env   Name of the env var holding the agent’s signing key. Default TENUO_SIGNING_KEY.
child_warrant TENUO_CHILD_WARRANT Warrant handed to delegate_task children.
on_denial   block (default) or log.
audit_log TENUO_AUDIT_LOG Path of the audit log, or false to disable it.

Without a warrant, the plugin loads, logs a warning, and enforces nothing. doctor reports that.


Connecting a control plane

Everything above runs from files on one machine. Set TENUO_CONNECT_TOKEN and the plugin streams every decision to a Tenuo control plane. That adds revocation before a warrant expires, central issuance and key rotation, human approval for sensitive tools, and one audit trail across agents. See Going to production.


More