2026-06-09 00:59:20 -06:00

Hermes Control Plane

A local operations console and authenticated AI API gateway for Hermes Agent. Includes admin authentication, per-user API key management, pre/post AI API gateways, and full audit logging.

What It Does

  • Admin-authenticated control plane at port 7843 (login required).
  • Per-user API key management: create, rotate, revoke, delete keys with rate limits and monthly token caps.
  • Pre-Hermes AI API gateway at port 8645 — authenticated proxy for requests sent before Hermes processing.
  • Post-Hermes AI API gateway at port 8646 — authenticated proxy for requests sent after Hermes processing.
  • Full audit logging of prompts and responses with 90-day retention and JSONL download.
  • Lists OAuth/API-key provider pools and account identity data from Codex, Claude, Gemini, and Hermes auth state.
  • Edits Hermes model/fallback configuration.
  • Provides panes for skills, plugins, bundles, MCP, cron, sessions, hooks, memory, kanban, webhooks, profiles, storage, config, tools, insights, security, and system status.

Prerequisites

  • Docker and Docker Compose, or Node.js >= 20 for local runs.

Run With Docker Compose

cp .env.example .env
# Edit .env with your values, then:
docker compose up --build -d
docker compose logs -f

The default stack starts PostgreSQL and the control plane. Open:

Admin login:        http://127.0.0.1:7843/login
Control plane:      http://127.0.0.1:7843

Required environment variables for auth and gateway features:

POSTGRES_PASSWORD              PostgreSQL password (default: hermes-change-me — change this)
HERMES_ADMIN_USERNAME          Admin username (default: admin)
HERMES_ADMIN_PASSWORD          Admin password — minimum 16 characters
HERMES_ADMIN_SESSION_TTL_HOURS Session lifetime in hours (default: 8)
HERMES_ADMIN_COOKIE_SECURE     Set true when admin UI is served only through HTTPS
HERMES_LOG_RETENTION_DAYS      Audit log retention in days (default: 90)
HERMES_AUDIT_MAX_BYTES         Max bytes per logged request body (default: 65536)
HERMES_IMAGE                   Registry image Portainer pulls for app services
HERMES_AGENT_REF               Pinned Hermes source revision baked into the image
HERMES_INTERNAL_API_SERVER_KEY Separate internal key required when enabling API gateways
HERMES_DEFAULT_PROVIDER        Provider used when an API request omits model/provider (default: openai-codex)
HERMES_DEFAULT_THINKING_EFFORT Effort used when an API request omits reasoning/thinking effort (default: medium)

Admin Login

Navigate to http://<host>:7843/login and sign in with HERMES_ADMIN_USERNAME and HERMES_ADMIN_PASSWORD.

The admin password must be at least 16 characters. Sessions expire after HERMES_ADMIN_SESSION_TTL_HOURS hours (default 8).

On first run, set your admin credentials in the environment before starting the container. There is no in-app registration flow.

If the admin UI is exposed through an HTTPS reverse proxy, set HERMES_ADMIN_COOKIE_SECURE=true so browsers do not send the admin session cookie over plain HTTP.

API Access (Pre/Post)

API keys start with hms_. Each key is scoped to allow the pre gateway, the post gateway, or both.

The native API gateways are opt-in so missing provider authentication cannot break the control plane. Both public API wrappers forward to one internal Hermes gateway. That internal gateway uses the configured primary model and fallback_providers chain, so the pre API is not pinned to one upstream provider.

# Starts the shared internal Hermes gateway plus both public API wrappers.
docker compose --profile pre-gateway --profile post-gateway up --build -d
  • Pre API (http://<host>:8645/v1/chat/completions) — public OpenAI-compatible API with pre API-key permissions and audit labels.
  • Post API (http://<host>:8646/v1/chat/completions) — public OpenAI-compatible API with post API-key permissions and audit labels.

Send requests with a Bearer token:

POST /v1/chat/completions
Authorization: Bearer hms_...
Content-Type: application/json

If a JSON request to /v1/chat/completions or /v1/responses omits a model, the wrapper fills one from the default provider:

Claude:  claude-sonnet-4.6
Codex:   gpt-5.4
Gemini:  gemini-3.5-flash

Clients can still set the normal OpenAI-compatible model field. For thinking effort, clients can send reasoning.effort, reasoning_effort, or thinking_effort; the wrapper forwards it as reasoning.effort. If omitted, it defaults to medium.

A key not authorized for a gateway returns 403 Forbidden. A revoked or rotated key returns 410 Gone.

API Key Management

From the admin UI, navigate to API Users to:

  • Create a new API user with a display name, gateway access (pre/post), requests-per-minute limit, and monthly token cap.
  • Rotate a key — the old key is immediately invalidated and a new hms_ key is issued.
  • Revoke a key — sets the key inactive without deleting the user record.
  • Delete a user — removes the user and their key permanently.

The plaintext key is shown only once at creation or rotation. Store it immediately.

Audit Logs

Every request proxied through the pre or post gateway is logged, including the full request body and upstream response body. Logs are retained for HERMES_LOG_RETENTION_DAYS days (default 90). Per-entry body size is capped at HERMES_AUDIT_MAX_BYTES bytes (default 64 KB).

Download logs as JSONL from the admin UI or directly:

GET /api/admin/logs/download?api_user_id=<id>
Cookie: hermes_admin=...

Response content-type is application/x-ndjson. Each line is a JSON object with request metadata, prompt, and response.

Security Operations

  • Admin password minimum: 16 characters.
  • Session TTL: configurable via HERMES_ADMIN_SESSION_TTL_HOURS (default 8 hours).
  • Rotate API keys regularly. Old keys are invalidated immediately on rotation.
  • Revoke keys for users who no longer need access.
  • Monitor audit logs for unexpected usage patterns.

Deploy With Portainer Git Stack

Use Portainer's Git-backed Stack flow:

  1. Build and push the app image to the registry Portainer can pull.
  2. Push this repo to Git.
  3. In Portainer, create a Stack from a Git repository.
  4. Set the Compose path to docker-compose.yml.
  5. Add the environment variables from .env.example, adjusted for your server. At minimum set HERMES_IMAGE, POSTGRES_PASSWORD, HERMES_ADMIN_USERNAME, HERMES_ADMIN_PASSWORD, and HERMES_ADMIN_COOKIE_SECURE=true.
  6. Enable Portainer's auto-update option, either polling or webhook.

Example Gitea package registry image using the internal HTTP registry endpoint. Docker image names do not include http://; configure the Docker daemon to trust 10.0.3.6:4000 as an insecure registry, then use 10.0.3.6:4000/... in the tag.

On the Docker host running Portainer:

{
  "insecure-registries": ["10.0.3.6:4000"]
}

Save that as /etc/docker/daemon.json, restart Docker, then verify:

sudo systemctl restart docker
curl -v http://10.0.3.6:4000/v2/
docker login 10.0.3.6:4000
docker login 10.0.3.6:4000
docker buildx build \
  --platform linux/amd64 \
  -t 10.0.3.6:4000/zachariahsharma/hermes-control-plane:latest \
  --push .

Then set this in Portainer:

HERMES_IMAGE=10.0.3.6:4000/zachariahsharma/hermes-control-plane:latest

Portainer pulls the latest Git version when it redeploys the Stack. A normal Docker container restart does not pull Git or rebuild the image, so use Portainer's webhook/polling redeploy for updates.

Vynte Internal MCP

The stack includes an internal-only MCP adapter named vynte-internal-mcp. It exposes http://vynte-internal-mcp:8787/mcp on the Compose network and is registered in Hermes as a URL-mode MCP server at container startup. Disable that auto-registration by setting HERMES_REGISTER_VYNTE_INTERNAL_MCP=false. Set HERMES_VYNTE_INTERNAL_MCP_TOKEN to a long random value before adding real service tokens; Hermes stores it in .hermes/.env and sends it as a bearer token when calling the adapter.

The custom Forms/FormBuilder app is connected as its own MCP server at https://forms.internal.vyntehome.com/api/mcp, not through the generic Vynte adapter. Set HERMES_FORMS_MCP_TOKEN to a rotated Forms MCP token; Hermes stores it in .hermes/.env and sends it as a bearer token to the Forms MCP endpoint. Disable that direct registration with HERMES_REGISTER_FORMS_MCP=false.

The adapter is read-only by default:

  • vynte_services_list returns the configured Vynte service catalog.
  • vynte_service_health_check checks a catalog URL with HEAD/GET and does not return response bodies.
  • vynte_api_request_readonly allows only GET or HEAD requests against configured service base URLs and caps returned response previews.
  • Focused read tools are included for Plane projects/work items, Cal event types/slots/bookings, Twenty CRM records, and Plunk contacts/campaigns.

Set service tokens in Portainer only for services Hermes should be able to read:

VYNTE_PLANE_TOKEN=<Plane read API token>
VYNTE_GITEA_TOKEN=<Gitea read-only token>
VYNTE_HERMES_PRE_API_TOKEN=<Hermes pre-gateway read API token>
VYNTE_HERMES_POST_API_TOKEN=<Hermes post-gateway read API token>
VYNTE_OPENWEBUI_TOKEN=<Open WebUI read API token>
VYNTE_PORTAINER_TOKEN=<Portainer read-only/API token>
VYNTE_NPM_TOKEN=<Nginx Proxy Manager read-only/API token>
VYNTE_AUTHENTIK_TOKEN=<Authentik read-only API token>
VYNTE_NETBIRD_TOKEN=<NetBird read-only management token>
VYNTE_MEDIA_TOKEN=<media service read token>
VYNTE_PENPOT_TOKEN=<Penpot read API token>
VYNTE_PLAUSIBLE_TOKEN=<Plausible read API token>
VYNTE_PLUNK_TOKEN=<Plunk read API token>
VYNTE_CAL_TOKEN=<Cal read API token>
VYNTE_POSTIZ_TOKEN=<Postiz read API token>
VYNTE_TWENTY_TOKEN=<Twenty read API token>
HERMES_FORMS_MCP_TOKEN=<rotated Forms MCP token>

Plane API keys use X-API-Key by default via VYNTE_PLANE_AUTH_HEADER. Cal v2 requests include VYNTE_CAL_API_VERSION, defaulting to 2026-05-01.

Infrastructure-control services such as Portainer, NPM, Authentik, and NetBird should stay read-only unless the operator explicitly approves a change.

Nginx Proxy Manager

The compose stack binds published ports to 127.0.0.1 by default via HERMES_PUBLISHED_BIND_IP. This prevents clients on the LAN or internet from bypassing Nginx Proxy Manager by calling http://<docker-host>:7843, :8645, or :8646 directly.

For https://hermes.internal.vyntehome.com, configure that hostname only in Nginx Proxy Manager and proxy it to the control plane on port 7843. Do not set an application base-URL environment variable. Browser requests such as /api/status are intentionally same-origin and resolve to https://hermes.internal.vyntehome.com/api/status automatically.

Use one of these deployment shapes:

  • NPM on the same Docker host: keep HERMES_PUBLISHED_BIND_IP=127.0.0.1 and proxy to 127.0.0.1:7843, 127.0.0.1:8645, or 127.0.0.1:8646.
  • NPM in a container on a shared Docker network: prefer adding NPM to the stack network and proxy to service names such as hermes-control-plane:7843, without broad host-port exposure.
  • NPM on another host/LXC: set HERMES_PUBLISHED_BIND_IP to the private interface IP that only NPM can reach, then block direct client access to ports 7843, 8645, and 8646 with host or network firewall rules.

When the public admin URL is HTTPS, also set HERMES_ADMIN_COOKIE_SECURE=true.

Recommended Portainer environment for that deployment:

HERMES_PUBLISHED_BIND_IP=<private Docker-host IP reachable by NPM>
HERMES_SETUP_UI_PORT=7843
HERMES_ADMIN_USERNAME=<admin username>
HERMES_ADMIN_PASSWORD=<long random password>
HERMES_ADMIN_COOKIE_SECURE=true
POSTGRES_PASSWORD=<long random password>
HERMES_HOME_HOST=/opt/hermes-control-plane/hermes
HERMES_PRE_HOME_HOST=/opt/hermes-control-plane/hermes-pre
CODEX_HOME_HOST=/opt/hermes-control-plane/codex
CLAUDE_HOME_HOST=/opt/hermes-control-plane/claude
GEMINI_HOME_HOST=/opt/hermes-control-plane/gemini

HERMES_CONTAINER_USER defaults to 0:0 so the container can write credentials/config into Portainer-created bind directories. If you pre-create the host directories and chown them to UID/GID 1000:1000, set HERMES_CONTAINER_USER=1000:1000.

Recommended persistent host layout on the Portainer host:

/opt/hermes-control-plane/hermes
/opt/hermes-control-plane/hermes-pre
/opt/hermes-control-plane/codex
/opt/hermes-control-plane/claude
/opt/hermes-control-plane/gemini

The Hermes executable is baked into the image at:

/opt/hermes-agent/venv/bin/hermes

It is intentionally outside /home/hermes/.hermes, so mounting the persistent Hermes state directory cannot hide or remove the executable. The four host directories above contain only mutable state and authentication data.

Provider directories may be empty. For example, a deployment with only Codex authentication can leave the Claude and Gemini host directories empty. The control plane discovers whichever provider authentication files exist and does not require all providers.

Mounted CLI auth files are shown directly in the Providers pane. The control plane scans auth*.json files in:

CODEX_HOME_HOST  -> openai-codex
CLAUDE_HOME_HOST -> anthropic
GEMINI_HOME_HOST -> google-gemini-cli

This does not require hermes auth list to import those files into Hermes' own credential pool first.

In Docker, OAuth cannot open a browser window on your workstation. The stack sets HERMES_OAUTH_BROWSER=echo, so OAuth commands print the authorization URL instead. The Providers pane renders printed URLs as clickable links.

Mounted provider credentials are shown one row per auth*.json file, even when that file contains multiple tokens. Deleting a mounted provider credential moves that provider's mounted auth*.json file into .hermes-control-plane-deleted-auth in the same directory. Deleting a Hermes-native credential still calls hermes auth remove.

On first startup, the container creates missing state directories and seeds a default Hermes config.yaml only when one does not already exist. Existing configuration and authentication files are preserved.

The default Portainer deployment starts only PostgreSQL and the control plane. This is deliberate: Codex, Claude, and Gemini auth mounts are optional. Enable both API profiles when you want the OpenAI-compatible endpoints.

Default services: hermes-postgres, hermes-control-plane
Pre profile:      hermes-pre-upstream, hermes-pre-api
Post profile:     hermes-post-upstream, hermes-post-api

The pre and post gateways intentionally use different upstream Hermes runtimes. hermes-pre-upstream stores state in HERMES_PRE_HOME_HOST and starts with MCP registration disabled, so raw Codex, Claude, or Gemini model calls do not inherit the Forms or Vynte internal tools. hermes-post-upstream shares the main HERMES_HOME_HOST state with the control plane and keeps MCP registration enabled for agent/tool workflows.

Enable optional gateway profiles only after their prerequisites are configured. With Docker Compose, use --profile pre-gateway --profile post-gateway. In Portainer, set COMPOSE_PROFILES=pre-gateway,post-gateway in the stack environment if the UI does not expose profile toggles.

Set these in Portainer when enabling gateways:

COMPOSE_PROFILES=pre-gateway,post-gateway
HERMES_PRE_AI_API_PORT=8645
HERMES_POST_AI_API_PORT=8646
HERMES_INTERNAL_API_SERVER_KEY=<separate-long-random-key>
HERMES_DEFAULT_PROVIDER=openai-codex
HERMES_DEFAULT_THINKING_EFFORT=medium
HERMES_DEFAULT_CLAUDE_MODEL=claude-sonnet-4.6
HERMES_DEFAULT_CODEX_MODEL=gpt-5.4
HERMES_DEFAULT_GEMINI_MODEL=gemini-3.5-flash
HERMES_PRE_HOME_HOST=/opt/hermes-control-plane/hermes-pre

The public pre and post APIs accept the control plane's hms_ user keys. Internally they replace that header with HERMES_INTERNAL_API_SERVER_KEY before forwarding to Hermes. Do not reuse the admin password or expose the native upstream port.

Backup

PostgreSQL

Back up the control plane database regularly:

docker compose exec hermes-postgres pg_dump -U hermes hermes > hermes_$(date +%Y%m%d).sql

This captures all admin sessions, API user records, and audit logs. Restore with psql or docker compose exec -T hermes-postgres psql -U hermes hermes < backup.sql.

Hermes / Codex / Claude / Gemini State

Back up the bind-mounted directories on the host:

/opt/hermes-control-plane/hermes
/opt/hermes-control-plane/codex
/opt/hermes-control-plane/claude
/opt/hermes-control-plane/gemini

These contain OAuth tokens, auth state, and agent configuration. Archive or snapshot them with your preferred backup method.

Warning: Privacy

Full request prompts and upstream responses are stored in the audit log for up to 90 days. This includes all content sent to and received from the AI provider through the pre and post gateways.

Operators must inform users that their prompts and responses are logged. Do not deploy this system for users who have not been notified of and consented to this logging.

Runtime Requirements

The container builds its own Linux-compatible Hermes runtime from the pinned HERMES_AGENT_REF. Do not copy or mount a host Hermes virtual environment into the container.

The default .env.example mounts:

  • Hermes mutable state from /opt/hermes-control-plane/hermes
  • Codex auth/config from /opt/hermes-control-plane/codex to /home/hermes/.codex
  • Claude auth/config from /opt/hermes-control-plane/claude to /home/hermes/.claude
  • Gemini auth/config from /opt/hermes-control-plane/gemini to /home/hermes/.gemini

Local Node Run

If Hermes is installed directly on the same OS as Node:

node server.cjs

The default local URL is:

http://127.0.0.1:7843

Useful Commands

npm run check
npm test
docker compose config
docker compose down
S
Description
No description provided
Readme 1.2 MiB
Languages
JavaScript 78.2%
HTML 11.3%
CSS 9.9%
Dockerfile 0.4%
Shell 0.2%