Architecture
August 20, 2026 · View on GitHub
OpenHermit is a gateway-managed, multi-agent runtime. The gateway owns process lifecycle, the API surface, the channel-bridge pool, and the central cron scheduler. Per agent, an in-process AgentRunner carries the workspace, security policy, tools, MCP connections, and per-session state — runners hydrate on demand when traffic arrives and are LRU-evicted when idle (controlled by OPENHERMIT_EVICTION_TTL_MINUTES, default 30). agents.status ('active' / 'disabled') in the DB is the source of truth for whether traffic is accepted; the in-memory runner map is a hydration cache, not a lifecycle.
Components
CLI / Web / Channels / API clients
|
v
Gateway (Hono HTTP + WebSocket, auth, admin UI)
|
v
AgentInstanceManager
|
+-- AgentRunner(agent A) -> exec backend -> docker / host / e2b / daytona
+-- AgentRunner(agent B) -> exec backend -> docker / host / e2b / daytona
|
v
PostgreSQL stores (Drizzle)
| Component | Responsibility |
|---|---|
apps/gateway | Control plane, auth, admin API, admin UI, agent lifecycle, WebSocket attachment, channel startup |
apps/agent | Agent loop, prompt assembly, tools, sessions, events, introspection, compaction, MCP client manager |
apps/cli | Setup, gateway lifecycle, agent management, chat TUI, config/secrets, schedules, logs/status |
apps/web | Standalone end-user chat web app |
apps/channels/* | Bundled bridges (Telegram, Discord, Slack) plus external plugins (e.g. @openhermit/channel-wechat) — all loaded uniformly through the ChannelManifestRegistry |
packages/protocol | Shared contracts and route builders |
packages/sdk | HTTP/SSE/WebSocket clients used by CLI/channels |
packages/store | Drizzle schema and PostgreSQL store implementations |
packages/shared | Errors, env loading, URL helpers, OpenHermit home helpers |
State Boundary
OpenHermit keeps internal runtime state separate from external task state.
Internal state is owned by OpenHermit and stored in PostgreSQL unless noted:
- agent records
- sessions and session events
- working memory and session descriptions
- long-term memories
- instructions
- users, roles, identities, and merges
- sandbox rows (per-agent execution environments) and their runtime state
- skills and skill assignments
- MCP server definitions and assignments
- schedules and schedule runs
- per-agent runtime config and security policy (
agents.config_json/agents.security_jsoncolumns) - per-agent secrets (encrypted in the
agent_secretstable whenOPENHERMIT_SECRETS_KEYis set; falls back to per-agentsecrets.jsononly when no key is configured) - channel rows with encrypted tokens (
agent_channels) and encrypted channel-owned auth state (agent_channel_credentials)
External state is the user's work surface:
- workspace files
- generated artifacts
- project repositories
- mounted data under the workspace
- workspace-installed skills under
.openhermit/skills
Per-Agent State
Runtime config, security policy, per-agent secrets, and channel-owned credentials are stored in PostgreSQL (agents.config_json, agents.security_json, agent_secrets, agent_channel_credentials) and managed through the admin UI, REST API, or hermit config ... / hermit security .... Channels live in agent_channels with encrypted tokens, managed through /api/agents/{agentId}/channels/.... Secrets and channel credential rows are encrypted at rest with OPENHERMIT_SECRETS_KEY; if no key is configured the gateway falls back to a plaintext secrets.json per agent for local-dev secrets only.
Per-agent state lives in Postgres; the only per-agent artifact on disk is the workspace:
~/.openhermit/
├── gateway/ # gateway.json, .env, registry/, logs
└── workspaces/{agentId}/
Enabled skills are synced into each backend's own sandbox at <agent_home>/.openhermit/skills/system/ rather than living under a gateway-side per-agent dir.
config_json controls model, exec backend, web provider, and memory introspection. security_json controls autonomy level, approval policy, access level, and access token. Secrets are referenced from config values with ${{SECRET_NAME}} and resolved through SecretStore at adapter-start time.
Per-agent instructions (the keyed sections of the system prompt) live in the instructions table, scoped by agent_id like schedules — there is no shared/global row. Each new agent is auto-seeded with three rows on creation: identity, soul, and rules. Owners edit their own; admins can apply the same mutation across every agent with --all (e.g. hermit instructions append rules "Always cite the source URL." --all), backed by POST /api/admin/instructions/fanout with { mode: 'set' | 'append' | 'remove', key, content? }.
Gateway Runtime
On startup, the gateway:
- loads environment variables
- loads
~/.openhermit/gateway/gateway.json - opens PostgreSQL stores if
DATABASE_URLis available - scans and registers built-in skills from
skills/ - starts the Hono server and WebSocket handler
- loads the
ChannelManifestRegistry— bundled defaults (Telegram/Slack/Discord) plus any plugin packages named ingateway.config.channelPackages(e.g.@openhermit/channel-wechat) — and boots the channel pool, attaching each adapter's webhook handler to gateway HTTP routes; runners hydrate lazily on first inbound traffic
Agents hydrate on demand: the first request that targets an agent constructs its AgentRunner, syncs enabled skills into the sandbox via runner.syncSkills, attaches existing channel outbounds from the pool, and connects enabled MCP servers lazily through the runner. Schedules fire from a single gateway-level CentralScheduler that hydrates the target runner on demand at fire time. Idle runners are evicted by an LRU after OPENHERMIT_EVICTION_TTL_MINUTES; channel bridges in the pool stay alive across eviction.
Agent Runtime
AgentRunner owns:
- durable session open/resume/list/delete behavior
- user and role resolution
- message queueing per session
- prompt assembly from instructions, memory, skills, user context, and recent history
- tool creation and role-based filtering
- tool approval pauses
- event persistence and realtime publication
- context compaction for long sessions
- introspection for memory and session metadata maintenance
- schedule execution host callbacks
- MCP tool discovery and namespacing
- channel outbound adapters for proactive
session_send - the Deep Research orchestrator (see below)
The main model loop is backed by @mariozechner/pi-agent-core and @mariozechner/pi-ai.
Deep Research
Deep Research is a durable, session-attached workflow — not a tool and not a
separate app. A ResearchOrchestrator (owned by AgentRunner, algorithm in
apps/agent/src/research/) drives a bounded plan → approve → search/read/
extract → synthesize loop:
- Models run as stateless phase calls (planner, action decider, per-source evidence extractor, synthesizer) with no tools; output is Zod-validated JSON with one repair attempt. The orchestrator — not the model — decides whether proposed actions are allowed and affordable (source policy, hard budgets with a synthesis reserve, duplicate-query and canonical-URL dedupe, concurrency caps, a deterministic finish gate, and a zero-information-gain stop).
- Every external action is a durable, idempotent
research_stepsrow written before execution; sources keep normalized text snapshots and hashes; evidence excerpts are verified verbatim against snapshots before insert. Report claims cite evidence IDs, validated server-side — unsupported findings downgrade to labeled caveats and citations render from durable records, never from model-supplied URLs. - Retrieved page content is untrusted data: it reaches only the no-tools
extractor inside an explicit envelope, and direct page fetches go through
the shared SSRF-safe fetch (
apps/agent/src/network/safe-fetch.ts). - Runs pause on graceful shutdown; stale active runs reconcile to
paused(runtime_restart)on hydration and resume manually. Chat turns in a session with an actively executing run are rejected with409 research_run_active.
Authoritative design: deep-research-design.md.
Execution Backends
The exec tool uses ExecBackendManager, built from the agent's rows in the sandboxes table (see sandbox-model.md).
Supported backend types:
docker: per-agent workspace container managed by the gatewayhost: runs commands on the gateway host (devops-only — see sandbox-model.md)e2b: runs commands in a per-agent E2B cloud sandboxdaytona: runs commands in a per-agent Daytona cloud sandbox
Agents created through the gateway use the preset named by autoProvisionSandbox in gateway.json (default: docker-ubuntu); pass sandbox: "<preset>" or null on POST /api/agents to override.
Sandbox lifecycle (per backend) supports:
- start:
ondemandorsession - stop:
idleorsession - idle timeout in minutes
Auth
Admin APIs require GATEWAY_ADMIN_TOKEN. Agent routes use a resolver that accepts:
- admin bearer token
- browser/device JWTs issued by
POST /api/auth/token(user-global; the JWT identifies a person, not an agent) - channel bearer tokens registered for built-in or external channel adapters
Agent access can be public, protected, or private. protected requires the agent access token during device-token exchange (members self-join via /members); private rejects anyone without an explicit membership row at session-open time. See sandbox-model.md for the full policy.
Extension Surfaces
- Tools: built-in toolsets plus dynamically connected MCP tools
- Skills: prompt instructions and supporting files, registered in DB and synced into each backend's sandbox via
runner.syncSkills - Channels: Telegram, Discord, Slack as bundled built-ins; additional adapters loaded as npm plugins via
channelPackages(e.g.@openhermit/channel-wechat). All registered through theChannelManifestRegistry. Seechannel-adapter.mdandchannel-plugin-design.md. - Schedules: cron/once jobs that post prompts into dedicated or configured sessions
- Web providers: Defuddle, Exa, Tavily
Development Commands
npm run dev:gateway
npm run dev:web
npm run dev:cli
npm run dev:studio
npm run typecheck
npm test