Observed since 20 Oct, 22:06 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
agentguard-hermes-plugin
AgentGuard security monitoring plugin for Hermes Agent
AgentGuard security monitoring plugin for Hermes Agent.
Forwards Hermes session, LLM, and tool-call events to an AgentGuard Timeplus stream for real-time observability, and synchronously blocks risky tool calls based on rule decisions from the AgentGuard server.
Two paths run depending on the hook type:
For every hook except pre_tool_call, the plugin POSTs the normalized event to Timeplus via urllib and the callback returns immediately — Hermes never waits.
pre_tool_call only)When AGENTGUARD_HOLDS_ENABLED=true, pre_tool_call:
/api/holds endpoint synchronously via urllib.request.urlopen(timeout=600). The endpoint ingests the event itself, so the plugin does NOT also call its async ingest path for this hook.block_policy:
allow (no rule fired, or rule policy is log_only) → returned immediatelyblock (rule policy is auto_block) → returned immediately with reasonhold (rule policy is hold) → opens a hold record, long-polls for human Approve/Deny in the AgentGuard UI (up to 5 minutes), returns the human's decision{"action": "block", "message": "..."} from the callback on block (Hermes short-circuits the tool call), or None on allow (Hermes runs the tool normally).The agent's hot path is paused for the duration of the hold — no LLM tokens are burned waiting for human review.
If AGENTGUARD_HOLDS_ENABLED is false, pre_tool_call falls back to the async observation path and never blocks. Useful for toggling blocking on/off during testing without uninstalling the plugin.
Hermes timeout note: unlike Claude Code (600s hook cap) and OpenClaw (configurable approval timeout), Hermes provides no hook timeout and no abort signal. The plugin self-imposes
urllib.request.urlopen(timeout=600)so a stuck/api/holdsrequest can't hang Hermes forever. The backend's hold-timeout ceiling is 540 s, leaving 60 s of margin under the plugin timeout.
pip install agentguard-hermes-plugin
agentguard-hermes install
This copies the plugin into ~/.hermes/plugins/agentguard/ where Hermes auto-loads it on startup.
Set environment variables before starting Hermes:
| Variable | Default | Description |
|---|---|---|
AGENTGUARD_TIMEPLUS_URL | http://localhost:3218 | Timeplus base URL. timeplusd HTTP port (3218) in timeplusd mode; the web console / REST API base (port 8000, or its HTTPS URL behind an ingress) in rest mode |
AGENTGUARD_INGEST_API | timeplusd | timeplusd = POST straight to timeplusd; rest = the Timeplus REST API (/{workspace}/api/v1beta2/streams/{stream}/ingest) for deployments where port 3218 is not exposed. Same username/password (Basic auth) in both modes |
AGENTGUARD_WORKSPACE | default | REST API workspace id in the URL path. Rarely needed |
AGENTGUARD_DATABASE | "ag" | Timeplus database owning the AgentGuard streams. Matches the AgentGuard Timeplus app's db_name: ag. |
AGENTGUARD_USERNAME | proton | Timeplus username |
AGENTGUARD_PASSWORD | (empty) | Timeplus password |
AGENTGUARD_STREAM | agentguard_hook_events | Target Timeplus stream |
| Variable | Default | Description |
|---|---|---|
AGENTGUARD_AGENT_ID | hostname | Identifier for this agent instance |
AGENTGUARD_DEPLOYMENT_ID | local | Deployment environment tag |
AGENTGUARD_DEPLOYMENT_NAME | Local Dev | Human-readable deployment name |
pre_tool_call → AgentGuard /api/holds)| Variable | Default | Description |
|---|---|---|
AGENTGUARD_URL | http://localhost:8080 | AgentGuard server base URL — where pre_tool_call POSTs its hold request |
AGENTGUARD_HOLDS_FAIL_POLICY | deny | What to return when /api/holds is unreachable: deny blocks the tool call, allow lets it proceed |
AGENTGUARD_HOLDS_ENABLED | false | When false, pre_tool_call reverts to async observation-only mode (no blocking, event still ingested) |
The plugin's synchronous /api/holds call hits the AgentGuard server with no user session cookie (server-to-server). The AgentGuard server in turn needs Timeplus credentials to ingest the event and query rules. You must set TIMEPLUS_USER and TIMEPLUS_PASSWORD env vars on the AgentGuard process — these are the canonical service credentials for plugin endpoints.
Without these, /api/holds returns:
HTTP 503
{"error":"service credentials not configured: set TIMEPLUS_USER and TIMEPLUS_PASSWORD env vars on the AgentGuard server"}
For the standard docker-compose.yaml setup these are pre-set to proton / timeplus@t+. Edit them if your wizard credentials differ, then docker compose up -d --build agentguard.
# Async ingest — self-hosted, timeplusd reachable
export AGENTGUARD_TIMEPLUS_URL=http://timeplus.example.com:3218
export AGENTGUARD_DATABASE=ag
export AGENTGUARD_USERNAME=proton
export AGENTGUARD_PASSWORD=secret
# …or via the Timeplus REST API when port 3218 is not exposed (same credentials)
# export AGENTGUARD_INGEST_API=rest
# export AGENTGUARD_TIMEPLUS_URL=http://timeplus.example.com:8000
# Identity
export AGENTGUARD_DEPLOYMENT_ID=production
export AGENTGUARD_DEPLOYMENT_NAME="Production Hermes"
# Sync tool blocking
export AGENTGUARD_URL=http://agentguard.example.com:8080
export AGENTGUARD_HOLDS_FAIL_POLICY=deny
export AGENTGUARD_HOLDS_ENABLED=true
hermes
Every Hermes hook event is forwarded to the agentguard_hook_events stream:
| Hermes hook | Sent as | Description |
|---|---|---|
on_session_start | on_session_start | New session begins |
on_session_end | conversation_end | Single chat turn completed |
on_session_finalize | on_session_end | Session fully torn down (CLI exit, /reset) — also fires POST /api/holds/_abandon-session to clean up any pending hold |
on_session_reset | on_session_reset | Session rotated via /new |
pre_llm_call | pre_llm_call | Before each LLM turn |
post_llm_call | post_llm_call | After each LLM turn |
pre_api_request | pre_api_request | Before each raw API call (token metrics) |
post_api_request | post_api_request | After each raw API call |
pre_tool_call | pre_tool_call | Before each tool execution — synchronous when AGENTGUARD_HOLDS_ENABLED=true: ingest happens via /api/holds (not the async path), and the callback returns {"action": "block", "message": ...} on deny |
post_tool_call | post_tool_call | After each tool execution |
conversation_history is stripped from all events before ingestion — it is unbounded and contains no information not already available from the individual turn events.
To exercise blocking end-to-end:
http://localhost:8080, log in, and install a rule that matches Hermes tool calls (e.g. Privilege Guard for shell-like tools).block_policy to hold on the rule detail page (/rules/:id → Block Policy panel).<tool> on <agent>"args_summaryagentguard_holds stream:
curl -s -X POST http://localhost:3218/proton/v1/query \
-u proton:'timeplus@t+' -H 'Content-Type: application/json' \
-d '{"query":"SELECT hold_id, tool_name, status, decided_by, decided_at FROM table(mv_holds_current) ORDER BY created_at DESC LIMIT 10"}'
If the plugin reports AgentGuard unreachable — backend returned 503, the AgentGuard server is missing service credentials — see Server-side prerequisite.
agentguard-hermes install --hermes-dir /path/to/hermes/data
If you run Hermes via the provided Docker Compose setup in agents/hermes/:
make configure # copies plugin into .hermes/plugins/agentguard/
make cli # start Hermes CLI with AgentGuard env vars pre-set
make start # start Hermes gateway + web dashboard
pip install hatch
cd agents/hermes/agentguard-plugin
hatch build # produces dist/agentguard_hermes_plugin-*.whl
hatch publish
Download counts and popularity will appear here as the collector receives them.
Declarations describe requirements; they are not a resolved dependency graph.
bdist_wheel · 11.43 KB · Python py3
sdist · 11.7 KB · Python source
Observed since 20 Oct, 22:06 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:9b8cd6a5b8b46a07568fc22cc40b9629a30e812e4d6f5ca25d10d66ecdf222d9