docs/architecture.md
Rama main · réplica de lectura del repositorio original.
# 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 `<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:
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: "<preset>"` 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
```