# Architecture 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 ```text 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_json` columns) - per-agent secrets (encrypted in the `agent_secrets` table when `OPENHERMIT_SECRETS_KEY` is set; falls back to per-agent `secrets.json` only 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: ```text ~/.openhermit/ ├── gateway/ # gateway.json, .env, registry/, logs └── workspaces/{agentId}/ ``` Enabled skills are synced into each backend's own sandbox at `/.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: 1. loads environment variables 2. loads `~/.openhermit/gateway/gateway.json` 3. opens PostgreSQL stores if `DATABASE_URL` is available 4. scans and registers built-in skills from `skills/` 5. starts the Hono server and WebSocket handler 6. loads the `ChannelManifestRegistry` — bundled defaults (Telegram/Slack/Discord) plus any plugin packages named in `gateway.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_steps` row 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 with `409 research_run_active`. Authoritative design: [`deep-research-design.md`](./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`](./sandbox-model.md)). Supported backend types: - `docker`: per-agent workspace container managed by the gateway - `host`: runs commands on the gateway host (devops-only — see sandbox-model.md) - `e2b`: runs commands in a per-agent [E2B](https://e2b.dev) cloud sandbox - `daytona`: runs commands in a per-agent [Daytona](https://www.daytona.io) cloud sandbox Agents created through the gateway use the preset named by `autoProvisionSandbox` in `gateway.json` (default: `docker-ubuntu`); pass `sandbox: ""` or `null` on `POST /api/agents` to override. Sandbox lifecycle (per backend) supports: - start: `ondemand` or `session` - stop: `idle` or `session` - 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`](./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 the `ChannelManifestRegistry`. See [`channel-adapter.md`](channel-adapter.md) and [`channel-plugin-design.md`](channel-plugin-design.md). - **Schedules:** cron/once jobs that post prompts into dedicated or configured sessions - **Web providers:** Defuddle, Exa, Tavily ## Development Commands ```bash npm run dev:gateway npm run dev:web npm run dev:cli npm run dev:studio npm run typecheck npm test ```