An Eight Mile project · open source
Hermes Control Center
One calm desk for a very capable agent. See whether Hermes is ready, start focused work, follow every run to a final state, inspect the context it keeps, and manage the small part of its workspace you deliberately expose — through a local control plane that never pretends to be the agent.
Manager
Python · FastAPI
Desk
React 19 · Vite
History
SQLite
Delivery
Docker Compose
control_plane
listening
The operator view
Start with the whole system, then move with intent.
A capable agent is easy to talk to and hard to supervise. The opening screen exists to answer the three questions that decide whether anything else on the desk is worth doing: is the manager alive, can it reach Hermes, and what can this instance actually do right now?
Overview
connected
manager
alive
checked locally, no upstream round trip
gateway
ready
the agent speaks for itself
capabilities
listed
whatever this instance exposes today
The manager answers for itself and asks Hermes for the rest. A gateway that has gone away shows up here as an unready gateway, not as a desk that has stopped working.
Readiness at a glance
Manager health, gateway readiness and the exposed capabilities sit in one opening view, so the first question is answered before any work is submitted.
Work stays observable
Start a run, watch its status and its output, stop it when it is no longer worth finishing, and keep the local record of what happened.
The boundary is visible
The desk names what is local, what belongs to Hermes and which workspace files it may touch. Nothing about the split is left to be guessed at.
From intent to result
Give Hermes a clear job. Keep the run in sight.
The control desk does not become the agent. It submits work through the supported run API, follows progress until the run finishes or is stopped, and keeps a small local record of what happened — which is the part a gateway restart would otherwise take with it.
The run loop
manager routes only
The agent works; the desk keeps watch. Everything in the middle box is a manager route — nothing here executes a step of the run itself.
01
Ask
Write the outcome you want, and any limits Hermes should observe.
02
Shape
Optionally choose the session, the instructions, the model and the provider.
03
Watch
The desk polls the run until it reaches a final state, showing status and output as they arrive.
04
Recall
A run started here stays in the local SQLite audit, so the question and its answer survive the tab.
6 control rooms
Everything close enough to operate. Nothing pretending to be the agent.
Each room has one job. Hermes stays the source of truth for execution, sessions, jobs, skills and tools; the manager concentrates access to them and adds the one thing the gateway does not keep — a local record of the runs started here.
readiness
Overview
Manager, gateway and capability health in one glance.
execution
Run agent
Submit work, monitor its status and stop an active run.
context
Sessions
Inspect recent Hermes conversations and the context they retain.
automation
Scheduled jobs
Review or create gateway jobs through their native JSON contract.
capability
Skills and tools
See the skills and toolsets the current Hermes instance exposes.
workspace
Files
Read and edit only the mounted paths the manager admits.
A deliberate boundary
Power belongs behind a clear gate.
This desk can launch tool-using work and change agent configuration, so the interesting question is not what it can do but what it refuses to. Its protections shrink the exposed surface; they do not turn a local starter into a shared security boundary, and the page would be worth less if it claimed otherwise.
The allow list is the whole policy. A path that does not begin with an admitted root, or that resolves outside the mount once symlinks are followed, never reaches the filesystem.
What holds it
The Hermes API key stays on the manager side, never in the browser.
Only the admitted Hermes home paths can be opened or saved.
A path is resolved before it is used, and traversal is refused.
Public exposure still needs TLS, authentication and network controls.
Do not expose it publicly by default
Repository layout
A small codebase with a job for every seam.
The frontend stays feature-oriented, one module per control room. The Python layer keeps upstream behaviour, local persistence and filesystem policy in separate files, so none of the three can quietly acquire the others' concerns. Docker joins them only at delivery time.
Operator desk
frontend/src
Feature modules per control room, a typed API context, one shared request lifecycle and an accessible application shell.
feeds app
Control manager
app
Authentication, the route adapters, upstream error translation, the run audit and the file policy — one module each, so a change to any of them has one place to be.
feeds Dockerfile · compose
Delivery boundary
Dockerfile · compose
The web build is copied into the Python image and served by the manager, so there is one process to run and one port to open.
repository tree
trimmed
hermes-control-center/ ├── app/ │ ├── main.py # manager routes │ ├── hermes.py # upstream HTTP client │ ├── files.py # allowed workspace │ ├── db.py # local run audit │ └── settings.py # server secrets ├── frontend/src/ │ ├── api/ # authenticated boundary │ ├── app/ # shell + navigation │ ├── features/ # the control rooms │ └── styles/ # product system ├── tests/ ├── Dockerfile └── docker-compose.yml
System anatomy
A thin control plane with sharp seams.
The browser knows the manager. The manager knows the Hermes gateway, its own audit database and one mounted workspace. Credentials and routing decisions stop at that middle layer, which is the whole reason the layer exists.
control plane
three neighbours, no more
Active seam
Idle
Ready
Hermes stays authoritative
Runs, sessions, jobs, skills and toolsets are the gateway’s. The manager reads them and shows them; it never keeps a second opinion about them.
SQLite stays small
The local database holds the run audit and cached response bodies. It is not a replica of the agent’s state, and nothing is served from it that the gateway could answer.
Proxy routes stay narrow
Jobs and sessions are admitted by name. An arbitrary upstream path is not forwarded, so new access is a deliberate adapter rather than a query parameter.
HTTP surface
Small enough to audit from one file.
FastAPI exposes named manager routes and one controlled proxy. A manager token guards everything except the local health check, and the Hermes key never leaves the server environment — the browser holds one credential and it is not the agent’s.
app/main.py
6 groups
GET
/api/health
Manager health
open
Local liveness, answered without an upstream round trip.
GET
/api/hermes/health · readiness · capabilities · models
Gateway state
manager token
Named adapters, one per upstream question. Each of them is a function, not a path the caller chooses.
POST · GET
/api/runs · /api/runs/{id}
Run lifecycle
manager token
Create a run and poll it. The manager records both ends, which is where the local audit comes from.
POST · GET
/api/runs/{id}/stop · /api/runs/{id}/events
Run control
manager token
Stop an active run, or stream its server-sent events while it is still going.
GET · POST · PATCH · DELETE
/api/hermes/api/jobs · /api/hermes/api/sessions
Controlled proxy
manager token
The only forwarding in the service, and only for the two approved upstream prefixes.
GET · PUT
/api/files · /api/files/content
Workspace files
manager token
List, read and save — after the path has been through the allow list and resolved inside the mount.
Filesystem boundary
The mount is not an invitation to wander.
The file routes are the ones worth reading twice, because they are the only place the manager writes anything outside its own database. Four checks stand between a requested path and the disk, and the allow list is the first of them rather than the last.
01
Begins with an admitted root
The allow list is a list of prefixes, not a pattern. A path that does not start with one is refused before anything touches the filesystem.
02
Resolves inside the mount
The path is resolved and compared against the Hermes home root, so a traversal segment or a symlink out of the mount fails the same check.
03
Is a regular file
Directories, devices and sockets are not files the desk will open or save over.
04
Parses, if it is YAML
A write to a YAML file is parsed before it is accepted, so the desk cannot be the thing that leaves the agent with a configuration it will not start on.
May pass
config.yaml
SOUL.md
skills/
memories/
cron/
logs/
Must stop
.env
auth.json
private keys
../ traversal
arbitrary host paths
There is a redaction branch for .env, and it is defensive rather than load-bearing: .env does not begin with an admitted root, so in normal operation it never reaches the branch at all.
Run it
Gateway first. Control desk second.
Hermes has to be serving its supported gateway API before the manager can do anything useful, so the first command belongs to the agent and not to this repository. After that one Compose build packages the React desk and the FastAPI service into a single image.
Hermes gateway
its API server enabled
An API key
the same value on both sides
Docker
builds and runs the whole desk
Hermes home
optional host mount for files
01 · enable the Hermes API
# ~/.hermes/.env API_SERVER_ENABLED=true API_SERVER_KEY=change-me-local-dev hermes gateway # listens on :8642
02 · build the desk
git clone https://github.com/IbiliAze/hermes-control-center.git cd hermes-control-center cp .env.example .env mkdir -p hermes-home docker compose up --build -d
http://localhost:8080
One port, because the manager serves the built frontend itself. If the desk opens and the gateway reads as unready, the second command worked and the first one did not.
Behind the project
Built by Eight Mile in London, as part of our software development work — the same engineers who build and run systems like it for clients.