Hermes Agent: an autonomous agent on Telegram
1. What? — Definition and context
Section titled “1. What? — Definition and context”Hermes Agent is an autonomous conversational agent released by Nous Research under an MIT licence. Unlike the AI building blocks already running on the VPS — which are gateways invoked by workflows — Hermes is a long-lived process that keeps the thread: it retains memory across conversations, decides for itself which tools to call, and needs no workflow to work out what to do.
On this infrastructure it runs in an ai-stack container with a dedicated Telegram bot as its only interface. No published port, no Caddy route, no dashboard.
Components
Section titled “Components”| Building block | Detail |
|---|---|
| Container | nousresearch/hermes-agent:latest — no published port, n8n-internal + mcp-backend networks |
| LLM | ChatGPT/Codex subscription through the native openai-codex provider (gpt-5.5) |
| Business tools | 32 MCP tools served by the N8N workflow Hermes MCP Tools |
| Debug tools | 4 read-only N8N tools (n8n-mcp), reserved for workflow diagnostics |
| Memory | External Mnemosyne provider — local SQLite, hybrid vector + FTS5 search |
| Plugins | 3 in-house plugins hooked into the gateway lifecycle |
Visual architecture
Section titled “Visual architecture”In this section
Section titled “In this section”This article covers the deployment: container, configuration, security model and operations. Three articles detail what plugs into it.
| Article | Content |
|---|---|
| The 32 MCP tools | Full catalogue, typed-input gateways, confined vault writes, pitfalls of the N8N MCP layer |
| Mnemosyne long-term memory | Wrapper mode, hybrid search, 20 memory tools, consolidation and day-to-day use |
| Custom skills | The skill system and the twelve procedures written for this deployment |
| Plugins and their hooks | Four Python plugins hooked into the gateway lifecycle |
2. Why? — Stakes and motivations
Section titled “2. Why? — Stakes and motivations”Six structuring decisions
Section titled “Six structuring decisions”Taken in a scoping session before the first line of configuration, they explain almost the entire shape of the deployment.
| Decision | Reason |
|---|---|
LLM direct through openai-codex, bypassing cli-ollama | The subscription is consumed natively by the Hermes provider. Interposing the gateway would add a hop, a timeout and a loss of context for zero gain |
| Dual-channel N8N access | One channel of curated business tools, one read-only diagnostic channel. The separation is explicit, not implicit |
| Telegram bot only | No port, no Caddy route, no dashboard: the HTTP attack surface is nil |
Service in ai-stack/docker-compose.yaml | Cohabits with Qdrant and cli-ollama, same internal networks, same lifecycle |
| Reactive only in v1 | The scheduler and automations stay disabled: an agent that acts unprompted is hard to audit until you trust its guardrails |
| Data dir in the daily backup | All accumulated value (memory, sessions, skills) lives in a single directory — losing it means starting over |
Why two MCP channels rather than one?
Section titled “Why two MCP channels rather than one?”The administrator MCP server (n8n-mcp) exposes 24 tools, including n8n_delete_workflow and n8n_manage_credentials. Wiring them as-is would hand the agent the keys to the entire automation estate just so it could read an execution log.
So the deployment separates them:
n8n-tools— the normal flow. Hand-written business tools with typed inputs, exposed by a dedicated N8N workflow. This is what the agent uses 99% of the time.n8n-admin— filtered down to 4 read-only diagnostic tools (n8n_list_workflows,n8n_get_workflow,n8n_executions,n8n_validate_workflow), and restricted by a system prompt rule to workflow debugging alone.
Why external memory?
Section titled “Why external memory?”Hermes’ native memory is a MEMORY.md file re-injected into the prompt. Simple, but it plateaus: no search, and a character budget that forces a trade-off between keeping everything and staying readable.
Mnemosyne replaces that mechanism with a local SQLite database featuring hybrid search (vector + FTS5) and background capture on every turn. Local-first: no conversation data leaves the VPS to be vectorised.
3. How? — Technical implementation
Section titled “3. How? — Technical implementation”The Docker service
Section titled “The Docker service”hermes: image: nousresearch/hermes-agent:latest container_name: hermes command: gateway run networks: - n8n-internal # MCP Server Trigger (n8n:5678) - mcp-backend # n8n-mcp:3000 (debug only) environment: - PUID=${CLAUDE_USER_ID:-1000} - PGID=${CLAUDE_GROUP_ID:-1000} - TELEGRAM_BOT_TOKEN=${HERMES_TELEGRAM_BOT_TOKEN} - TELEGRAM_ALLOWED_USERS=${HERMES_ALLOWED_CHAT_ID} - HERMES_N8N_MCP_TOKEN=${HERMES_N8N_MCP_TOKEN} - N8N_MCP_AUTH_TOKEN=${N8N_MCP_AUTH_TOKEN} volumes: - ./hermes/data:/opt/data security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - CHOWN - SETUID - SETGID - DAC_OVERRIDE deploy: resources: limits: memory: 3G cpus: '2' healthcheck: test: ['CMD-SHELL', 'pgrep -f "bin/hermes[ ]gateway run" > /dev/null || exit 1'] interval: 30sNo ports section: that is deliberate and it is the heart of the security model. The only inbound path is outbound Telegram long-polling.
Configuration
Section titled “Configuration”Hermes generates and migrates its own config.yaml on first start; only this deployment’s keys are carried over by hand.
model: provider: "openai-codex" default: "gpt-5.5" base_url: "https://chatgpt.com/backend-api/codex"
mcp_servers: n8n-tools: # normal flow url: "http://n8n:5678/mcp/hermes-tools" headers: Authorization: "Bearer ${HERMES_N8N_MCP_TOKEN}" timeout: 120 n8n-admin: # workflow debugging ONLY url: "http://n8n-mcp:3000/mcp" headers: Authorization: "Bearer ${N8N_MCP_AUTH_TOKEN}" tools: include: [n8n_list_workflows, n8n_get_workflow, n8n_executions, n8n_validate_workflow]
memory: provider: "mnemosyne"
plugins: enabled: - telegram-voice-transcriptor
gateway: platforms: telegram: extra: allow_from: - "<CHAT_ID>" # LITERAL value requiredThe system prompt as a security layer
Section titled “The system prompt as a security layer”Some rules cannot be enforced by plumbing: they live in SOUL.md, occupying the first slot of the system prompt.
- Normal flow: every business action goes through
n8n-tools. n8n-admin= debug only, never for a business action, and never to modify a workflow.docker_manage= sensitive action: no approval chain exists downstream, so the agent must ask for explicit confirmation before everystart/stop/restart/update, naming the stack and the action.- Vault: always cite the source file path; writes only through
vault_write, confined, and never on its own initiative. - No unsolicited action: the agent does what is asked and offers the rest.
What plugs into the gateway
Section titled “What plugs into the gateway”Three extension mechanisms coexist, and the distinction between them structures the whole deployment.
| Mechanism | Nature | Where it lives | Detail |
|---|---|---|---|
| MCP tools | N8N nodes | Hermes MCP Tools workflow | 32 tools across 8 domains |
| Skills | Markdown | /opt/data/skills/ | 145 active, 12 of them custom |
| Plugins | Python | /opt/data/plugins/ | 4 plugins, hooked into the lifecycle |
The dividing rule: an MCP tool makes an action possible, a skill makes it well done, a plugin triggers it without the agent deciding. And long-term memory — Mnemosyne — installs through the third mechanism, in wrapper mode.
Security
Section titled “Security”| Surface | Protection |
|---|---|
| N8N MCP endpoint | Bearer required — 403 without a token, verified end-to-end |
| MCP endpoint from the Internet | /mcp/* and /mcp-test/* return 404 at Caddy; only the internal n8n:5678 path works |
| Telegram bot | Strict fail-closed allowlist — an unknown chat_id is blocked without a reply |
| Destructive Docker actions | Action whitelist in Docker Actions, security-stack not actionable |
docker_manage, crm_delete_lead, timesheet_delete | No downstream approval chain → confirmation required by the system prompt |
| Vault writes | Confined to research/ and inbox/, .md extension, anti-traversal, 150,000-byte cap |
| Secrets | Passed through the compose environment, never written into /opt/data (which goes into the backup) |
The confinement of vault_write deserves a word, because it solves a classic problem with agents that write: blind overwrites. A read through vault_read returns a contentHash; modifying an existing file requires passing that hash back as Base_Hash. Without a hash, the reply is __EXISTS__; if the file has moved since the read, __STALE_READ__. And errors never return the current hash — otherwise the model could harvest it and overwrite without having read. Implementation detail.
Backup
Section titled “Backup”The data directory goes into the daily backup to Google Drive, after exclusions: OAuth credentials (auth.json, google_token.json, google_client_secret.json), Git credentials, the Mnemosyne venv and the model caches. The memory database, however, is included. The archive thus drops to 8.2 MB instead of 58 MB.
After a full restore, three manual steps: redo the Codex device flow, redo the Google OAuth flow, reinstall the Mnemosyne venv.
Operations commands
Section titled “Operations commands”docker exec hermes hermes mcp test n8n-tools # live MCP connection + tool listdocker exec hermes hermes auth list # OAuth credential loaded?docker exec hermes hermes plugins list # plugins enabled/disableddocker exec hermes hermes memory status # active memory providerdocker exec hermes hermes gateway statusdocker exec hermes hermes -z "ping" # one-shot LLM turn (consumes quota)4. What if? — Outlook and limits
Section titled “4. What if? — Outlook and limits”Current limits
Section titled “Current limits”| Limit | Impact | Mitigation |
|---|---|---|
| Shared ChatGPT quota | The subscription serves Hermes, Codex CLI and the N8N workflows. Cap reached = the bot returns errors until reset | codex-usage-alert plugin on a threshold; route non-interactive usage to gemini-flash |
| Guardrails carried by the prompt | docker_manage and deletions have no technical validation downstream | Action whitelist in Docker Actions; a real Telegram approval chain remains to be wired |
| Reactive v1 | No scheduled task: the agent does nothing unprompted | Deliberate until the guardrails are hardened |
Hard-coded employee_id | timesheet_log_hours always writes against employee 1 | Moot today (a single employee), to be parameterised if the team grows |
| CRM stage mapping | Stage IDs are encoded in the $fromAI descriptions | crm_list_stages now provides the dynamic source |
| MCP sub-nodes cannot be disabled | Disabling a tool in Hermes MCP Tools makes every subsequent tool execute the wrong node, silently | Delete the node rather than disable it (detail) |
Evolution scenarios
Section titled “Evolution scenarios”If the agent becomes proactive:
- Dropping jobs into
/opt/data/cron/activates the scheduler (inactive while it is empty). - Prerequisite: a real approval chain on sensitive actions, not just a prompt rule.
- Natural candidates: morning digest of Prometheus alerts, follow-up on dormant CRM leads.
If memory has to leave the VPS:
- Export Mnemosyne memories as markdown notes into the vault, to make them readable and versioned.
- Then bidirectional sync VPS ↔ workstation, for a single memory shared between the agent and the local assistant. Detail.
If the write scope widens:
- The
research/+inbox/confinement is a whitelist of prefixes: extending it is trivial, but every new prefix must come with its justification. - The commit + immediate push sequence already propagates to the Obsidian clients; raising the write throughput will raise the noise in the Git history.
If a second user needs access:
- The allowlist accepts several chat_ids, but memory and the user profile are global to the container.
- It would take one container per user (separate data directories) rather than a widened allowlist.
Troubleshooting commands
Section titled “Troubleshooting commands”# The bot is not respondingdocker logs hermes --tail 100docker inspect hermes --format='{{.State.Health.Status}}'
# Message ignored → allowlistdocker logs hermes 2>&1 | grep -i "unauthorized"
# MCP tools missing or staledocker exec hermes hermes mcp test n8n-toolsdocker compose -f ai-stack/docker-compose.yaml restart hermes
# Memory unavailabledocker exec hermes hermes memory statusdocker exec --user hermes -e HOME=/opt/data -e HERMES_HOME=/opt/data hermes \ /opt/data/.mnemosyne/venv/bin/mnemosyne stats
# Check the MCP endpoint is still closed from outsidecurl -so /dev/null -w '%{http_code}\n' https://n8n.guigpap.com/mcp/hermes-tools # expected: 404Related pages
Section titled “Related pages”Hermes
Section titled “Hermes”- The 32 MCP tools — Catalogue, gateways and confined writes
- Mnemosyne long-term memory — Wrapper mode, hybrid recall
- Custom skills — Twelve procedures written for this deployment
- Plugins and their hooks — Four Python extensions of the gateway
Infrastructure
Section titled “Infrastructure”- AI Stack — Qdrant, CLI Ollama and the MCP gateway
- VPS architecture — Overview and network topology
- Security Stack — Caddy, external blocking of internal endpoints
- Database backup — Backing up the data directory
Workflows
Section titled “Workflows”- Content Pipeline — The
vps-vaultObsidian vault and its mirrors - Conversational system — The other Telegram agent, through CLI Ollama
- Codex CLI Integration —
cli-ollama’s Codex login, distinct from Hermes’ - Error Handler — Error workflow for the gateways
Reference
Section titled “Reference”- Glossary — MCP, Autonomous agent, Long-term memory, LLM