mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
docs(docker): document new persist-across-processes contract and orphan reaper (#20561)
Updates the Docker Backend section of the user-guide configuration page to match the actual behavior shipped in PR #33645. Pre-PR the docs claimed "container is stopped and removed on shutdown," which was never quite true for the documented happy path and is now actively wrong: in default mode the container survives across Hermes processes so background processes (npm watchers, dev servers, long-running pytest) carry over the way the "ONE long-lived container shared across sessions" promise requires. Changes to `website/docs/user-guide/configuration.md`: * Reworked the intro paragraph at the top of the Docker Backend section to describe the actual cross-process reuse contract. * Expanded the YAML example with the new keys `docker_persist_across_processes` and `docker_orphan_reaper`, plus the pre-existing-but-undocumented `docker_env`, `timeout`, and `lifetime_seconds`. Clarified the `container_persistent` comment to disambiguate from `docker_persist_across_processes`. * Added a `docker_env` vs `docker_forward_env` explainer (one injects literal KEY=value, the other forwards values from the host/.env — easy to confuse). * Replaced the one-line "Container lifecycle" paragraph with a full subsection covering: - the three labels Hermes tags every container with (hermes-agent, hermes-task-id, hermes-profile) - the label-probe reuse mechanism on startup - a teardown-trigger table with four rows for every situation that destroys the container in default mode - edge cases (OOM kill, profile switching) * Added an "Environment variable overrides" table covering all TERMINAL_* env vars relevant to the Docker backend, including the previously-undocumented `TERMINAL_DOCKER_ENV` and `HERMES_DOCKER_BINARY`. Changes to `website/docs/user-guide/docker.md`: * Extended the cross-link admonition (around l.227) so the Hermes-in-Docker page points at the new terminal-backend keys (`docker_env`, `docker_persist_across_processes`, `docker_orphan_reaper`) alongside the ones already mentioned. No code changes. Behavior already covered by tests added in earlier commits on this branch (#33645 commits 1-5). Refs #20561
This commit is contained in:
parent
2f0f03c40d
commit
3c6e70aef1
2 changed files with 66 additions and 5 deletions
|
|
@ -130,7 +130,7 @@ The agent has the same filesystem access as your user account. Use `hermes tools
|
|||
|
||||
Runs commands inside a Docker container with security hardening (all capabilities dropped, no privilege escalation, PID limits).
|
||||
|
||||
**Single persistent container, not per-command.** Hermes starts ONE long-lived container on first use and routes every terminal, file, and `execute_code` call through `docker exec` into that same container — across sessions, `/new`, `/reset`, and `delegate_task` subagents — for the lifetime of the Hermes process. Working-directory changes, installed packages, and files in `/workspace` carry over from one tool call to the next, just like a local shell. The container is stopped and removed on shutdown. See **Container lifecycle** below for details.
|
||||
**Single persistent container, shared across Hermes processes.** Hermes starts ONE long-lived container on first use and routes every terminal, file, and `execute_code` call through `docker exec` into that same container — across sessions, `/new`, `/reset`, and `delegate_task` subagents. Working-directory changes, installed packages, files in `/workspace`, and **background processes** all carry over from one tool call to the next, and from one Hermes process to the next. When you close a TUI session, run `/quit`, or start a new `hermes` invocation, the container keeps running and the next Hermes process reuses it via a labeled lookup. See **Container lifecycle** below for the exact teardown rules.
|
||||
|
||||
```yaml
|
||||
terminal:
|
||||
|
|
@ -138,8 +138,11 @@ terminal:
|
|||
docker_image: "nikolaik/python-nodejs:python3.11-nodejs20"
|
||||
docker_mount_cwd_to_workspace: false # Mount launch dir into /workspace
|
||||
docker_run_as_host_user: false # See "Running container as host user" below
|
||||
docker_forward_env: # Env vars to forward into container
|
||||
docker_forward_env: # Host env vars to forward into container
|
||||
- "GITHUB_TOKEN"
|
||||
docker_env: # Literal env vars to inject (KEY=value)
|
||||
DEBUG: "1"
|
||||
PYTHONUNBUFFERED: "1"
|
||||
docker_volumes: # Host directory mounts
|
||||
- "/home/user/projects:/workspace/projects"
|
||||
- "/home/user/data:/data:ro" # :ro for read-only
|
||||
|
|
@ -151,14 +154,49 @@ terminal:
|
|||
container_cpu: 1 # CPU cores (0 = unlimited)
|
||||
container_memory: 5120 # MB (0 = unlimited)
|
||||
container_disk: 51200 # MB (requires overlay2 on XFS+pquota)
|
||||
container_persistent: true # Persist /workspace and /root across sessions
|
||||
container_persistent: true # Persist /workspace and /root bind-mount dirs
|
||||
|
||||
# Cross-process container reuse (defaults match the "one long-lived
|
||||
# container shared across sessions" contract — see Container lifecycle).
|
||||
docker_persist_across_processes: true # Reuse container across Hermes restarts
|
||||
docker_orphan_reaper: true # Sweep abandoned Exited containers at startup
|
||||
|
||||
# Cross-backend lifecycle settings (apply to docker as well)
|
||||
timeout: 180 # Per-command timeout in seconds
|
||||
lifetime_seconds: 300 # Idle-reaper window; also feeds 2× orphan-reaper threshold
|
||||
```
|
||||
|
||||
**`docker_env`** vs **`docker_forward_env`**: the former injects literal `KEY=value` pairs you specify in the config (the values live in your `config.yaml` or are passed as a JSON dict via `TERMINAL_DOCKER_ENV='{"DEBUG":"1"}'`). The latter forwards values from your shell or `~/.hermes/.env`, so the actual secret never appears in the config file. Use `docker_forward_env` for tokens and `docker_env` for static knobs the container needs.
|
||||
|
||||
**`terminal.docker_extra_args`** (also overridable via `TERMINAL_DOCKER_EXTRA_ARGS='["--gpus=all"]'`) lets you pass arbitrary `docker run` flags that Hermes doesn't surface as first-class keys — `--gpus`, `--network`, `--add-host`, alternative `--security-opt` overrides, etc. Each entry must be a string; the list is appended last to the assembled `docker run` invocation so it can override Hermes' defaults if needed. Use sparingly — flags that conflict with the sandbox hardening (capability drops, `--user`, the workspace bind mount) will silently weaken isolation.
|
||||
|
||||
**Requirements:** Docker Desktop or Docker Engine installed and running. Hermes probes `$PATH` plus common macOS install locations (`/usr/local/bin/docker`, `/opt/homebrew/bin/docker`, Docker Desktop app bundle). Podman is supported out of the box: set `HERMES_DOCKER_BINARY=podman` (or the full path) to force it when both are installed.
|
||||
|
||||
**Container lifecycle:** Hermes reuses a single long-lived container (`docker run -d ... sleep 2h`) for every terminal and file-tool call, across sessions, `/new`, `/reset`, and `delegate_task` subagents, for the lifetime of the Hermes process. Commands run via `docker exec` with a login shell, so working-directory changes, installed packages, and files in `/workspace` all persist from one tool call to the next. The container is stopped and removed on Hermes shutdown (or when the idle-sweep reclaims it).
|
||||
#### Container lifecycle
|
||||
|
||||
Every Hermes-managed container is tagged with three labels so subsequent processes (and the orphan reaper) can identify it:
|
||||
|
||||
- `hermes-agent=1` — marks it as Hermes-managed
|
||||
- `hermes-task-id=<sanitized task_id>` — keys the per-task reuse probe
|
||||
- `hermes-profile=<sanitized profile name>` — scopes reuse and reaping to the active Hermes profile
|
||||
|
||||
On startup, Hermes runs `docker ps --filter label=hermes-task-id=<id> --filter label=hermes-profile=<profile>` and **attaches to the existing container** when it finds one. If the container is `exited` (e.g. after a Docker daemon restart), it's `docker start`'d and reused — filesystem state and any installed packages survive, but in-container background processes do not.
|
||||
|
||||
When a Hermes process exits — `/quit`, closing a TUI session, gateway shutdown, even SIGKILL — the cleanup path is a **no-op for the container in default mode**. The container keeps running. The next Hermes process attaches to it in milliseconds via the label probe. This is the behavior the "one long-lived container shared across sessions" contract requires: it's the only way background processes (npm watchers, dev servers, long-running pytest) survive across sessions.
|
||||
|
||||
**The container is only torn down (stopped and `docker rm -f`'d) in these cases:**
|
||||
|
||||
| Trigger | When it fires |
|
||||
|---|---|
|
||||
| `docker_persist_across_processes: false` | Explicit per-process isolation. Every `cleanup()` does `stop` + `rm -f`. Matches pre-issue-#20561 behavior. |
|
||||
| Idle reaper (`lifetime_seconds`, default 300s) | Only when the env is `persist_across_processes=false`. Persist-mode envs are no-op'd; container survives the idle sweep. |
|
||||
| Orphan reaper at next startup | Sweeps **Exited** hermes-labeled containers older than `2 × lifetime_seconds` (default 600s = 10 min), scoped to the current profile. **Running containers are never touched** — sibling-process safety. Set `docker_orphan_reaper: false` to disable. |
|
||||
| Direct user action | `docker rm -f`, `docker system prune`, Docker Desktop restart. We don't set `--restart=always`, so a host reboot leaves the container `Exited` (its CoW layer survives and gets reused on next startup, but bg processes are gone). |
|
||||
|
||||
Edge cases worth knowing:
|
||||
|
||||
- **OOM kill of in-container PID 1** transitions the container to `Exited`. Next reuse will `docker start` it; filesystem state survives, bg processes do not.
|
||||
- **Switching profiles** isolates containers from each other — a container labeled `hermes-profile=work` is invisible to a Hermes process running under `hermes-profile=research`. The orphan reaper is profile-scoped too, so cross-profile containers don't get reaped accidentally, but they also won't get cleaned up automatically until you start Hermes again under their original profile.
|
||||
|
||||
Parallel subagents spawned via `delegate_task(tasks=[...])` share this one container — concurrent `cd`, env mutations, and writes to the same path will collide. If a subagent needs an isolated sandbox, it must register a per-task image override via `register_task_env_overrides()`, which RL and benchmark environments (TerminalBench2, HermesSweEnv, etc.) do automatically for their per-task Docker images.
|
||||
|
||||
|
|
@ -170,6 +208,29 @@ Parallel subagents spawned via `delegate_task(tasks=[...])` share this one conta
|
|||
|
||||
**Credential forwarding:** Env vars listed in `docker_forward_env` are resolved from your shell environment first, then `~/.hermes/.env`. Skills can also declare `required_environment_variables` which are merged automatically.
|
||||
|
||||
#### Environment variable overrides
|
||||
|
||||
Every key under `terminal:` has an env-var override of the form `TERMINAL_<KEY_UPPERCASE>`. The most useful ones for the Docker backend:
|
||||
|
||||
| Env var | Maps to | Notes |
|
||||
|---|---|---|
|
||||
| `TERMINAL_DOCKER_IMAGE` | `docker_image` | Base image |
|
||||
| `TERMINAL_DOCKER_FORWARD_ENV` | `docker_forward_env` | JSON array: `'["GITHUB_TOKEN","OPENAI_API_KEY"]'` |
|
||||
| `TERMINAL_DOCKER_ENV` | `docker_env` | JSON dict: `'{"DEBUG":"1"}'` |
|
||||
| `TERMINAL_DOCKER_VOLUMES` | `docker_volumes` | JSON array of `"host:container[:ro]"` strings |
|
||||
| `TERMINAL_DOCKER_EXTRA_ARGS` | `docker_extra_args` | JSON array |
|
||||
| `TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE` | `docker_mount_cwd_to_workspace` | `true` / `false` |
|
||||
| `TERMINAL_DOCKER_RUN_AS_HOST_USER` | `docker_run_as_host_user` | `true` / `false` |
|
||||
| `TERMINAL_DOCKER_PERSIST_ACROSS_PROCESSES` | `docker_persist_across_processes` | `true` / `false` — default `true` |
|
||||
| `TERMINAL_DOCKER_ORPHAN_REAPER` | `docker_orphan_reaper` | `true` / `false` — default `true` |
|
||||
| `TERMINAL_CONTAINER_CPU` | `container_cpu` | CPU cores |
|
||||
| `TERMINAL_CONTAINER_MEMORY` | `container_memory` | MB |
|
||||
| `TERMINAL_CONTAINER_DISK` | `container_disk` | MB |
|
||||
| `TERMINAL_CONTAINER_PERSISTENT` | `container_persistent` | `true` / `false` — controls the bind-mount workspace dirs, distinct from `docker_persist_across_processes` |
|
||||
| `TERMINAL_LIFETIME_SECONDS` | `lifetime_seconds` | Idle reaper window |
|
||||
| `TERMINAL_TIMEOUT` | `timeout` | Per-command timeout |
|
||||
| `HERMES_DOCKER_BINARY` | _none_ | Force a specific docker/podman binary path |
|
||||
|
||||
### SSH Backend
|
||||
|
||||
Runs commands on a remote server over SSH. Uses ControlMaster for connection reuse (5-minute idle keepalive). Persistent shell is enabled by default — state (cwd, env vars) survives across commands.
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ docker run -it --rm \
|
|||
Direct `-e` flags override values from `.env`. This is useful for CI/CD or secrets-manager integrations where you don't want keys on disk.
|
||||
|
||||
:::note Looking for Docker as the **terminal backend**?
|
||||
This page covers running Hermes itself inside Docker. If you want Hermes to execute the agent's `terminal` / `execute_code` calls inside a Docker sandbox container (one persistent container per Hermes process), that's a separate config block — `terminal.backend: docker` plus `terminal.docker_image`, `terminal.docker_volumes`, `terminal.docker_forward_env`, `terminal.docker_run_as_host_user`, and `terminal.docker_extra_args`. See [Configuration → Docker Backend](configuration.md#docker-backend) for the full set.
|
||||
This page covers running Hermes itself inside Docker. If you want Hermes to execute the agent's `terminal` / `execute_code` calls inside a Docker sandbox container (one long-lived container shared across Hermes processes — see issue #20561), that's a separate config block — `terminal.backend: docker` plus `terminal.docker_image`, `terminal.docker_volumes`, `terminal.docker_forward_env`, `terminal.docker_env`, `terminal.docker_run_as_host_user`, `terminal.docker_extra_args`, `terminal.docker_persist_across_processes`, and `terminal.docker_orphan_reaper`. See [Configuration → Docker Backend](configuration.md#docker-backend) for the full set including container-lifecycle rules.
|
||||
:::
|
||||
|
||||
## Docker Compose example
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue