Hermes Agent Host¶
The hermes-agent-host is the external worker that executes agent jobs. It runs the NousResearch Hermes agent CLI in one container per organization, claims queued jobs from the Backend API as an AGENT-role bot, and proposes reviewable output back through the ordinary API — it is a pure API consumer with no database access of its own.
It is the counterpart to the platform's in-process agent sessions: those run an LLM conversation inside the backend, this runs autonomous work outside it.
Overview¶
flowchart TB
subgraph Host [hermes-agent-host]
GW[agent-gateway<br/>shared OAuth login]
D[dispatcher<br/>bot credential + Docker socket]
W1["hermes-org-A<br/>own HERMES_HOME"]
W2["hermes-org-B<br/>own HERMES_HOME"]
L[launcher<br/>per-org dashboards]
end
D -->|poll / claim| API[Backend API]
D -->|spawn · deliver job + token · reap| W1
D --> W2
W1 -->|sartiq CLI, job-scoped token| API
W2 --> API
W1 --> GW
W2 --> GW
L -.->|start on demand| W1
| Service | Role |
|---|---|
| agent-gateway | Holds one OAuth login and exposes an OpenAI-compatible endpoint on the compose network. Every other container points its model config at it — one login, one token refresher, no refresh race between containers |
| dispatcher | The only holder of the bot credential and the Docker socket: poll → claim → spawn → deliver → reap |
| hermes-org-\<id> | One worker container per organization, with its own Hermes home. Receives only the job-scoped token, never the bot credential |
| launcher | An index over all organizations that starts a per-org dashboard on demand. It never runs an agent — it lists directories and starts containers |
| dashboard (per org) | Chat, sessions, memory and skills for a single organization, mounted with only that org's home and workspaces |
There is deliberately no all-orgs dashboard: its chat page runs a real agent, so pointing one at every organization would put every tenant's memory one prompt injection apart.
Job flow¶
sequenceDiagram
participant D as Dispatcher
participant API as Backend API
participant W as Org worker
participant GW as agent-gateway
loop per job type
D->>API: GET /agent-jobs?status=QUEUED&type=… (bot token)
end
D->>API: POST /agent-jobs/{id}/claim (bot token)
API-->>D: job-scoped token + organization_id + manifest
D->>W: ensure container, deliver {job, token}
W->>W: prepare per-job workspace, download manifest
W->>GW: run Hermes headless with the job-type prompt
W->>API: sartiq call … (job-scoped token)
W->>API: heartbeat / events
W->>API: complete | fail | release
Polling is one request per job type because the list endpoint defaults to product ingest. Each worker processes its organization's jobs serially — a single writer per org, so concurrent orgs never race even though they run at the same time.
Recovering a dead worker¶
A job whose worker was killed would otherwise stay CLAIMED/RUNNING forever, because polling only looks at queued jobs. The backend already re-claims a job whose claimant stopped heartbeating — the dispatcher simply asks it to. Attempts are counted from the job's own event log (so the count survives a dispatcher restart) and capped.
This is safe only because the worker heartbeats: the backend judges staleness from the last heartbeat, and a long guidelines run would otherwise look stale while perfectly healthy.
Safety model¶
| Boundary | How it is enforced |
|---|---|
| Credential separation | The bot credential lives only in the dispatcher. A worker gets only the job-scoped token from the claim |
| Organization boundary | Derived from the backend's organization_id in host code, never from anything the model says — prompt injection cannot switch org. Workers additionally reject any job not addressed to their org |
| Token scope | The job token is org-scoped with a curated, default-deny allowlist on the backend: reads, presign, its capability's write surface, and its own job endpoints. Granting a new endpoint is an explicit one-line change, never a prompt change |
| Data isolation | Each org has its own container and its own Hermes home, so memories, skills, sessions and state are isolated by Hermes' own per-instance boundary rather than by a mapping we invented |
| Human review | The agent only ever proposes; a human confirms in the webapp |
| Opt-in mutation | Claiming is gated (CLAIM_ENABLED) — by default the dispatcher only reads the queue and logs what it would claim. Terminal reporting is gated separately |
| Gateway gate | If the model gateway serves neither the worker nor the vision model, the dispatcher does not poll or claim at all — an expired login leaves the queue untouched instead of dragging every job through a spawn and a release |
What an organization accumulates¶
Each org is a standalone Hermes instance with a host-visible home directory, so an operator can read what that org's agent has learned without entering a container.
- Memory is Hermes' own loop end to end: the agent writes it with its memory tool, Hermes injects it at session start. The host deliberately does neither half — writing to the memory file from outside trips Hermes' external-drift detection and permanently blocks the agent from consolidating its own memory.
- Skills are stock: the agent authors one and it is discovered on every later job. Bundled skills are read from the image, not copied per org, so rebuilding updates them everywhere at once. Authored skills are capped — past the limit the least-recently-used ones are disabled (never deleted), with usage read from Hermes' own state.
- Identity comes from a host-generated
SOUL.md: a versioned block the host owns plus a free section it never touches. The agent cannot rewrite the generated block — jobs scrape third-party storefronts, and an identity file the agent could rewrite from that context would let hostile page content redefine the agent for every future session. - Each job starts a fresh session, on purpose. Sessions do persist and resuming is available, but continuity should come from what the agent chose to keep — memory and skills — not from every wrong turn it happened to take, which would grow the context window until it dominated the cost of every later job.
Capabilities¶
A capability is one kind of job the host knows how to run: a prompt, a workspace shape, a write surface and a completion gate. Today the host runs the two job types the backend defines — product_ingest and guidelines_extraction — with one prompt file per type; the runner itself stays generic and fails loudly on an unknown type.
Long guidelines runs are checkpointed: results are persisted per finished category, and a run that hits its turn limit resumes the same session automatically. The job is reported complete only when both the final response and the durable results agree.
Planned: job chaining
A model_selection job chained from a completed guidelines_extraction job (per-category subject assignment) is designed but not yet enabled — see SAR-1899.
How the worker calls the backend¶
The worker ships a small sartiq CLI that self-discovers the live OpenAPI document and attaches the job token from the environment — no MCP server and no generated client to keep in sync:
sartiq ops <substring> # discover operationIds
sartiq call <operationId> [--path k=v] [--query k=v] [--json '<body>']
The agent shell-calls it. The token reaches the CLI through the environment; the model never sees it and never picks the organization.
Related documentation¶
- Agent Job Queue — the backend side of this contract
- Agent Sessions — the in-backend conversational agents
- Product Ingestion — where ingest proposals land
- Backend API — the API this host consumes