Skip to main content

VAIBot Governance Plugin for Hermes

VAIBot governance for Hermes. Routes every tool call through the local @vaibot/guard, blocks or escalates it per policy, and writes signed, tamper-evident receipts.

One of the VAIBot circuit breakers, alongside the Claude Code, Codex, OpenClaw and Cursor plugins. They share one guard, one credential store and one signed policy, so an account's rules apply the same way whichever agent you run.

What it does

Guard verdict Hermes directive
allow None — the call proceeds
approve {"action": "approve"} — Hermes' native [o]nce / [s]ession / [a]lways / [d]eny prompt
deny {"action": "block"} — the reason becomes the tool result the model sees
anything else block (fail-closed)

The guard's published effective_mode is authoritative. In observe, everything proceeds except the catastrophic floor, which blocks in every mode.

When the guard can't answer, the plugin degrades rather than bricking the agent. The guard's own classifier is the floor on every degraded path:

Rung Behaviour
Containment engaged The account-wide stop. Checked before every rung below — before settings are even read, and again if governing a call fails unexpectedly — and it is the one thing that holds in observe and under VAIBOT_FAIL_OPEN. Read from machine-wide state with no daemon, no network and no credentials, so it survives exactly the paths that never reach the guard. Governance tools stay exempt, so an operator can still lift it.
No API key Provision one via vaibot-guard bootstrap. If that can't, govern locally: floor blocks, risky calls prompt, safe work runs. Retries after 5 min (1 h if the account exists and only vaibot login can help).
Breaker tripped 3 guard failures inside 10 s. Decide locally for 60 s without calling the guard: denylist blocks, classifier-safe passes, the rest blocks. State persists in ~/.vaibot/breaker-state/hermes.json.
Guard down, fresh install No rendezvous lock yet, so non-catastrophic work runs while the daemon comes up.
Guard down, established install The lock exists but nothing answers. That looks the same as tampering, so the call is governed locally and flagged loudly.

Approvals, and what still blocks

  • The floor holds in every mode. On a degraded path a classifier deny still blocks, even under observe or VAIBOT_FAIL_OPEN. Those settings change whether you are asked, never whether a catastrophic action can run.
  • A call that cannot be classified is asked about, in every mode. If no guard CLI can be reached — none installed, no Node for the bundled copy, or one too old to have classify — there is no floor left to apply, so the call escalates rather than proceeding. Observe behaves exactly as enforce does here rather than becoming stricter than it.
  • An escalation Hermes would grant automatically is refused unless policy says otherwise. Under --yolo, /yolo, or approvals.cron_mode: approve, Hermes approves plugin escalations before anyone sees a prompt. The plugin reports that posture on each decision and the guard applies the account's hostBypassAction: deny (the default) blocks and mints no approval, approve hands it to Hermes' prompt and records the receipt as bypassed, never approved. Reporting rather than deciding is deliberate — loosening a bypass takes a verified signed bundle, which a plugin cannot mint. Every degraded rung keeps refusing, because no guard answered there and so no policy authorised anything. If Hermes' approval internals can't be read, the plugin assumes a bypass is active: failing to detect one must cost a prompt, never an ungoverned action.
  • [a]lways is scoped to the policy rule that fired, so one answer covers what the human was asked about ("writes outside the workspace") rather than one exact path — and still can't blanket a tool, since a different rule on the same tool asks again. Against a guard that names no rule it falls back to the exact call (tool + arguments), which under-grants rather than over-grants.

Install

pip install vaibot-hermes-circuitbreaker
hermes plugins enable vaibot

A directory install works too, with no build step — cp -r vaibot ~/.hermes/plugins/vaibot. The same vaibot/ tree serves both.

One install is the whole thing. The guard ships inside the package, as it does in the Claude Code, Codex, Cursor and OpenClaw plugins, so there is no second thing to fetch and no order to get right. No Python dependencies either — the plugin loads in-process inside your agent on a security path, so it adds none.

Needs Node on the machine, which is what runs the guard. If you already have vaibot-guard on PATH that one is used in preference to the bundled copy, so a full VAIBot install keeps a single guard rather than quietly running the one inside a plugin. VAIBOT_GUARD_CLI overrides both.

/vaibot status tells you which one answered, and whether it is new enough to provide the floor:

Classifier    from PATH · `classify` answers
Classifier    vendored with this plugin · `classify` answers
Classifier    NOT FOUND · degraded paths cannot reach the floor, so they ask instead

If no guard is running, one is started for you; see the local guard.

Settings

Every setting is an environment variable, and every one is also a Hermes plugin setting — plugins.entries.vaibot.<key> in config.yaml, declared in the plugin's own config_schema, so hermes can show it.

Setting Variable Default Meaning
mode VAIBOT_MODE enforce Posture when no guard answers; the guard's effective_mode wins whenever one does
fail_open VAIBOT_FAIL_OPEN false true lets guard errors through (the floor still blocks)
timeout_ms VAIBOT_TIMEOUT_MS 10000 Guard decide timeout
workspace VAIBOT_WORKSPACE cwd Workspace sent with each decision, and pinned on a guard VAIBot starts
guard_cli VAIBOT_GUARD_CLI vaibot-guard on PATH Guard CLI for classify / bootstrap; a .mjs path runs under node
auto_launch VAIBOT_AUTO_LAUNCH true Start a local guard when none is running
breaker_failure_threshold VAIBOT_BREAKER_FAILURE_THRESHOLD 3 Guard failures inside the window that trip the breaker
breaker_window_ms VAIBOT_BREAKER_WINDOW_MS 10000 Breaker failure window
breaker_cooldown_ms VAIBOT_BREAKER_COOLDOWN_MS 60000 How long the breaker decides locally before handing back
breaker_denylist VAIBOT_BREAKER_DENYLIST empty Tools always blocked while tripped (a list, or a comma-separated variable)

Which source wins. The guard's published effective_mode beats everything local whenever a guard answers. Below that: the variable, then the plugin setting, then the default. The variable comes first because it is the narrower, more deliberate scope — one process, one job, one debugging session — and because it is the only source the other VAIBot breakers have.

mode and fail_open are the exception, and it only ever tightens. They are the two settings that can only loosen governance, so the stricter source wins rather than the nearer one: enforce pinned in config.yaml is not undone by VAIBOT_MODE=observe in the environment the agent itself runs in. A variable can still tighten a loose setting, and a variable on its own behaves exactly as it always has.

Credentials and endpoints have no plugin setting — VAIBOT_API_KEY, VAIBOT_ENV, VAIBOT_API_URL, VAIBOT_GOVERNANCE_URL, VAIBOT_ALLOW_URL_OVERRIDE, VAIBOT_CREDS_DIR, VAIBOT_GUARD_BASE_URL and VAIBOT_GUARD_TOKEN stay where the guard can see them. One machine has one VAIBot identity, resolved by the shared credential store so every breaker on it agrees; a per-plugin override would fork that identity, and would be a second way around the production URL-override gate.

The local guard

When no guard is running — at load, or when a decision finds none — VAIBot starts one. When one is already running it is adopted, not duplicated.

A machine has exactly one guard, and that invariant belongs to the daemon: the rendezvous lock it writes at ~/.vaibot/guard/guard.json and the port it binds, which acts as a mutex. So the plugin probes before it launches, and the launch itself is the guard's own launcher rather than a second copy of it — one implementation of the single-instance rule, one writer of the rendezvous file, the same way the classifier and the credential writer are the guard's and not this plugin's.

It never holds up a tool call. The launch runs in the background and the call in flight is governed by the ladder above; the next call finds a guard. Concurrent callers produce one attempt, and an attempt that fails backs off rather than paying a subprocess per tool call.

hermes vaibot guard     make sure the guard is up, in the foreground, and say what happened
hermes vaibot status    the readout below, from a terminal

auto_launch: false (or VAIBOT_AUTO_LAUNCH=0) switches it off, and an explicit VAIBOT_GUARD_BASE_URL — someone else's guard — is always left alone.

/vaibot

/vaibot status   what is governing this session right now
/vaibot help

Registered through Hermes' slash-command API, so it works in a CLI session and through the Telegram and Discord gateways alike — and as hermes vaibot status outside a session altogether. It exists for one moment in particular — an agent has just stopped and you need to know why — so containment and breaker state are read from local state and the guard is probed rather than assumed. It answers with no daemon and no network.

VAIBot status

  Containment   CONTAINED · laptop looks compromised · 4m ago
  Guard         none found · governing locally from the classifier floor
  Account       no key yet · production · run `vaibot login`
  Breaker       healthy

  Every action on this account is blocked, on every machine.
  Lift it from the dashboard or with `vaibot release`.

It never prints a credential: it says whether a key resolves and for which environment, never the key, and never the guard token. The output can land in a Telegram or Discord channel, so it is written to be public. A Hermes without the slash-command API still loads the plugin — governance outranks a readout.

Design notes

Zero dependencies. Standard library only, and the imports are tested, not just declared. This runs in-process inside your agent on a security path; HTTP is urllib and the daemon is loopback. Three things are deliberately not reimplemented, because a Python copy would drift from the original: risk classification (vaibot-guard classify), credential writes (vaibot-guard bootstrap) and starting the daemon (the guard's own ensureGuardDefault). One safety floor, one writer for the shared credential store, one launcher.

Tool names. Guards that advertise host-vocab:hermes on /health understand Hermes' names natively, so receipts say terminal. Released guards before that would treat terminal as an unknown tool, skipping both the catastrophic floor and the workspace-boundary check. For those, the plugin renames each call to a tool they already understand (terminal → shell, write_file → write, …). The rename is safe against every guard, so a capability-detection mistake can only mislabel a receipt, never open the floor. execute_code is deliberately never renamed to a shell: Python read as a shell command would pass cat = open(...) as cat.

Discovery via the rendezvous lock, never a hardcoded port. The daemon writes its real host/port/token to ~/.vaibot/guard/guard.json, and it often doesn't bind the default.

Shared identity. Reads the same ~/.vaibot/credentials.json (v3, env-namespaced) as the Node plugins, with the same resolution precedence, the same lenient key-prefix guard, and the same production URL-override gate: VAIBOT_GOVERNANCE_URL is ignored for production unless VAIBOT_ALLOW_URL_OVERRIDE is set. A cross-language test resolves the same fixtures through the Node original and this port and requires identical answers.

Receipts close synchronously. Hermes hooks run in-process, so run state is a dict rather than /tmp files claimed by unlinking. And post_tool_call fires even for blocked calls, including a declined approval prompt, so every decision closes its own receipt.

A receipt never claims a person decided something they were never asked. A call handed to Hermes' gate that comes back blocked is finalized approval: "denied", approvalScope: "prompt" — without it the guard records an escalated run as approved. A call VAIBot refused itself, such as declining an auto-granted escalation, returns a block directive, so the gate never opens and no prompt fires: that finalizes as a plain policy deny. What still cannot be told apart is which of deny, timeout or gate error ended a real prompt — Hermes reports all three as a blocked tool call, and the receipt vocabulary has one state for "not granted", so nothing honest separates them yet.

Fail-closed where it counts. Hermes runs a call whose hook raised, so an unexpected error while governing blocks the call (unless you chose observe or fail-open — and containment holds even then). A guard that is reachable and still gives no usable verdict has decided nothing: that blocks in enforce, and under observe or fail-open it falls to the classifier floor like any other degraded path, rather than reading as "nothing to see here". A 4xx is a real answer, not an outage, so it never trips the breaker. Bookkeeping failures degrade to "no receipt" and never reach tool dispatch.

Tests

python3 -m unittest discover -s tests
VAIBOT_GUARD_SRC=/path/to/vaibot-guard python3 -m unittest discover -s tests   # + real-CLI floor tests

No dependencies to install. Some tests are cross-language and run only when node and a guard checkout are available; each skips cleanly otherwise:

  • credential resolution against the Node creds.mjs
  • every rename target against the guard's released classifier
  • the floor through the real classify CLI (needs a guard that has the subcommand)

License

MIT

Metadata

Release files for vaibot-hermes-circuitbreaker 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vaibot-hermes-circuitbreaker 0.2.0
File Size Uploaded
vaibot_hermes_circuitbreaker-0.2.0.tar.gz 205.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for vaibot-hermes-circuitbreaker 0.2.0
File Interpreter ABI Platform
vaibot_hermes_circuitbreaker-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 399.0 kB

Release files / vaibot_hermes_circuitbreaker-0.2.0.tar.gz

Download URL vaibot_hermes_circuitbreaker-0.2.0.tar.gz
Size 205.7 kB
Tags Source
SHA-256 checksum
How to use checksums
f85313f2b5c1ba4b3c1eb4ac22d22d8cbdcadf8d433d142364ece08cd3cdfd79
BLAKE2b-256 checksum
How to use checksums
894b684e35fc7d3d6bcc7eaad18490ce386ff55afe88775847fc43b1a0b442d2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / vaibot_hermes_circuitbreaker-0.2.0-py3-none-any.whl

Download URL vaibot_hermes_circuitbreaker-0.2.0-py3-none-any.whl
Size 193.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9c1181aee1fcc4127d5fd58b465998db9c0fe6851098444b32cfa1b3f6bca69c
BLAKE2b-256 checksum
How to use checksums
d9df35feafe4f60d48d84eb02f5983925c03e74de332e6521c5ba704b0482c55
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page