From 7cac4824ef1d0eaa3bd4061f0b504a0356298e0f Mon Sep 17 00:00:00 2001 From: Charlie Obien Date: Wed, 10 Jun 2026 11:34:12 +0800 Subject: [PATCH] fix(docs): align guide with current Hermes runtime --- docs/quickstart.md | 16 +- docs/reference-architectures/road-warrior.md | 172 +++++---- docs/reference-architectures/small-agency.md | 7 +- docs/wizard/index.html | 64 +--- part17-mcp-servers.md | 179 +++++---- part19-security-playbook.md | 126 +++---- part21-remote-sandboxes.md | 375 ++++++++----------- skills/security/audit-mcp/SKILL.md | 76 ++-- templates/config/production.yaml | 47 +-- templates/config/security-hardened.yaml | 5 +- templates/systemd/hermes-dashboard.service | 3 +- templates/systemd/hermes.service | 3 +- 12 files changed, 481 insertions(+), 592 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 9c6b9ef..1712928 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -54,7 +54,9 @@ chmod 600 ~/.hermes/.env ## Step 5 — Start it ```bash -hermes run & +hermes gateway install +hermes gateway start +hermes gateway status ``` DM your bot. It should reply in seconds. @@ -87,9 +89,9 @@ Now try: ## Common first-hour issues -| Symptom | Fix | -|---|---| -| Bot doesn't respond | `journalctl --user -u hermes` — 99% of the time it's a missing env var | -| 401 from Anthropic | Check `ANTHROPIC_API_KEY` has no trailing newline: `cat -A ~/.hermes/.env` | -| "skill not found: /cost-report" | `hermes /reload` after symlinking skills | -| Replies are slow | You're on Anthropic free tier — rate-limited. Upgrade or route to Gemini Flash via the `cost-optimized` template | +| Symptom | Fix | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Bot doesn't respond | `hermes gateway status` — 99% of the time it's a missing env var | +| 401 from Anthropic | Check `ANTHROPIC_API_KEY` has no trailing newline: `cat -A ~/.hermes/.env` | +| "skill not found: /cost-report" | `hermes /reload` after symlinking skills | +| Replies are slow | You're on Anthropic free tier — rate-limited. Upgrade or route to Gemini Flash via the `cost-optimized` template | diff --git a/docs/reference-architectures/road-warrior.md b/docs/reference-architectures/road-warrior.md index 2a64b44..ad3b532 100644 --- a/docs/reference-architectures/road-warrior.md +++ b/docs/reference-architectures/road-warrior.md @@ -1,6 +1,6 @@ # Reference Architecture: Road Warrior -**Phone drives, disposable cloud boxes do the heavy lifting.** Inspired by [Part 21](../../part21-remote-sandboxes.md). You carry a tiny $5 always-on VPS; it orchestrates Modal / Daytona / Fly sandboxes that spin up on demand for real work. +**Phone drives, remote execution does the heavy lifting.** Inspired by [Part 21](../../part21-remote-sandboxes.md). You carry a tiny always-on VPS for gateways, memory, approvals, and Kanban; real work runs through terminal backends, worktrees, or external integrations. ## Who this is for @@ -10,40 +10,42 @@ ## Cost -- **Always-on driver box:** $5/mo (Hetzner CX22) -- **On-demand remote compute:** $0–50/mo (only pay when you're actually running things) -- **LLM:** $20–60/mo +- **Always-on driver box:** about $5/mo +- **On-demand remote compute:** $0-50/mo, depending on how often workers run +- **LLM:** $20-60/mo ## Architecture -``` - Phone (Telegram) ──→ Driver VPS ($5/mo, always-on) - │ - │ hermes.service - │ remote_sandbox: modal (default) - │ - ▼ - On-demand sandbox: - Modal (GPU-ish) - Daytona (full dev env) - Fly Machines (persistent) - E2B (Python sandbox) - SSH (your own beast) +```text +Phone (Telegram) ---> Driver VPS (always on) + | + | hermes gateway + | memory, skills, MCP config + | Kanban board + | terminal.backend: ssh/docker/modal/daytona/local + | + v + Execution targets: + SSH dev box or home workstation + Docker/Singularity container + Modal/Daytona backend, when supported + External Vercel/Fly/E2B/CI integration through CLI/MCP/skills ``` -Your phone → Telegram → 5¢/mo VPS → spins up a $0.05/hr Modal sandbox → runs Claude Code, pulls the repo, does the work → syncs files back on teardown → pushes PR. +Your phone talks to Hermes on the driver VPS. Hermes creates a worktree or Kanban task, sends shell/file work to the configured terminal backend, runs the coding agent or skill, then reports the diff, test result, PR link, or external artifact back to chat. ## Parts list -- **Hetzner CX22** as the driver ($5/mo) -- **Modal account** (free $30/mo credits) OR **Daytona** OR **Fly Machines** — see [Part 21](../../part21-remote-sandboxes.md) +- **Driver VPS** such as Hetzner CX22, Fly machine, Render, Railway, or a tiny homelab box +- **Execution target:** SSH dev box, Docker/Singularity, or Modal/Daytona if supported by your Hermes build +- **External integrations:** Vercel, Fly Machines, E2B, and CI through vendor CLIs, MCP servers, or custom skills - **Telegram bot** + your user ID -- **API keys:** Anthropic (for Claude Code inside sandbox), optional Google (for Hermes triage on the driver) +- **API keys:** model providers for Hermes and any coding agents you choose to run ## Install ```bash -# On the driver VPS — as root +# On the driver VPS - as root curl -sSL https://raw.githubusercontent.com/OnlyTerp/hermes-optimization-guide/main/scripts/vps-bootstrap.sh | bash ``` @@ -54,12 +56,12 @@ Then customize: version: 1 models: - default: google/gemini-3.1-flash # Cheap + fast for "plan the work" phase + default: google/gemini-3.1-flash # Cheap + fast for triage providers: google: api_key: "${GOOGLE_API_KEY}" anthropic: - api_key: "${ANTHROPIC_API_KEY}" # Used by sandboxed Claude Code + api_key: "${ANTHROPIC_API_KEY}" # Optional: used by Claude Code or routed model calls gateways: cli: { enabled: true } @@ -71,84 +73,102 @@ gateways: allowed_user_ids: - ${TELEGRAM_OWNER_ID} -# The money section -remote_sandbox: - default_backend: modal # Or daytona / fly / e2b / ssh - backends: - modal: - token_id: "${MODAL_TOKEN_ID}" - token_secret: "${MODAL_TOKEN_SECRET}" - image: "python:3.12-slim" - timeout_idle: 600 # 10m idle → auto-shutdown - ssh: # your home beast, if any - host: "beast.tailnet-xxx.ts.net" - user: "hermes" - identity_file: "~/.ssh/id_ed25519" - -# Hermes loads skills from here; these let you orchestrate from Telegram +# The execution section. Use one backend at a time. +terminal: + backend: ssh # local | docker | singularity | modal | daytona | ssh + ssh_host: "beast.tailnet-xxx.ts.net" + ssh_user: "hermes" + ssh_port: 22 + ssh_key: "~/.ssh/id_ed25519" + +# Hermes loads skills from here; use them to create Kanban cards or call vendor CLIs/MCP tools. skills: allowlist: - pr-review - release-notes - cost-report - - remote-run # triggers a sandbox + - remote-run ``` -## The workflow +For a fully local-but-contained driver, switch the terminal block to Docker: + +```yaml +terminal: + backend: docker + docker_image: nikolaik/python-nodejs:python3.11-nodejs20 + cwd: /workspace + docker_mount_cwd_to_workspace: false +``` +For Modal or Daytona, use only the backend keys your installed Hermes build exposes. Verify after config edits: + +```bash +hermes config check +hermes doctor ``` + +## The workflow + +```text you: "@bot fix the null-check in auth.ts" -bot: [spinning up modal sandbox…] -bot: cloned acme/app, branch devin-123 -bot: claude code: analyzing… -bot: [file diff preview, 3 lines] +bot: created Kanban task #342 on branch wt/auth-null-check +bot: worker claimed an isolated worktree +bot: terminal backend: ssh -> beast.tailnet-xxx.ts.net +bot: codex-worker: running tests... +bot: diff preview: 3 lines changed Approve? /yes /no /changes you: /yes -bot: [syncing files back, running tests] -bot: tests green. Pushed PR #342 → https://… -bot: sandbox torn down (ran 4m 12s, $0.014) +bot: tests green. Pushed PR #342 -> https://... ``` -## Key wins from Part 21 + PR #8018 +The important bit: the source of truth is git. Hermes is not starting a native sandbox and syncing files back on teardown; it is coordinating worktrees, terminal execution, worker logs, tests, and PRs. -- **Bulk tar-pipe sync** — 30s cold start beats 5 minutes of 100× `scp` -- **SIGINT-safe sync-back** — lose signal mid-run, the sandbox still flushes on teardown -- **Hash-only sync** — only changed files come back, not the whole tree -- **Local `git push`** — the driver VPS keeps your authenticated git creds; sandbox never sees them +## Kanban setup -## Skill setup +Create durable work from chat, CLI, or the dashboard: ```bash -# Symlink all the guide skills -for s in /opt/hermes-optimization-guide/skills/*/*/; do - ln -sfn "$s" "/home/hermes/.hermes/skills/$(basename $s)" -done - -# Write a tiny remote-run skill (paste into ~/.hermes/skills/remote-run/SKILL.md) -# that wraps `hermes sandbox run --repo acme/app -- claude -p "$@"` -hermes /reload +hermes kanban create "Fix the null-check in auth.ts and open a PR" \ + --assignee codex-worker \ + --workspace worktree \ + --branch wt/auth-null-check + +hermes kanban dispatch --max 1 ``` +A `remote-run` skill should wrap one of these real surfaces: + +- create a Kanban card with `--workspace worktree`; +- run a coding agent through a configured terminal backend; +- call a vendor CLI/API such as `vercel`, `flyctl`, or an E2B MCP server; +- return logs, artifact URLs, test status, and PR links. + +It should not wrap a sandbox CLI subcommand; that interface is not part of the current CLI. + ## Safety rails -- Sandbox = **quarantine profile** (as if it were untrusted input) — Claude Code in the sandbox cannot touch the driver's MCP servers or secrets -- Driver has read-only GitHub PAT (for triage/search) -- The **write** PAT only exists inside the sandbox, short-lived, piped through stdin so it's never on disk +- Terminal backend isolation confines shell/file-tool activity, not every in-process Hermes component. Use [Part 19](../../part19-security-playbook.md) for whole-process containment guidance. +- Keep the driver VPS as the owner of gateways, approvals, memory, and durable Kanban state. +- Pass only the minimum credentials to the remote execution target. Prefer vendor secret stores, short-lived tokens, and PR-based review. +- Use `hermes --worktree` or Kanban `--workspace worktree` for parallel coding work so agents do not collide in one checkout. +- Treat "worker ran" as evidence, not completion. Require tests, review, and a clean branch before calling work done. ## Costs in the wild -Typical month for an active user: +Typical month for an active solo user: -| Line | Cost | -|---|---:| -| CX22 driver | $5 | -| Modal compute (3h/day × 30 days × $0.05/h) | $4.50 | -| Anthropic (Claude Code, routed) | $20–40 | -| Google Gemini Flash (triage) | ~$0.50 | -| **Total** | **~$30–50/mo** | +| Line | Cost | +| -------------------------------- | ----------------: | +| Driver VPS | $5 | +| SSH home box | existing hardware | +| Modal/Daytona/Fly/E2B usage | $0-50 | +| Premium coding-agent model calls | $20-60 | +| Cheap triage model calls | <$5 | +| **Total** | **~$30-120/mo** | ## When to graduate -- You're running 10+ sandbox hours a day → migrate to a persistent Fly Machine + scale up -- You need GPU in the sandbox → Modal A10G is ~$1.10/hr, still cheap for spot usage -- You want *multi-user* → [Small Agency](./small-agency.md) +- You run remote jobs all day -> move the execution target to a persistent dev box or a larger VPS. +- You need GPU bursts -> use Modal, a GPU SSH host, or a CI/vendor runner skill. +- You need vendor-native previews -> call Vercel/Fly/E2B through skills, MCP, or CI instead of pretending they are Hermes-native backends. +- You want multi-user operations -> [Small Agency](./small-agency.md). diff --git a/docs/reference-architectures/small-agency.md b/docs/reference-architectures/small-agency.md index 2398698..f10575f 100644 --- a/docs/reference-architectures/small-agency.md +++ b/docs/reference-architectures/small-agency.md @@ -56,8 +56,9 @@ After=network-online.target Type=simple User=%i WorkingDirectory=/home/%i -ExecStart=/usr/local/bin/hermes run +ExecStart=/usr/bin/env hermes gateway run EnvironmentFile=-/home/%i/.hermes/.env +Environment=PATH=/home/%i/.local/bin:/home/%i/.cargo/bin:/usr/local/bin:/usr/bin:/bin # ... all the hardening bits from templates/systemd/hermes.service [Install] @@ -80,10 +81,10 @@ systemctl enable --now hermes@alice.service ## Per-client separation - **`profile:`** in the Hermes config — `quarantine` (untrusted input for a public bot) vs `trusted` (the dev's admin DM) -- **Approval channels** — the dev's DM is the only trusted approval source; client support channels are *never* trusted +- **Approval channels** — the dev's DM is the only trusted approval source; client support channels are _never_ trusted - **LightRAG dirs** — `~/.hermes/lightrag-/` per client; never mix - **MCP** — per-client read-only PATs (`GITHUB_PAT_CLIENT_A`, `GITHUB_PAT_CLIENT_B`) -- **Audit log** — append-only JSONL per session, centralized to a single append-only bucket the dev can *read* but not *delete* (makes client reviews easy) +- **Audit log** — append-only JSONL per session, centralized to a single append-only bucket the dev can _read_ but not _delete_ (makes client reviews easy) ## Cost routing at agency scale diff --git a/docs/wizard/index.html b/docs/wizard/index.html index 3292f49..30d47c1 100644 --- a/docs/wizard/index.html +++ b/docs/wizard/index.html @@ -397,54 +397,35 @@

Hermes Config Wizard

lines.push(``); } - // MCP servers + // MCP servers are CLI-managed. The wizard emits comment-only companion + // commands so Hermes owns the persisted MCP registry/config shape. const anyMcp = Object.values(mcps).some(Boolean); if (anyMcp) { - lines.push(`mcp_servers:`); + lines.push(`# MCP servers are managed with Hermes CLI commands.`); + lines.push(`# Run selected commands after saving this file:`); if (mcps.github) { - lines.push(` github:`); - lines.push(` command: npx`); - lines.push(` args: [-y, "@modelcontextprotocol/server-github"]`); - lines.push(` env:`); - lines.push(` GITHUB_PERSONAL_ACCESS_TOKEN: "$\{GITHUB_PAT\}"`); - lines.push(` trust: trusted`); - lines.push(` allow_sampling: false`); + lines.push(`# hermes mcp add github --command npx --env GITHUB_PERSONAL_ACCESS_TOKEN=$\{GITHUB_PAT\} --args -y @modelcontextprotocol/server-github`); } if (mcps.postgres) { - lines.push(` postgres:`); - lines.push(` command: npx`); - lines.push(` args: [-y, "@modelcontextprotocol/server-postgres", "$\{DATABASE_URL\}"]`); - lines.push(` trust: trusted`); - lines.push(` allow_sampling: false`); + lines.push(`# hermes mcp add postgres --command npx --args -y @modelcontextprotocol/server-postgres "$\{DATABASE_URL\}"`); } if (mcps.cloudflare) { - lines.push(` cloudflare:`); - lines.push(` command: npx`); - lines.push(` args: [-y, "@cloudflare/mcp-server-cloudflare"]`); - lines.push(` env:`); - lines.push(` CLOUDFLARE_API_TOKEN: "$\{CLOUDFLARE_API_TOKEN\}"`); - lines.push(` trust: trusted`); + lines.push(`# hermes mcp add cloudflare --command npx --env CLOUDFLARE_API_TOKEN=$\{CLOUDFLARE_API_TOKEN\} --args -y @cloudflare/mcp-server-cloudflare`); } if (mcps.linear) { - lines.push(` linear:`); - lines.push(` url: https://mcp.linear.app/mcp`); - lines.push(` # OAuth is completed by the MCP client on first connection.`); - lines.push(` trust: trusted`); - lines.push(` allow_sampling: false`); + lines.push(`# hermes mcp add linear --url https://mcp.linear.app/mcp --auth oauth`); } if (mcps.filesystem) { - lines.push(` filesystem:`); - lines.push(` command: npx`); - lines.push(` args: [-y, "@modelcontextprotocol/server-filesystem", "/home/hermes/scratch"]`); - lines.push(` trust: trusted`); + lines.push(`# hermes mcp add filesystem --command npx --args -y @modelcontextprotocol/server-filesystem /home/hermes/scratch`); } if (mcps.mem0) { - lines.push(` mem0:`); - lines.push(` command: npx`); - lines.push(` args: [-y, "@mem0ai/mcp-server-mem0"]`); - lines.push(` env:`); - lines.push(` MEM0_API_KEY: "$\{MEM0_API_KEY\}"`); + lines.push(`# hermes mcp add mem0 --command npx --env MEM0_API_KEY=$\{MEM0_API_KEY\} --args -y @mem0ai/mcp-server-mem0`); } + lines.push(`# Configure auth/tool filters as needed:`); + lines.push(`# hermes mcp configure NAME`); + lines.push(`# Verify registrations:`); + lines.push(`# hermes mcp test NAME`); + lines.push(`# hermes mcp list`); lines.push(``); } @@ -473,18 +454,6 @@

Hermes Config Wizard

lines.push(` - '169\\.254\\.169\\.254'`); lines.push(` - 'cat\\s+~?/?\\.?ssh/'`); - // Persona-specific security blocks - if (persona === 'security-hardened') { - lines.push(` mcp:`); - lines.push(` default_trust: untrusted`); - lines.push(` require_allowlist: true`); - lines.push(` allow_sampling: false`); - } else if (anyMcp) { - lines.push(` mcp:`); - lines.push(` default_trust: trusted`); - lines.push(` allow_sampling: false`); - } - if (persona === 'security-hardened' || persona === 'production') { lines.push(` webhook:`); lines.push(` max_body_bytes: 1048576`); @@ -495,10 +464,9 @@

Hermes Config Wizard

lines.push(``); lines.push(`profiles:`); lines.push(` quarantine:`); - lines.push(` # Low-trust gateways route here: no memory writes, no MCP, read-only tools`); + lines.push(` # Low-trust gateways route here: no memory writes, read-only tools`); lines.push(` allow_memory_write: false`); lines.push(` allow_send: false`); - lines.push(` mcp_servers: []`); lines.push(` toolsets: [classify]`); } lines.push(``); diff --git a/part17-mcp-servers.md b/part17-mcp-servers.md index 07e07a5..0b969a2 100644 --- a/part17-mcp-servers.md +++ b/part17-mcp-servers.md @@ -1,6 +1,6 @@ # Part 17: MCP Servers — Give Hermes Any Tool With Zero Glue Code -*Model Context Protocol (MCP) is the "USB-C of AI agents" — a standard way for any tool server to plug into any agent. Hermes has supported MCP natively since [v0.7.0](https://github.com/NousResearch/hermes-agent/releases/tag/v2026.4.3). This is the part of the guide nobody reads until they realize they can stop writing tool adapters by hand.* +_Model Context Protocol (MCP) is the "USB-C of AI agents" — a standard way for any tool server to plug into any agent. Hermes has supported MCP natively since [v0.7.0](https://github.com/NousResearch/hermes-agent/releases/tag/v2026.4.3). This is the part of the guide nobody reads until they realize they can stop writing tool adapters by hand._ --- @@ -42,64 +42,56 @@ Hermes auto-discovers tools at startup and subscribes to dynamic updates — if ## Configuration -MCP servers live under the `mcp_servers` key in `~/.hermes/config.yaml`. +MCP servers are managed by the Hermes CLI. Hermes may persist MCP entries in its own config/registry, but operators should treat `hermes mcp add/list/test/configure` as the source of truth instead of hand-authoring an `mcp_servers:` block from old examples. + +Use this loop for every server: + +```bash +hermes mcp add NAME [--url URL] [--command MCP_COMMAND] [--args ...] [--auth oauth|header] [--preset PRESET] [--env KEY=VALUE] +hermes mcp test NAME +hermes mcp list +``` + +Use `hermes mcp configure NAME` when a server needs auth, tool filtering, or other CLI-managed settings. ### stdio Servers (Local Subprocess) -```yaml -mcp_servers: - github: - command: npx - args: ["-y", "@modelcontextprotocol/server-github"] - env: - GITHUB_PERSONAL_ACCESS_TOKEN: ${GITHUB_TOKEN} - - filesystem: - command: npx - args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/you/projects"] - - postgres: - command: npx - args: ["-y", "@modelcontextprotocol/server-postgres", "${DATABASE_URL}"] +```bash +hermes mcp add github --command npx --env GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_TOKEN} --args -y @modelcontextprotocol/server-github +hermes mcp add filesystem --command npx --args -y @modelcontextprotocol/server-filesystem /home/you/projects +hermes mcp add postgres --command npx --args -y @modelcontextprotocol/server-postgres "$DATABASE_URL" + +hermes mcp test github +hermes mcp list ``` -Hermes spawns the subprocess on startup, pipes JSON-RPC over stdio, and unspawns it on exit. Restart Hermes after adding a new stdio server. +Hermes spawns stdio servers as CLI-managed subprocesses, pipes JSON-RPC over stdio, and unspawns them on exit. Use `hermes mcp test NAME` immediately after adding a server so command, args, env, and tool discovery fail fast. ### HTTP / SSE Servers (Remote) -```yaml -mcp_servers: - mem0: - url: https://mcp.mem0.ai/sse - headers: - Authorization: Bearer ${MEM0_API_KEY} - - cloudflare: - url: https://observability.mcp.cloudflare.com/sse - headers: - Authorization: Bearer ${CLOUDFLARE_API_TOKEN} +```bash +hermes mcp add mem0 --url https://mcp.mem0.ai/sse --auth header +hermes mcp configure mem0 +hermes mcp test mem0 + +hermes mcp add linear --url https://mcp.linear.app/mcp --auth oauth +hermes mcp configure linear +hermes mcp test linear ``` -HTTP servers can add/remove tools live. Hermes handles reconnection with exponential backoff. +HTTP servers can add/remove tools live. Hermes handles reconnection with exponential backoff. For header or OAuth auth, register the transport first, then use `hermes mcp configure NAME` so secrets stay in Hermes-managed state instead of pasted into guide YAML. ### Scoped Enablement Some servers are chatty — you don't want every tool they expose loaded into every conversation. Scope them: -```yaml -mcp_servers: - postgres: - command: npx - args: ["-y", "@modelcontextprotocol/server-postgres", "${DATABASE_URL}"] - enabled_for: # Only load in these sessions - - profile: engineering - - channel: "#data-questions" - tools_allowlist: # Only expose these tools - - query - - describe_table +```bash +hermes mcp configure postgres +hermes mcp test postgres +hermes mcp list ``` -Without a `tools_allowlist`, every tool the server exposes is available. +Use the configure flow to restrict auth, profiles, roots, or exposed tools when your Hermes build exposes those controls. If the CLI does not show a restriction you need, leave the server unregistered or keep it in a lower-trust Hermes home instead of inventing YAML keys. --- @@ -107,24 +99,24 @@ Without a `tools_allowlist`, every tool the server exposes is available. These are the ones that pay for themselves within a day: -> **2026 reality check:** MCP is also a supply-chain boundary. Prefer official servers, pin package versions, restrict filesystem roots, and keep `allow_sampling: false` unless the server genuinely needs to call an LLM. - -| Server | What it adds | Why you want it | -|--------|--------------|-----------------| -| **@modelcontextprotocol/server-github** | Issues, PRs, repo search, branch diffs | Hermes becomes a code-aware teammate | -| **@modelcontextprotocol/server-filesystem** | Scoped file reads/writes/search | Safer than giving terminal access | -| **@modelcontextprotocol/server-postgres** | Read-only SQL | Answer "what's in the db?" without exposing DSN | -| **@modelcontextprotocol/server-sqlite** | Local SQLite analysis | Great for log files, analytics snapshots | -| **@modelcontextprotocol/server-puppeteer** | Browser automation | Complement to the Tool Gateway's Browser Use; sandbox it tightly | -| **@modelcontextprotocol/server-memory** | Knowledge-graph memory | Pairs with [Part 3 LightRAG](./part3-lightrag-setup.md) for redundancy | -| **mcp.mem0.ai** | Hosted long-term memory | Cross-device memory across Hermes + Claude Code | -| **Cloudflare Observability MCP** | Query your Worker logs/analytics | If you run anything on Cloudflare | -| **@supabase/mcp-server-supabase** | Supabase RPC + Postgres + storage | One config for a whole backend | -| **linear-mcp** | Linear issue CRUD | Turn Hermes into an issue assignee | -| **stripe-mcp** | Stripe reads (customers, subs) | Support triage from Telegram | -| **@notionhq/notion-mcp-server** | Notion pages + databases | Company wiki as grounded context | -| **@browserbase/mcp** | Headless browser-as-a-service | Scraping sites Firecrawl can't handle | -| **@chroma-core/chroma-mcp** | ChromaDB vectors | Works alongside LightRAG | +> **2026 reality check:** MCP is also a supply-chain boundary. Prefer official servers, pin package versions, restrict filesystem roots, and review sampling-capable servers before enabling or keeping them registered. + +| Server | What it adds | Why you want it | +| ------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------- | +| **@modelcontextprotocol/server-github** | Issues, PRs, repo search, branch diffs | Hermes becomes a code-aware teammate | +| **@modelcontextprotocol/server-filesystem** | Scoped file reads/writes/search | Safer than giving terminal access | +| **@modelcontextprotocol/server-postgres** | Read-only SQL | Answer "what's in the db?" without exposing DSN | +| **@modelcontextprotocol/server-sqlite** | Local SQLite analysis | Great for log files, analytics snapshots | +| **@modelcontextprotocol/server-puppeteer** | Browser automation | Complement to the Tool Gateway's Browser Use; sandbox it tightly | +| **@modelcontextprotocol/server-memory** | Knowledge-graph memory | Pairs with [Part 3 LightRAG](./part3-lightrag-setup.md) for redundancy | +| **mcp.mem0.ai** | Hosted long-term memory | Cross-device memory across Hermes + Claude Code | +| **Cloudflare Observability MCP** | Query your Worker logs/analytics | If you run anything on Cloudflare | +| **@supabase/mcp-server-supabase** | Supabase RPC + Postgres + storage | One config for a whole backend | +| **linear-mcp** | Linear issue CRUD | Turn Hermes into an issue assignee | +| **stripe-mcp** | Stripe reads (customers, subs) | Support triage from Telegram | +| **@notionhq/notion-mcp-server** | Notion pages + databases | Company wiki as grounded context | +| **@browserbase/mcp** | Headless browser-as-a-service | Scraping sites Firecrawl can't handle | +| **@chroma-core/chroma-mcp** | ChromaDB vectors | Works alongside LightRAG | For the full catalog, see the [MCP Registry](https://registry.modelcontextprotocol.io/) and the `awesome-mcp-servers` list on GitHub. @@ -141,19 +133,21 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" const server = new Server( { name: "my-mcp", version: "0.1.0" }, - { capabilities: { tools: {} } } + { capabilities: { tools: {} } }, ); server.setRequestHandler("tools/list", async () => ({ - tools: [{ - name: "deploy_staging", - description: "Deploys current git HEAD to the staging environment", - inputSchema: { - type: "object", - properties: { service: { type: "string" } }, - required: ["service"] - } - }] + tools: [ + { + name: "deploy_staging", + description: "Deploys current git HEAD to the staging environment", + inputSchema: { + type: "object", + properties: { service: { type: "string" } }, + required: ["service"], + }, + }, + ], })); server.setRequestHandler("tools/call", async (req) => { @@ -168,11 +162,9 @@ await server.connect(new StdioServerTransport()); Register it: -```yaml -mcp_servers: - ops: - command: node - args: ["/home/you/mcp/my-mcp/index.js"] +```bash +hermes mcp add ops --command node --args /home/you/mcp/my-mcp/index.js +hermes mcp test ops ``` Now `deploy_staging` is a tool Hermes can call from any surface — CLI, Telegram, iMessage, Discord — without touching Hermes' code. @@ -187,28 +179,24 @@ This is MCP's killer feature and the reason it matters for agents specifically. - A security-review MCP reads a diff → asks the LLM to classify severity → returns a triage label. - A translation MCP reads a file → asks the LLM to localize it → writes the output. -Hermes handles the inference request with the active provider and meters the tokens against the current session. Enable sampling for a server: +Hermes handles the inference request with the active provider and meters the tokens against the current session. Do not add a YAML `allow_sampling:` block; current MCP setup is CLI-managed. If a server needs sampling or any other privileged behavior, review it first, then use the CLI-managed settings: -```yaml -mcp_servers: - scraper: - command: node - args: ["./scraper-mcp.js"] - allow_sampling: true # Off by default - sampling_model: gpt-5-mini # Optional: pin a cheaper model for sampling +```bash +hermes mcp add scraper --command node --args ./scraper-mcp.js +hermes mcp configure scraper +hermes mcp test scraper ``` -**Security note:** Sampling means an MCP server can burn your tokens. Only enable it for servers you trust. See [Part 19](./part19-security-playbook.md#layer-5-mcp-and-plugin-trust). +**Security note:** Sampling means an MCP server can burn your tokens. Only keep sampling-capable servers registered when you trust their code and have verified the CLI-managed restrictions. See [Part 19](./part19-security-playbook.md#layer-5-mcp-and-plugin-trust). --- ## Observing MCP Traffic ```bash -/mcp list # Show registered servers + tool counts -/mcp reload # Reload servers without restarting Hermes -/mcp disable github # Temporarily unregister -/mcp enable github # Bring it back +hermes mcp list # Show registered servers and tool counts +hermes mcp test github # Start the server and verify discovery +hermes mcp configure github # Change auth/env/tool settings ``` The [Web Dashboard](./part12-web-dashboard.md) has an **MCP Servers** tab that shows connection status, tool list, recent invocations, and error logs for each server. This is the fastest way to debug a misbehaving MCP. @@ -226,6 +214,7 @@ MCP adds a process (or a network hop) per tool. For things that live inside Herm - **Skills** — if the workflow is deterministic, a [skill](./part5-creating-skills.md) is cheaper to maintain. Use MCP when you want: + - A tool that already has a community-maintained server (GitHub, Slack, Postgres, etc.) - A tool you'd want to share with other agents (Claude Code, Cursor, Copilot) - A tool that needs its own runtime (Node/Go/Rust) you'd rather not embed into Hermes @@ -234,13 +223,13 @@ Use MCP when you want: ## Troubleshooting -| Symptom | Likely cause | Fix | -|---------|--------------|-----| -| `MCP server 'github' failed to start` | `npx` not on PATH in the gateway's environment | Use an absolute path in `command:` or set `PATH` in `env:` | -| Server shows connected but 0 tools | Permissions — server's env vars are missing its auth token | Check `env:` entries and that referenced `${VARS}` exist in `.env` | -| Tools show up in CLI but not Telegram | Gateway process has its own env — restart it after config change | `hermes gateway restart` | -| Constant reconnects on HTTP server | SSE timeout behind a reverse proxy | Set `proxy_read_timeout 300s` in nginx/Caddy | -| `sampling not permitted` in server logs | `allow_sampling: false` (default) | Set `allow_sampling: true` in the server's block | +| Symptom | Likely cause | Fix | +| --------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `MCP server 'github' failed to start` | `npx` not on PATH in the Hermes environment | Run `hermes mcp configure github` and set an absolute command or PATH/env override | +| Server shows connected but 0 tools | Permissions or auth values are missing | Run `hermes mcp test NAME`, then `hermes mcp configure NAME` to review auth/env | +| Tools show up in CLI but not Telegram | Gateway process has its own env — restart it after config change | `hermes gateway restart` | +| Constant reconnects on HTTP server | SSE timeout behind a reverse proxy | Set `proxy_read_timeout 300s` in nginx/Caddy | +| `sampling not permitted` in server logs | Server is requesting sampling without an approved CLI-managed setting | Review the server, then use `hermes mcp configure NAME` or unregister it | --- diff --git a/part19-security-playbook.md b/part19-security-playbook.md index 7c80d08..981fe77 100644 --- a/part19-security-playbook.md +++ b/part19-security-playbook.md @@ -1,6 +1,6 @@ # Part 19: Security Playbook — Locking Down an Agent That Reads Untrusted Text -*April 15, 2026 published [Comment and Control](https://oddguan.com/blog/comment-and-control-prompt-injection-credential-theft-claude-code-gemini-cli-github-copilot/) — cross-vendor prompt injection that steals GitHub Actions secrets from Claude Code, Gemini CLI, and Copilot Agent via PR titles. Your Hermes bot reads messages from Telegram, Discord, email, webhooks, and SMS — every one of them an injection vector. This part is the defensive posture that stops your agent from becoming someone else's command-and-control channel.* +_April 15, 2026 published [Comment and Control](https://oddguan.com/blog/comment-and-control-prompt-injection-credential-theft-claude-code-gemini-cli-github-copilot/) — cross-vendor prompt injection that steals GitHub Actions secrets from Claude Code, Gemini CLI, and Copilot Agent via PR titles. Your Hermes bot reads messages from Telegram, Discord, email, webhooks, and SMS — every one of them an injection vector. This part is the defensive posture that stops your agent from becoming someone else's command-and-control channel._ > **Schema note (2026-05-31):** Earlier revisions of this part documented a `security:` config block with `provenance`, `approval.require_approval` regex, `secrets.scope`, and `network.egress_allowlist` keys. **None of those exist in Hermes Agent.** This rev is rewritten against the real schema — top-level [`approvals:`](https://hermes-agent.nousresearch.com/docs/user-guide/security), a native dangerous-command detector, `command_allowlist:`, `.env` user allowlists, and OS-level isolation. See the [official Security guide](https://hermes-agent.nousresearch.com/docs/user-guide/security) and [SECURITY.md trust model](https://github.com/NousResearch/hermes-agent/blob/main/SECURITY.md). @@ -10,19 +10,19 @@ Hermes is uniquely exposed because it takes input from **many** surfaces and has **many** capabilities: -| Surface | Attacker controls | Risk | -|---------|-------------------|------| -| Telegram DM | Message body, filename, image caption | Injection → tool calls | -| Discord channel | Embed text, webhook payloads, usernames | Injection → tool calls | -| Email inbox | Headers, body, attachment filenames | Multi-stage (HTML + links) | -| SMS / Twilio | Message body + webhook payloads | Injection → tool calls | -| GitHub MCP | PR titles, issue bodies, comments | Comment-and-Control pattern | -| Web-scraped content | Page HTML the agent reads | "Read then act" injections | -| Voice transcript | STT transcription | "Say the magic phrase" attacks | -| MCP/plugin package | Tool schema, stdout, hook behavior | Supply-chain prompt injection / token burn | -| Dashboard plugin | Browser UI + backend endpoints | Local secret/config exposure | +| Surface | Attacker controls | Risk | +| ------------------- | --------------------------------------- | ------------------------------------------ | +| Telegram DM | Message body, filename, image caption | Injection → tool calls | +| Discord channel | Embed text, webhook payloads, usernames | Injection → tool calls | +| Email inbox | Headers, body, attachment filenames | Multi-stage (HTML + links) | +| SMS / Twilio | Message body + webhook payloads | Injection → tool calls | +| GitHub MCP | PR titles, issue bodies, comments | Comment-and-Control pattern | +| Web-scraped content | Page HTML the agent reads | "Read then act" injections | +| Voice transcript | STT transcription | "Say the magic phrase" attacks | +| MCP/plugin package | Tool schema, stdout, hook behavior | Supply-chain prompt injection / token burn | +| Dashboard plugin | Browser UI + backend endpoints | Local secret/config exposure | -The goal isn't to eliminate these channels — Hermes is *for* reading them. The goal is to make sure untrusted text can't cross a trust boundary into secrets, writes, or shell. +The goal isn't to eliminate these channels — Hermes is _for_ reading them. The goal is to make sure untrusted text can't cross a trust boundary into secrets, writes, or shell. --- @@ -36,8 +36,8 @@ Every in-process control below (approval prompts, secret redaction, skill scanni Hermes supports two OS-level isolation postures — choose deliberately: -- **Terminal-backend isolation.** A non-default `terminal.backend` (Docker, Singularity, Modal, Daytona, SSH) runs LLM-emitted shell *and* file-tool operations inside a container/remote host. Confines anything the agent does *through the shell*. Does **not** confine the agent's own Python process (code-execution tool, MCP subprocesses, plugins, hooks, skills). -- **Whole-process wrapping.** Runs the entire agent process tree in a sandbox so *every* path — shell, code-exec, MCP, file tools, plugins, hooks — is subject to one filesystem/network/process policy. Hermes supports this via its own Docker/Compose setup, or via [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) for declarative filesystem + **L7 network egress** + syscall + inference-routing policy. +- **Terminal-backend isolation.** A non-default `terminal.backend` (Docker, Singularity, Modal, Daytona, SSH) runs LLM-emitted shell _and_ file-tool operations inside a container/remote host. Confines anything the agent does _through the shell_. Does **not** confine the agent's own Python process (code-execution tool, MCP subprocesses, plugins, hooks, skills). +- **Whole-process wrapping.** Runs the entire agent process tree in a sandbox so _every_ path — shell, code-exec, MCP, file tools, plugins, hooks — is subject to one filesystem/network/process policy. Hermes supports this via its own Docker/Compose setup, or via [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) for declarative filesystem + **L7 network egress** + syscall + inference-routing policy. If your agent ingests content from surfaces you don't control (the open web, inbound email, multi-user channels, untrusted MCP servers), **whole-process wrapping is the supported posture.** Running the default local backend against untrusted input is operating outside Hermes' supported security model. The layers below harden a real deployment; they are not a substitute for the boundary. @@ -45,7 +45,7 @@ If your agent ingests content from surfaces you don't control (the open web, inb ## Layer 1: User Authorization — Who Can Talk to the Agent -The first gate is *who is even allowed to reach the agent*. On every messaging gateway, Hermes is **default-deny**: if no allowlist is configured and `GATEWAY_ALLOW_ALL_USERS` is unset, all users are rejected. +The first gate is _who is even allowed to reach the agent_. On every messaging gateway, Hermes is **default-deny**: if no allowlist is configured and `GATEWAY_ALLOW_ALL_USERS` is unset, all users are rejected. Set per-platform allowlists in `~/.hermes/.env` (comma-separated IDs): @@ -70,9 +70,9 @@ Per-platform hardening worth setting: ```yaml # ~/.hermes/config.yaml discord: - require_mention: true # Bot only responds when @mentioned in channels (default) - free_response_channels: "" # Channel IDs exempt from the mention requirement -group_sessions_per_user: true # Each group participant gets an isolated session + require_mention: true # Bot only responds when @mentioned in channels (default) + free_response_channels: "" # Channel IDs exempt from the mention requirement +group_sessions_per_user: true # Each group participant gets an isolated session ``` --- @@ -86,20 +86,20 @@ Configure the policy with the top-level `approvals:` block: ```yaml # ~/.hermes/config.yaml approvals: - mode: manual # manual | smart | off - timeout: 60 # seconds to wait before fail-closed deny - cron_mode: deny # deny | approve — behavior when a cron job hits a dangerous command - mcp_reload_confirm: true # /reload-mcp confirms before invalidating the MCP tool cache + mode: manual # manual | smart | off + timeout: 60 # seconds to wait before fail-closed deny + cron_mode: deny # deny | approve — behavior when a cron job hits a dangerous command + mcp_reload_confirm: true # /reload-mcp confirms before invalidating the MCP tool cache destructive_slash_confirm: true # /clear, /new, /reset, /undo confirm before discarding state ``` -| Mode | Behavior | -|------|----------| -| **manual** (default) | Always prompt on a dangerous command | -| **smart** | An auxiliary LLM assesses risk first — auto-approves clearly low-risk matches, auto-denies clearly dangerous ones, escalates the uncertain middle to a manual prompt | -| **off** | Skip all approval prompts (equivalent to `--yolo`) | +| Mode | Behavior | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **manual** (default) | Always prompt on a dangerous command | +| **smart** | An auxiliary LLM assesses risk first — auto-approves clearly low-risk matches, auto-denies clearly dangerous ones, escalates the uncertain middle to a manual prompt | +| **off** | Skip all approval prompts (equivalent to `--yolo`) | -When a prompt fires in the CLI you get four choices — **once / session / always / deny** (deny is the default if you time out). On messaging platforms the prompt is delivered as a message (inline buttons on Telegram/Discord/Slack); reply *yes/approve* or *no/deny*. +When a prompt fires in the CLI you get four choices — **once / session / always / deny** (deny is the default if you time out). On messaging platforms the prompt is delivered as a message (inline buttons on Telegram/Discord/Slack); reply _yes/approve_ or _no/deny_. ### `command_allowlist` — the "always approve" list @@ -107,11 +107,11 @@ Choosing **always** writes a human-readable pattern description to the top-level ```yaml command_allowlist: - - recursive delete # matches the "rm -r" detector category + - recursive delete # matches the "rm -r" detector category - shell command via -c/-lc flag ``` -Entries are description strings that match the detector's pattern categories — they are **not** raw regex. Be conservative: allowing `recursive delete` means *every* `rm -r`, including paths you didn't intend, runs without a prompt. Edit `~/.hermes/config.yaml` (or `hermes config edit`) to remove entries. +Entries are description strings that match the detector's pattern categories — they are **not** raw regex. Be conservative: allowing `recursive delete` means _every_ `rm -r`, including paths you didn't intend, runs without a prompt. Edit `~/.hermes/config.yaml` (or `hermes config edit`) to remove entries. ### YOLO mode — what it does and doesn't bypass @@ -127,11 +127,11 @@ A small set of catastrophic, irreversible commands is refused **regardless** of - `dd if=/dev/zero of=/dev/sd*` - Piping untrusted URLs to `sh` at the rootfs top level -The blocklist trips *before* the approval layer sees the command. It's the seatbelt, not the whole car. +The blocklist trips _before_ the approval layer sees the command. It's the seatbelt, not the whole car. ### Two caveats that matter -- **Container backends skip approval entirely.** When `terminal.backend` is `docker`, `singularity`, `modal`, or `daytona`, dangerous-command checks are bypassed because the container *is* the boundary (Layer "OS isolation" above). That's the intended trade — unrestricted execution inside a disposable box. +- **Container backends skip approval entirely.** When `terminal.backend` is `docker`, `singularity`, `modal`, or `daytona`, dangerous-command checks are bypassed because the container _is_ the boundary (Layer "OS isolation" above). That's the intended trade — unrestricted execution inside a disposable box. - **Approvals route to the channel the message came from.** There is no separate "approval channel" config. The defense against "trick the bot into approving itself" is your **allowlist** (Layer 1): keep the public-facing bot's allowlist tight, and drive privileged actions from a separate, owner-only bot or DM that untrusted users can't reach. ### Optional: tirith pre-exec scanning @@ -143,7 +143,7 @@ security: tirith_enabled: true tirith_path: tirith tirith_timeout: 5 - tirith_fail_open: true # allow commands if tirith is unavailable + tirith_fail_open: true # allow commands if tirith is unavailable ``` A tirith `warn` is folded into the same approval prompt; a tirith `block` rejects the command outright. @@ -152,7 +152,7 @@ A tirith `warn` is folded into the same approval prompt; a tirith `block` reject ## Layer 3: Secrets and Credential Scoping -The Comment-and-Control attack class succeeds by exfiltrating credentials. Hermes' defenses here reduce *casual* leakage — pair them with isolation for real containment. +The Comment-and-Control attack class succeeds by exfiltrating credentials. Hermes' defenses here reduce _casual_ leakage — pair them with isolation for real containment. **On-disk hygiene (automatic):** @@ -164,14 +164,14 @@ The Comment-and-Control attack class succeeds by exfiltrating credentials. Herme ```yaml # ~/.hermes/config.yaml security: - redact_secrets: true # default on — redacts secret-like patterns from tool output and logs + redact_secrets: true # default on — redacts secret-like patterns from tool output and logs ``` Leave it on. `~/.hermes/logs/` is redacted by default; only set `redact_secrets: false` to debug an auth issue, and treat the resulting logs as secret-bearing. **Credential scoping (automatic):** Hermes filters the environment it hands to its lower-trust in-process children — shell subprocesses, MCP subprocesses, and the code-execution child. Provider API keys and gateway tokens are **stripped by default**; only variables an operator or a loaded skill explicitly declares are passed through. You don't configure per-tool secret scoping — it's the default behavior. -> **The honest caveat (from SECURITY.md §2.3):** this reduces casual exfiltration; it is *not* containment. Anything running *inside* the agent process — skills, plugins, hook handlers — can read whatever the agent can, including in-memory credentials. The mitigation for a hostile in-process component is operator review before install (Layer 5), not env scrubbing. +> **The honest caveat (from SECURITY.md §2.3):** this reduces casual exfiltration; it is _not_ containment. Anything running _inside_ the agent process — skills, plugins, hook handlers — can read whatever the agent can, including in-memory credentials. The mitigation for a hostile in-process component is operator review before install (Layer 5), not env scrubbing. --- @@ -184,11 +184,11 @@ Pick a non-default terminal backend so LLM-emitted shell and file-tool operation ```yaml # ~/.hermes/config.yaml terminal: - backend: docker # local | docker | singularity | modal | daytona | ssh + backend: docker # local | docker | singularity | modal | daytona | ssh docker_image: nikolaik/python-nodejs:python3.11-nodejs20 cwd: /workspace - docker_mount_cwd_to_workspace: false # off by default — opt in to mount host cwd - container_persistent: true # persist FS across sessions; false = reset each session + docker_mount_cwd_to_workspace: false # off by default — opt in to mount host cwd + container_persistent: true # persist FS across sessions; false = reset each session ``` What a container backend buys you: @@ -208,7 +208,7 @@ terminal: ssh_key: ~/.ssh/id_rsa ``` -For **true egress allowlisting** (block private ranges, block the metadata IP `169.254.169.254`, restrict outbound domains), wrap the *whole process* with [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell), which enforces hot-reloadable L7 network policy across every code path — including MCP subprocesses and the code-execution child that a terminal-only backend leaves exposed. For a home-lab / [Home Assistant](./part15-new-platforms.md#home-assistant) setup, an explicit OpenShell egress allowlist beats hoping a config key blocks SSRF (it doesn't exist). +For **true egress allowlisting** (block private ranges, block the metadata IP `169.254.169.254`, restrict outbound domains), wrap the _whole process_ with [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell), which enforces hot-reloadable L7 network policy across every code path — including MCP subprocesses and the code-execution child that a terminal-only backend leaves exposed. For a home-lab / [Home Assistant](./part15-new-platforms.md#home-assistant) setup, an explicit OpenShell egress allowlist beats hoping a config key blocks SSRF (it doesn't exist). --- @@ -216,34 +216,26 @@ For **true egress allowlisting** (block private ranges, block the metadata IP `1 MCP servers and plugins are third-party code you give tool access to. Hermes does **not** have per-server `trust:` levels, `allow_sampling`, or `max_concurrent_calls` config knobs. The real controls are credential filtering, tool filtering, and **operator review before install**. -Configure servers with the documented [MCP schema](https://hermes-agent.nousresearch.com/docs/reference/mcp-config-reference) and use `tools.include` / `tools.exclude` to expose only the tools you audited: +Register servers with the Hermes MCP CLI and verify each one before use: -```yaml -# ~/.hermes/config.yaml -mcp_servers: - github: - command: npx - args: ["-y", "@modelcontextprotocol/server-github"] - env: - GITHUB_PERSONAL_ACCESS_TOKEN: ${GITHUB_RO_TOKEN} # read-only, scoped PAT - enabled: true - timeout: 120 - tools: - include: [] # empty = all; or list exactly the tools you trust - exclude: [] - - scraper-mcp: - command: npx - args: ["-y", "some-web-scraper-mcp"] - enabled: true - tools: - include: [read_docs] # lock an untrusted-content server to read-only tools you audited +```bash +hermes mcp add github --command npx --env GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_RO_TOKEN} --args -y @modelcontextprotocol/server-github +hermes mcp configure github +hermes mcp test github + +hermes mcp add scraper-mcp --command npx --args -y some-web-scraper-mcp +hermes mcp configure scraper-mcp +hermes mcp test scraper-mcp + +hermes mcp list ``` +Use `hermes mcp configure NAME` to set auth, environment values, roots, or tool filters when the CLI exposes those controls. If a server ingests untrusted content and you cannot restrict its tool surface, do not register it in the production Hermes home. + Trust is enforced by review and isolation, not a config flag: -- **Credential filtering** strips provider keys/gateway tokens from MCP subprocess environments by default (Layer 3). Pass only what a server genuinely needs, via its `env:`. -- **Skills run arbitrary Python at import time; plugins run with full agent privileges.** "Reviewing" a skill or plugin means reading its Python and scripts, not just its `SKILL.md`. **Skills Guard** scans installable skill content for injection patterns — treat it as a review *aid*, not a boundary. +- **Credential filtering** strips provider keys/gateway tokens from MCP subprocess environments by default (Layer 3). Pass only what a server genuinely needs, via `hermes mcp add --env KEY=VALUE` or `hermes mcp configure NAME`. +- **Skills run arbitrary Python at import time; plugins run with full agent privileges.** "Reviewing" a skill or plugin means reading its Python and scripts, not just its `SKILL.md`. **Skills Guard** scans installable skill content for injection patterns — treat it as a review _aid_, not a boundary. - Never give a server that ingests untrusted content (web scrapers, email parsers) a broad tool surface or sensitive env. Run those flows under whole-process isolation. See [Part 17](./part17-mcp-servers.md) for install patterns. @@ -274,7 +266,7 @@ Aonan Guan's writeup has the exploit chain in full. Patch, don't just read. ## Diagnostic Bundle Safety -Logs under `~/.hermes/logs/` pass through the secret redactor when `security.redact_secrets` is on (the default). Before sharing *any* debug output or log bundle with someone else: +Logs under `~/.hermes/logs/` pass through the secret redactor when `security.redact_secrets` is on (the default). Before sharing _any_ debug output or log bundle with someone else: 1. Review it first — redaction is pattern-based and not exhaustive. 2. Never share output from a session that touched production secrets over a public link. @@ -291,7 +283,7 @@ Cron the audits (these skills ship in this guide's `skills/security/` hub). Reme ```yaml # ~/.hermes/cron.yaml - name: weekly-mcp-audit - schedule: "0 9 * * 1" # Weekly Monday + schedule: "0 9 * * 1" # Weekly Monday task: | /audit-mcp List every MCP, its env, its tools include/exclude, and last update from npm/github. @@ -303,7 +295,7 @@ Cron the audits (these skills ship in this guide's `skills/security/` hub). Reme - name: weekly-approval-bypass-review schedule: "0 10 * * 1" - task: /audit-approval-bypass # flags YOLO/off/cron-approve and container-bypass surfaces + task: /audit-approval-bypass # flags YOLO/off/cron-approve and container-bypass surfaces ``` Install with `hermes skills install security/audit-mcp` and `security/audit-approval-bypass`. diff --git a/part21-remote-sandboxes.md b/part21-remote-sandboxes.md index 802b2e0..8cb3104 100644 --- a/part21-remote-sandboxes.md +++ b/part21-remote-sandboxes.md @@ -1,307 +1,240 @@ -# Part 21: Remote Sandboxes & Bulk File Sync — SSH, Modal, Daytona, Vercel +# Part 21: Remote Execution & Workspace Isolation -*Running Hermes on a $5 VPS is great for chat. Running heavy coding work there is not. This part sets up the "phone drives, beefy remote does the work" pattern: Hermes lives on your small VPS, delegates execution to a disposable sandbox on SSH/Modal/Daytona/Vercel, syncs files both ways, and tears it down when idle.* +_Hermes can still do the "phone drives, beefy machine does the work" pattern, but the current runtime does not expose a native sandbox subsystem. There is no sandbox-profile config block, no sandbox CLI subcommand, and no sandbox slash command. Use terminal backends, git worktrees, Kanban worker lanes, MCP servers, and skills instead._ --- -## The Pattern - -``` -Your phone (Telegram) - │ - ▼ -Hermes on $5 VPS ─────────────► Remote sandbox ($0 when idle) -- Memory - Whole workspace in /home/runner/ -- Skills - Coding agents (Claude/Codex/etc) -- Conversation state - Build tools, Docker, GPU - ▲ │ - │ │ - └─── bulk file sync on teardown ─┘ -``` - -Hermes uploads your workspace on task start, delegates work, then downloads only the diff back on teardown. The sandbox dies, Hermes keeps the state — and your $5 VPS never needed the 32GB of RAM the sandbox ran in. +## The Current Pattern + +```text +Your phone / chat client + | + v +Hermes driver on a small VPS +- gateways and approvals +- conversation state +- memory, skills, MCP config +- Kanban board + | + v +Execution target +- local shell +- Docker or Singularity container +- SSH host +- Modal or Daytona terminal backend, when supported by your installed build +- external runtime reached through a CLI, MCP server, or custom skill +``` + +Hermes stays the coordinator. The execution target is where LLM-emitted shell and file-tool operations run. For coding work, use git branches or worktrees as the state boundary, then push a reviewable diff. Do not rely on a Hermes-managed remote sandbox lifecycle or automatic diff-back sync; those are not current CLI/config surfaces. --- -## Pick Your Backend +## Pick The Right Execution Boundary -| Backend | Billing | Idle cost | Best for | -|---------|---------|-----------|----------| -| **SSH** | Your infra | Whatever your host costs | Homelab / always-on dev box | -| **Modal** | Per-second compute | $0 (hibernate) | Bursty coding tasks, GPU work | -| **Daytona** | Per-second workspace | $0 (hibernate) | Long-lived dev workspaces | -| **Vercel Sandbox** | Per-run / platform billing | $0 when unused | Webapp builds and isolated `execute_code` tasks | -| **Fly Machines** | Per-second | $0 (stop) | Regional sandboxes near your users | -| **E2B** | Per-second | $0 | Quick throwaway Python sandboxes | -| **Local Docker** | Your hardware | N/A | Testing / development | +| Pattern | Hermes surface | Best for | Caveat | +| ----------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| Local shell | `terminal.backend: local` or default config | Trusted personal projects on the driver host | No OS isolation from the host | +| Local container | `terminal.backend: docker` or `singularity` | Risky shell commands, dependency-heavy builds, repeatable toolchains | Only shell/file-tool paths are isolated; in-process plugins/MCP still run with the agent | +| Remote host | `terminal.backend: ssh` | Homelab boxes, beefy workstations, GPU hosts, existing dev servers | You own lifecycle, updates, and source checkout | +| Modal / Daytona | `terminal.backend: modal` or `daytona`, where your Hermes build supports it | Bursty remote compute or persistent cloud workspaces | Configure only keys your installed build exposes; do not create a legacy sandbox-profile block | +| Git worktree | `hermes --worktree` or Kanban `--workspace worktree` | Parallel local coding sessions and worker lanes | Isolation is source-tree isolation, not a remote runtime | +| External runtimes | Vendor CLI/API through MCP or a skill | Vercel builds, Fly Machines, E2B notebooks, CI runners | Hermes orchestrates them; they are not native Hermes-managed execution backends | -Hermes ships native support for SSH, Modal, Daytona, and Vercel Sandbox. Fly Machines and E2B work via thin plugins. +For untrusted input, remember the security model from [Part 19](./part19-security-playbook.md): a terminal backend confines shell/file-tool activity, not every in-process code path. Use whole-process wrapping when the whole agent must be isolated. --- -## SSH Backend (Homelab / Always-On Dev Box) +## Configure A Terminal Backend -### Prereqs +Hermes terminal backends live under the top-level `terminal:` config key. -- SSH access to the remote host with key auth (no password prompts) -- Remote has `python3`, `rsync`, `tar`, `git` -- Your SSH config uses `ControlMaster` + `ControlPath` for connection reuse (shown below) - -### Config +### Local ```yaml # ~/.hermes/config.yaml -sandboxes: - dev-box: - backend: ssh - host: dev.local - user: hermes - identity_file: ~/.ssh/hermes_ed25519 - workdir: /home/hermes/sandboxes - control_master: auto # Reuses connection for bulk sync - control_persist: 600 - sync: - push: ~/.hermes # Uploaded at sandbox create - pull_on_teardown: true - pull_paths: - - .hermes - - projects # Grabs any code changes made in-sandbox - ignore: - - .git - - node_modules - - __pycache__ - - "*.log" +terminal: + backend: local + cwd: /home/hermes/projects/myapp ``` -### Use It +Use this for the simplest driver-box workflow. Pair it with `hermes --worktree` for isolated coding sessions. -``` -/sandbox start dev-box -/claude-code refactor src/auth/ to use JWT rotation -/sandbox stop dev-box # Syncs changes back, then stops -``` +### Docker Or Singularity -Under the hood on teardown: +```yaml +# ~/.hermes/config.yaml +terminal: + backend: docker # local | docker | singularity | modal | daytona | ssh + docker_image: nikolaik/python-nodejs:python3.11-nodejs20 + cwd: /workspace + docker_mount_cwd_to_workspace: false # opt in only when you want the host cwd mounted + container_persistent: false # false resets the container filesystem per session +``` -1. Hermes runs `tar cf - -C ~/.hermes .` on the remote -2. Pipes it over the SSH ControlMaster to the local box -3. Unpacks into a staging dir -4. Diffs against SHA-256 hashes of what was originally pushed -5. Applies only changed files back to `~/.hermes`, with `fcntl.flock` serialization if another sandbox runs concurrently -6. SIGINT-safe — pressing Ctrl-C during sync rolls back cleanly +Switch `backend` to `singularity` in environments where Singularity is the supported container runtime. Keep host secrets in `~/.hermes/.env`; do not bake them into the image. -This is the hardening that made remote sandboxes safe enough for real coding work. Before diff-based sync-back, you either rsynced everything every time (slow) or lost remote-made edits on teardown. +### SSH Host ---- +```yaml +# ~/.hermes/config.yaml +terminal: + backend: ssh + ssh_host: devbox.example.com + ssh_user: hermes + ssh_port: 22 + ssh_key: ~/.ssh/id_ed25519 +``` -## Modal Backend (Bursty / Serverless) +This is the most practical "small VPS drives a bigger machine" setup. Put the repo on the SSH host, run the worker there, and use git to move reviewed changes back through branches and PRs. -Modal hibernates sandboxes to zero between runs and spins up in ~2 seconds. Ideal for bursty coding-agent use. +### Modal Or Daytona -```bash -pip install modal -modal token new -``` +Current docs list Modal and Daytona as terminal backend choices, but provider-specific fields can vary by Hermes build. Treat them as terminal backends, not named sandbox profiles: ```yaml -sandboxes: - modal-big: - backend: modal - image: - from: python:3.12 - apt_install: [git, ripgrep, build-essential] - pip_install: [claude-code-cli, aider-chat] - cpu: 4 - memory: 16384 - gpu: null # Set to "T4" / "A10G" / "H100" if you need one - timeout: 3600 - sync: - push: ~/.hermes - pull_on_teardown: true - pull_paths: [.hermes, projects] +# ~/.hermes/config.yaml +terminal: + backend: modal # or daytona, when supported by your installed build ``` -Sync uses Modal's `exec tar cf -` → `proc.stdout.read()` → local file pattern — same diff/apply logic as SSH. - -Cost tip: set `timeout: 300` and a short `idle_shutdown:` for chat-driven sandboxes; Modal bills per second of actual runtime. - -### GPU Sandboxes for Voice / Image Tasks +After changing backend config, run: -If you've disabled the [Tool Gateway](./part13-tool-gateway.md) and run your own image-gen or voice pipeline, a GPU sandbox is cheaper than keeping a GPU VPS live: - -```yaml -sandboxes: - gpu-a10g: - backend: modal - image: - from: nvcr.io/nvidia/pytorch:24.10-py3 - pip_install: [diffusers, transformers] - gpu: "A10G" - timeout: 600 - commands: - - /generate_image # Route image gen to this sandbox - - /speech_synth +```bash +hermes config check +hermes doctor ``` -Hermes routes the tool calls transparently — the user has no idea the sandbox span is happening. +If your installed Hermes build does not expose that backend, fall back to Docker or SSH, or wrap the vendor API with a skill/MCP integration. --- -## Daytona Backend (Long-Lived Workspaces) +## Isolated Local Sessions With `--worktree` -Daytona is the "it's like GitHub Codespaces for your own code" option. Pair with Hermes when you want the workspace to persist across sessions: +For a one-off coding pass from a git repo: -```yaml -sandboxes: - workspace: - backend: daytona - workspace_id: hermes-dev - auto_create: true # Create if it doesn't exist - image: daytonaio/workspace-project:latest - hibernate_after: 900 - sync: - push: ~/.hermes - pull_on_teardown: false # Work persists, no need to sync every time - pull_on_command: "/sync-home" # Manual sync when you want it +```bash +cd ~/projects/myapp +hermes --worktree --tui ``` -Pair with the [Gemini OAuth provider](./part9-custom-models.md#gemini-oauth--free-tier-friendly) for free-tier-friendly long-context reads inside the sandbox. +For a scripted prompt: ---- +```bash +cd ~/projects/myapp +hermes --worktree -z "Run the test suite, fix the auth null-check failure, and summarize the diff." +``` -## Vercel Sandbox (Web Builds / Isolated Code Execution) +`--worktree` gives the session a separate git worktree so parallel agents do not edit the same checkout. It is ideal when the driver host has enough CPU/RAM and you mainly need source isolation. -Vercel Sandbox is now a native backend for `execute_code` and terminal-style runs. Use it when the task is webapp-shaped: install dependencies, run a build, inspect generated output, and throw the environment away. +--- -```yaml -sandboxes: - vercel-web: - backend: vercel - project: my-webapp - timeout: 1800 - sync: - push: ~/projects/my-webapp - pull_on_teardown: true - pull_paths: - - . - ignore: - - node_modules - - .next - - dist -``` +## Durable Worker Lanes With Kanban -It is not a replacement for Daytona if you want a persistent dev workspace. Treat it as a clean execution target for builds, tests, and short isolated scripts. +For work that should survive restarts, handoffs, review, or retries, put it on the Kanban board and assign a worker profile: ---- +```bash +hermes kanban create "Fix the auth null-check and open a PR" \ + --assignee codex-worker \ + --workspace worktree \ + --branch wt/auth-null-check -## Fly Machines (Regional / Low-Latency) +hermes kanban dispatch --max 1 +``` -For users in specific regions, Fly Machines deliver sub-100ms latency from a nearby PoP: +Useful commands while it runs: -```yaml -sandboxes: - fly-sin: - backend: fly_machines # Plugin, not core - app: hermes-sandbox - region: sin # Singapore - size: performance-2x - auto_stop: true - stopped_shutdown_at: 120 +```bash +hermes kanban list +hermes kanban show +hermes kanban runs +hermes kanban log ``` -Useful when you want the sandbox physically near your iOS / Telegram users for lower round-trip. +The worker uses the configured terminal backend for shell/file operations and the Kanban workspace setting for source isolation. Keep "worker exited" and "work is done" separate: require tests, review, and a clean git diff before closing the card. --- -## E2B (Disposable Python Sandboxes) +## Coding Agents On Remote Targets -E2B gives you a clean Linux sandbox in ~500ms. Best for data analysis / running unknown code: +The coding-agent layer from [Part 18](./part18-coding-agents.md) still applies: Claude Code, Codex, Gemini CLI, OpenCode, and ACP-compatible runtimes can sit behind Hermes as worker lanes or interactive sessions. For this chapter, the important rule is that shell/file operations land wherever `terminal.backend` points. -```yaml -sandboxes: - e2b-scratch: - backend: e2b - template: python # E2B template - metadata: - purpose: data-analysis - timeout: 300 -``` +A common road-warrior setup is: -Hermes routes any tool call marked `/sandbox e2b` into this template. Teardown is automatic. +1. Hermes gateway and memory run on the small VPS. +2. `terminal.backend: ssh` points shell/file work at a stronger dev box. +3. Coding-agent work is assigned through Kanban with `--workspace worktree`. +4. The worker pushes a branch or opens a PR instead of syncing opaque files back to the driver. --- -## Cross-Sandbox Patterns +## External Runtimes: Vercel, Fly, E2B, CI -### Pattern A: Primary-Replica Dev Box + Ephemeral Sandboxes +Vercel, Fly Machines, E2B, and CI runners can be very useful execution targets, but in the current Hermes surface they should be modeled as external integrations. -- **Primary:** SSH dev box with your long-lived workspace -- **Replica:** Modal sandbox spun up per delegation +| Runtime | Recommended integration shape | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Vercel builds | Use the Vercel CLI/API from a Docker or SSH terminal backend, or expose a Vercel MCP/tool skill that runs build/deploy commands and returns logs/artifacts | +| Fly Machines | Use `flyctl` from a skill or MCP server to start/stop a machine; if you want Hermes shell execution there, expose SSH and configure `terminal.backend: ssh` | +| E2B | Use an E2B MCP server or custom tool for notebook-style execution; pass source through git/artifacts, not a native Hermes execution profile | +| CI runners | Have Hermes open a PR, trigger CI, then read status/logs through GitHub/GitLab MCP or CLI tools | -``` -/sandbox start dev-box -/delegate (runs in modal-big, reads from dev-box via git) -/sandbox stop dev-box -``` - -Works great when each coding-agent delegation runs a git-backed feature branch. Sandboxes are stateless; dev-box is the source of truth. - -### Pattern B: Per-Project Daytona Workspaces +A good custom skill for an external runtime should: -``` -/project open myapp → daytona workspace "myapp" -/project open sideproject → daytona workspace "sideproject" -``` +1. State the target runtime and repo/branch it will use. +2. Create or reuse an isolated checkout. +3. Run the vendor CLI/API with explicit timeouts. +4. Return logs, artifact URLs, and exit status. +5. Avoid writing provider secrets to `config.yaml`, logs, or the repo. -Each project has its own workspace with its own deps, env, and git state. Hermes remembers which is active per Telegram topic. +--- -### Pattern C: Sandboxed MCP Servers +## File Movement And Source Of Truth -Route untrusted MCP servers (see [Part 19](./part19-security-playbook.md#layer-5-mcp-and-plugin-trust)) into a sandbox: +Use git as the primary sync mechanism: -```yaml -mcp_servers: - random-scraper: - trust: untrusted - run_in_sandbox: e2b-scratch # Isolate execution -``` +- Work in a branch or worktree. +- Push changes to the remote repository. +- Review and merge through the normal PR path. +- Pull artifacts explicitly from the external runtime when needed. -Sandbox catches any malicious behavior — even if the scraper is compromised, it can't touch your host. +If you need raw file copy for an SSH host, use explicit `rsync`, `scp`, or a skill that names source, destination, ignore patterns, and conflict behavior. Do not describe it as Hermes automatic sandbox teardown sync. --- -## Observability: `hermes sandbox status` +## Observability -``` -$ hermes sandbox status -NAME BACKEND STATE AGE CPU MEM COST -dev-box ssh connected 3h 12m 0.4 2.1 GB $0 (your infra) -modal-big modal running 0m 42s 3.8 14.2 GB $0.09 -workspace daytona hibernated 0m 0s - - $0 +There is no sandbox-status subcommand. Use the surfaces that actually exist: + +```bash +hermes status +hermes doctor +hermes config check +hermes logs -f +hermes kanban list +hermes kanban show +hermes kanban runs ``` -The [Web Dashboard](./part12-web-dashboard.md) has a Sandboxes panel with the same info plus: streaming logs, per-sandbox cost totals for the month, sync history, and a one-click "sync back and stop" button. +The [Web Dashboard](./part12-web-dashboard.md) is useful for config editing, chat/TUI access, sessions, and Kanban state. Vendor runtimes keep their own logs and billing dashboards; surface those back through MCP/skills when you need them in Hermes. --- ## Troubleshooting -| Symptom | Fix | -|---------|-----| -| "sandbox teardown timed out during sync" | Increase `sync.timeout: 600` — big workspaces over slow SSH | -| "sync conflict: host file also changed" | Last-write-wins by default; set `sync.conflict: prompt` to interactively resolve | -| "SSH ControlMaster socket in use" | Another Hermes process on the box is running; `hermes sandbox ps` to find it | -| "Modal sandbox cold-start keeps timing out" | Pre-warm with `hermes sandbox warm modal-big` before interactive work | -| "Daytona hibernate → resume corrupts git state" | Put `.git` in `pull_paths` so Hermes holds the canonical copy | -| "File-sync uploads .venv every time" | Add it to `ignore:` — missed by default in some templates | - -Enable `HERMES_SANDBOX_LOG=debug` to get full tar/ssh command traces. +| Symptom | Fix | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `hermes: invalid choice: sandbox` | Expected on current Hermes. Use `terminal.backend`, `hermes --worktree`, or `hermes kanban ... --workspace worktree`. | +| Config with a legacy sandbox-profile block has no effect | Remove it. Configure `terminal:` or external runtime skills/MCP servers instead. | +| Modal/Daytona backend is unavailable | Check `hermes config check` and `hermes doctor`; use Docker/SSH or a custom integration if your build does not ship that backend. | +| Remote worker edited the wrong checkout | Use Kanban `--workspace worktree`, name branches with `--branch`, and review `git status` before completion. | +| External runtime needs secrets | Put secrets in the vendor's secret store or a protected local env path; do not write them into the repo or `config.yaml`. | +| Need full-agent containment | Terminal backend isolation is not enough; use the whole-process isolation guidance in Part 19. | --- ## What's Next -- [Part 18: Coding Agents](./part18-coding-agents.md) — delegate Claude Code / Codex / Gemini CLI *into* these sandboxes -- [Part 19: Security Playbook](./part19-security-playbook.md) — isolate untrusted MCPs in sandboxes -- [Part 20: Observability & Cost](./part20-observability.md) — track sandbox-hour costs alongside LLM spend -- [Part 1: Setup](./README.md#part-1-setup-stop-fumbling-with-installation) — the base VPS install these extend +- [Part 18: Coding Agents](./part18-coding-agents.md) - delegate Claude Code / Codex / Gemini CLI through Hermes. +- [Part 19: Security Playbook](./part19-security-playbook.md) - understand terminal-backend isolation versus whole-process containment. +- [Part 23: Tenacity Stack](./part23-tenacity-stack.md) - use Kanban, worktrees, checkpoints, and durable worker lanes. +- [Part 17: MCP Servers](./part17-mcp-servers.md) - connect external runtimes and vendor APIs as tool integrations. diff --git a/skills/security/audit-mcp/SKILL.md b/skills/security/audit-mcp/SKILL.md index b9b2743..74342bc 100644 --- a/skills/security/audit-mcp/SKILL.md +++ b/skills/security/audit-mcp/SKILL.md @@ -1,54 +1,58 @@ --- name: audit-mcp -description: Audit every configured MCP server — trust level, allowlist, last-update, risk flags +description: Audit every CLI-managed MCP server - registration, health, source, auth exposure, and risk flags when_to_use: - User asks to audit or review MCP configuration - Scheduled weekly security check - After installing a new MCP server - - Before granting `allow_sampling: true` + - Before keeping a sampling-capable MCP server registered toolsets: - terminal - file --- -# audit-mcp — MCP Server Security Audit +# audit-mcp - MCP Server Security Audit -Walk every server declared in `~/.hermes/config.yaml` under `mcp_servers:` and produce a structured report with risk flags. +Audit the MCP servers registered through the Hermes CLI. Prefer `hermes mcp list/test/configure` over parsing `~/.hermes/config.yaml`; Hermes owns the persisted MCP registry/config shape. ## Procedure -1. **Read the config.** Load `~/.hermes/config.yaml` and extract the `mcp_servers:` block. If the block is empty or missing, report "No MCP servers configured" and exit. +1. **List registered servers.** Run `hermes mcp list`. If it returns no servers, report "No MCP servers configured" and exit. 2. **For each server, collect:** - - Server name and transport (`stdio` if `command:` present, `http` if `url:` present) - - Declared `trust:` level (`trusted` / `community` / `untrusted`; default `community` if unset) - - `allow_sampling:` flag (default `false`) - - `tools_allowlist:` presence and length - - Source identifier: npm package (parse from `args:`), git URL, or HTTP origin + - Server name from `hermes mcp list` + - Health and discovered tools from `hermes mcp test NAME` + - Transport/source shown by `list` or `test` (`stdio` command/package, HTTP URL/origin, or preset) + - Auth mode and environment keys shown by the CLI output; never print secret values + - Any configured roots, profile scope, or tool filters visible through `hermes mcp list` / `hermes mcp test` - Last-updated timestamp: - npm: `npm view time.modified` - git: `git -C log -1 --format=%cI` - http: attempt a `HEAD` and grab `Last-Modified` + - If non-secret details are missing from list/test output, note "unknown" and recommend `hermes mcp configure NAME` for operator review. 3. **Risk-flag each server:** - - 🔴 **HIGH**: `trust: trusted` AND reads untrusted content (web scraping, email parsing, public RSS). List any tool names matching `/scrape|fetch|email|rss|crawl/i` as evidence. - - 🔴 **HIGH**: `allow_sampling: true` AND `trust` is not `trusted`. - - 🟡 **MEDIUM**: last updated > 90 days ago. - - 🟡 **MEDIUM**: no `tools_allowlist` for a server with > 10 tools exposed. - - 🟡 **MEDIUM**: referenced `${VAR}` in `env:` is not set in `~/.hermes/.env`. - - 🟢 **LOW**: unscoped `enabled_for`, making the server available in every profile. + - **HIGH**: `hermes mcp test NAME` fails for a production-registered server. + - **HIGH**: server reads untrusted content (web scraping, email parsing, public RSS) and exposes write/send/exec/file tools. List matching tool names such as `/scrape|fetch|email|rss|crawl|send|write|exec/i` as evidence. + - **HIGH**: sampling-capable server is registered without a reviewed CLI-managed restriction or isolation plan. + - **MEDIUM**: last updated > 90 days ago. + - **MEDIUM**: source package or command is not version-pinned. + - **MEDIUM**: server exposes > 10 tools and no tool filter/scope is visible in CLI output. + - **MEDIUM**: required env/auth key name is missing from the environment used by Hermes. + - **LOW**: scope/profile/root details are unknown; recommend `hermes mcp configure NAME` review. -4. **Render a table.** Columns: name, transport, trust, sampling, tools-allowed / tools-exposed, last-update age, flags. +4. **Render a table.** Columns: name, transport/source, test status, tools exposed, auth/env keys present, last-update age, flags. 5. **Summarize next steps.** Group findings by flag color and recommend: - - HIGH: "Change `trust:` to `community` or `untrusted`, disable sampling, add tools_allowlist." + - HIGH failing test: "Run `hermes mcp configure NAME`, fix command/url/auth/env, then rerun `hermes mcp test NAME`." + - HIGH broad untrusted server: "Remove it from this Hermes home or restrict it with `hermes mcp configure NAME` before use." - MEDIUM stale: "Run `npm update ` or rebuild the git source; verify release notes." - - MEDIUM missing allowlist: "Add `tools_allowlist:` with the specific tools you actually use." + - MEDIUM missing filter/scope: "Use `hermes mcp configure NAME` to expose only the tools and roots you actually use." 6. **Offer to apply fixes.** Ask the user if they'd like to: - - Downgrade any `trusted` → `community` - - Disable `allow_sampling` on flagged servers - - Write a suggested `tools_allowlist` based on `hermes logs` usage history + - Run `hermes mcp configure NAME` for flagged servers + - Remove or re-add a broken server with corrected `hermes mcp add ...` arguments + - Write suggested CLI-managed tool filters based on Hermes usage history Never auto-apply without confirmation. @@ -57,26 +61,30 @@ Never auto-apply without confirmation. Report as markdown. Paste into Telegram / Discord / dashboard as-is. Example: ```markdown -## MCP Security Audit — 2026-04-17 +## MCP Security Audit - 2026-04-17 -### 🔴 HIGH (1) -- **random-scraper** — trusted + reads untrusted content (`scrape_url`, `fetch_rss`) +### HIGH (1) -### 🟡 MEDIUM (2) -- **postgres** — last updated 127 days ago (package @modelcontextprotocol/server-postgres) -- **github** — no tools_allowlist, 34 tools exposed +- **random-scraper** - untrusted-content server exposes broad tools (`scrape_url`, `fetch_rss`, `write_file`) -### 🟢 LOW (1) -- **filesystem** — enabled_for empty, loads in every profile +### MEDIUM (2) + +- **postgres** - last updated 127 days ago (package @modelcontextprotocol/server-postgres) +- **github** - 34 tools exposed and no CLI-visible filter/scope + +### LOW (1) + +- **filesystem** - root/profile scope unknown from list/test output ### Recommendations -1. Change `random-scraper` to `trust: untrusted` and add tools_allowlist. -2. `npm update @modelcontextprotocol/server-postgres`. -3. Scope `github` to the 6 tools actually used in last 30d. + +1. Remove `random-scraper` from this Hermes home or restrict it with `hermes mcp configure random-scraper`. +2. `npm update @modelcontextprotocol/server-postgres`, then `hermes mcp test postgres`. +3. Run `hermes mcp configure github` and expose only the tools actually used in the last 30d. ``` ## Notes - Runs entirely locally. No data leaves the host. - Pair with `cron.yaml` to run weekly (see [Part 19](../../../part19-security-playbook.md#periodic-security-hygiene)). -- Uses `terminal` to exec `npm view` / `git log`; uses `file` to read the config. +- Uses `terminal` to run `hermes mcp list`, `hermes mcp test NAME`, `npm view`, `git log`, and HTTP `HEAD` checks. diff --git a/templates/config/production.yaml b/templates/config/production.yaml index 4be376d..3c14b51 100644 --- a/templates/config/production.yaml +++ b/templates/config/production.yaml @@ -5,7 +5,7 @@ # - Multi-provider with task-aware routing # - Telegram + Discord + Slack + Google Chat + LINE + Teams + email gateways # - LightRAG + mem0 for cross-device memory -# - MCP: GitHub, Postgres, Cloudflare, Linear, filesystem +# - CLI-managed MCP: GitHub, Postgres, Cloudflare, filesystem # - Langfuse tracing, cost alerts, eval hooks # - Hardened approval posture # - All scheduled skills wired to cron @@ -135,41 +135,16 @@ context: compress_model: google/gemini-3.1-flash preserve_last_k: 6 -mcp_servers: - github: - command: npx - args: [-y, "@modelcontextprotocol/server-github"] - env: - GITHUB_PERSONAL_ACCESS_TOKEN: ${GITHUB_PAT} - trust: trusted - allow_sampling: false - tools_allowlist: - - get_pull_request - - list_pull_requests - - get_pull_request_diff - - create_issue - - add_issue_comment - - create_pull_request_review - postgres: - command: npx - args: [-y, "@modelcontextprotocol/server-postgres", "${POSTGRES_URL}"] - trust: trusted - allow_sampling: false - cloudflare: - command: npx - args: [-y, "@cloudflare/mcp-server-cloudflare"] - env: - CLOUDFLARE_API_TOKEN: "${CLOUDFLARE_API_TOKEN}" - trust: trusted - allow_sampling: false - filesystem: - command: npx - args: - - -y - - "@modelcontextprotocol/server-filesystem" - - ${HOME}/Documents - - ${HOME}/Projects - trust: trusted +# MCP servers are CLI-managed. Let `hermes mcp add` write the current persisted +# MCP registry/config shape instead of hand-authoring stale YAML. +# Register and verify them after writing the config: +# hermes mcp add github --command npx --env GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PAT} --args -y @modelcontextprotocol/server-github +# hermes mcp add postgres --command npx --args -y @modelcontextprotocol/server-postgres "${POSTGRES_URL}" +# hermes mcp add cloudflare --command npx --env CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN} --args -y @cloudflare/mcp-server-cloudflare +# hermes mcp add filesystem --command npx --args -y @modelcontextprotocol/server-filesystem ${HOME}/Documents ${HOME}/Projects +# hermes mcp configure NAME +# hermes mcp test NAME +# hermes mcp list security: approval: diff --git a/templates/config/security-hardened.yaml b/templates/config/security-hardened.yaml index 98dfa37..50b56b3 100644 --- a/templates/config/security-hardened.yaml +++ b/templates/config/security-hardened.yaml @@ -91,9 +91,8 @@ security: require_signature: true max_body_bytes: 524288 ttl_seconds: 300 # Reject webhooks older than 5 min - mcp: - default_trust: untrusted # Must be explicitly elevated per-server - require_allowlist: true + # MCP servers are managed with `hermes mcp add` / `hermes mcp configure`. + # Keep this home lean: register only reviewed servers and expose only audited tools. telemetry: level: info diff --git a/templates/systemd/hermes-dashboard.service b/templates/systemd/hermes-dashboard.service index f1e1686..6cf76ce 100644 --- a/templates/systemd/hermes-dashboard.service +++ b/templates/systemd/hermes-dashboard.service @@ -10,11 +10,12 @@ Type=simple User=hermes Group=hermes WorkingDirectory=/home/hermes -ExecStart=/usr/local/bin/hermes dashboard --listen 127.0.0.1:8765 +ExecStart=/usr/bin/env hermes dashboard --host 127.0.0.1 --port 8765 --no-open --skip-build Restart=on-failure RestartSec=5 EnvironmentFile=-/home/hermes/.hermes/.env +Environment=PATH=/home/hermes/.local/bin:/home/hermes/.cargo/bin:/usr/local/bin:/usr/bin:/bin Environment=HOME=/home/hermes StandardOutput=journal diff --git a/templates/systemd/hermes.service b/templates/systemd/hermes.service index f621abb..af9685d 100644 --- a/templates/systemd/hermes.service +++ b/templates/systemd/hermes.service @@ -10,7 +10,7 @@ Type=simple User=hermes Group=hermes WorkingDirectory=/home/hermes -ExecStart=/usr/local/bin/hermes run +ExecStart=/usr/bin/env hermes gateway run ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=5 @@ -19,6 +19,7 @@ StartLimitBurst=5 # Environment EnvironmentFile=-/home/hermes/.hermes/.env +Environment=PATH=/home/hermes/.local/bin:/home/hermes/.cargo/bin:/usr/local/bin:/usr/bin:/bin Environment=HOME=/home/hermes Environment=HERMES_CONFIG=/home/hermes/.hermes/config.yaml