diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a86a307..f8b15c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- **PR #2482** by @Michaelyklam (refs #2453) — Add a durable source/API boundary inventory for the WebUI's remaining Hermes Agent source dependencies: chat execution, runtime events, profiles, goals, slash/plugin commands, provider/auth/model catalogs, redaction parity, and imported Agent/Gateway sessions. The new RFC tracks replacement API contracts before the source mount can be removed. + +### Changed + +- **PR #2482** by @Michaelyklam (refs #2453) — Make the multi-container source boundary more explicit: Docker docs and README now link the boundary inventory, and `docker_init.bash` emits a startup warning when the WebUI sees a writable agent-source mount instead of the default read-only `hermes-agent-src` mount. + ## [v0.51.85] — 2026-05-17 — Release BI (stage-378 — 3-PR batch — workspace-prefix display leakage fix + release-tag update banner + Slice 3a cancel-control gate RFC) ### Fixed diff --git a/README.md b/README.md index 1a941b9e..9d2d2169 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,8 @@ docker compose -f docker-compose.three-container.yml up -d Both compose files use **named Docker volumes** by default, which solves the UID/GID problem by construction. If you need bind mounts to share an existing host directory, see [`docs/docker.md`](docs/docker.md) for the full migration recipe. > **Known limitation (#681)**: in the two-container setup, tools triggered from the WebUI run in the **WebUI container**, not the agent container. If you need git/node/etc. on the WebUI's filesystem, either use the single-container setup, extend the WebUI Dockerfile, or use the community [all-in-one image](https://github.com/sunnysktsang/hermes-suite). +> +> **Source boundary note (#2453)**: the multi-container setup mounts `hermes-agent-src` read-only into the WebUI by default. This prevents WebUI-side source rewrites but is still an implementation-coupling bridge, not a stable Agent API boundary. See [`docs/rfcs/agent-source-boundary.md`](docs/rfcs/agent-source-boundary.md) for the current source/API decoupling inventory. ### Common failure modes diff --git a/docker_init.bash b/docker_init.bash index 4cbc4467..e70a0889 100644 --- a/docker_init.bash +++ b/docker_init.bash @@ -391,6 +391,15 @@ else fi done if [ -n "$_agent_src" ]; then + if [ -w "$_agent_src" ]; then + echo "" + echo "!! WARNING: hermes-agent source mount is writable from the WebUI container." + echo "!! Path: $_agent_src" + echo "!! The multi-container compose defaults use a read-only mount for defence-in-depth." + echo "!! If this is not an intentional local development checkout, switch the WebUI" + echo "!! agent source volume/bind mount to read-only. See docs/rfcs/agent-source-boundary.md." + echo "" + fi uv pip install "$_agent_src[all]" --trusted-host pypi.org --trusted-host files.pythonhosted.org || error_exit "Failed to install hermes-agent's requirements" else echo "" diff --git a/docs/docker.md b/docs/docker.md index 4d1a5944..f238773b 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -211,6 +211,8 @@ What multi-container does **not** isolate: If you need **filesystem isolation** between the chat UI and the agent (e.g. you don't trust the WebUI to read agent state), the multi-container setup is not enough — run the agent on a separate host and connect the WebUI to it via the gateway HTTP API. If you don't need any boundary, the single-container setup is simpler. +The direct source mount is a compatibility bridge, not the long-term API contract. The current source/API boundary inventory and decoupling task list live in [`docs/rfcs/agent-source-boundary.md`](rfcs/agent-source-boundary.md) for [#2453](https://github.com/nesquena/hermes-webui/issues/2453). If you customize the compose files with bind mounts, keep the WebUI-side agent source mount read-only unless you are intentionally doing local development; `docker_init.bash` warns at startup when that path is writable. + ## Bind-mount migration (advanced) If you really need to bind-mount an existing host `~/.hermes` (e.g. you're keeping config in dotfiles, sharing with a non-Docker `hermes` install, etc.): diff --git a/docs/rfcs/agent-source-boundary.md b/docs/rfcs/agent-source-boundary.md new file mode 100644 index 00000000..4cb527cd --- /dev/null +++ b/docs/rfcs/agent-source-boundary.md @@ -0,0 +1,70 @@ +# Agent Source Boundary and API Decoupling Inventory + +- **Status:** Proposed +- **Created:** 2026-05-17 +- **Tracking issue:** [#2453](https://github.com/nesquena/hermes-webui/issues/2453) + +## Problem + +The WebUI currently depends on Hermes Agent Python source being importable at +runtime. In local installs this usually means a neighboring checkout; in the +multi-container Docker setup it means the WebUI reads the `hermes-agent-src` +volume that the agent container also uses. + +That source mount is a compatibility bridge, not the desired long-term contract. +Even when mounted read-only on the WebUI side, it couples WebUI releases to +Hermes Agent internal module layout and makes the multi-container setup look more +isolated than it really is. + +## Current safety posture + +- The multi-container compose files mount `hermes-agent-src` read-only into the + WebUI service by default. +- `docker_init.bash` prunes the agent source subtree from `chown` so read-only + mounts do not break startup. +- If an operator overrides the compose files with a mutable agent-source mount, + startup now emits a notable warning. The WebUI still starts because local dev + checkouts and custom deployments may intentionally be writable, but the warning + makes the reduced boundary explicit. + +## Source-access inventory + +These are the current WebUI capabilities that still rely on Agent source or +`hermes_cli`/`agent` modules being importable. Each item should eventually move +behind an explicit, versioned Agent API or a packaged library contract that does +not require mounting the live source checkout. + +| WebUI capability | Current dependency | Desired API / contract | Notes | +|---|---|---|---| +| Browser chat execution | `run_agent.AIAgent` imported by `api/streaming.py` | Run lifecycle API: start, observe, status, cancel, approval, clarify, final usage | Covered by the runtime-adapter migration in [#1925](https://github.com/nesquena/hermes-webui/issues/1925), but still source-backed today. | +| Runtime event rendering | WebUI callbacks around Agent token/reasoning/tool events | Stable event envelope for tokens, reasoning, progress, tool lifecycle, approvals, clarify, errors, and final usage | The existing run-adapter RFC describes the browser-facing shape; Agent still needs a durable producer contract. | +| Profile list/create/delete/seed | `hermes_cli.profiles` from `api/profiles.py` | Profile management API with profile metadata, env/runtime context, seed/delete operations, and validation errors | WebUI has fallback filesystem handling for some operations, but feature parity follows Hermes CLI internals. | +| Goal command state | `hermes_cli.goals` from `api/goals.py` | Goal CRUD/control API: get, save, pause/resume/clear, and status | Should preserve current `/goal` WebUI behavior without direct module import. | +| Slash command registry and plugin commands | `hermes_cli.commands` and `hermes_cli.plugins` from `api/commands.py` | Command/plugin capability discovery API scoped by active profile | WebUI should render command help from a stable capability response. | +| Provider/auth/model catalogs | `hermes_cli.models`, `hermes_cli.auth`, and `agent.credential_pool` from `api/config.py` | Provider registry, model catalog, auth status, OAuth/credential-pool status APIs | WebUI has static fallbacks, but exact parity and custom provider state come from Agent internals. | +| Redaction helper parity | `agent.redact.redact_sensitive_text` from `api/helpers.py` | Redaction service/library contract with signature/version compatibility | WebUI keeps a fallback redactor because this import has changed before. | +| CLI/Gateway session bridge | Agent `state.db` schema and gateway metadata read by sidebar/session helpers | Session listing/transcript/metadata API for non-WebUI-originated sessions | Direct SQLite/schema coupling should narrow over time, especially for messaging/email/gateway sessions. | + +## Decoupling task list + +1. Keep the Docker default safe: WebUI-side `hermes-agent-src` stays read-only in + two- and three-container compose files. +2. Keep documenting the boundary honestly: multi-container isolates process, + network, and resources, not filesystem/source compatibility. +3. Warn loudly when the WebUI container sees a writable agent-source mount in + Docker, because that weakens the defense-in-depth posture. +4. Convert runtime execution first through the #1925 RuntimeAdapter path instead + of adding new direct imports. +5. For each inventory row, file or link a follow-up that defines the Agent API + response shape before replacing the import. +6. Do not claim the source mount can be removed until chat execution, provider + catalogs/auth status, profiles, goals, commands/plugins, redaction, and + imported Agent/Gateway sessions all have stable replacement contracts. + +## Non-goals for this slice + +- Do not remove `HERMES_WEBUI_AGENT_DIR`. +- Do not break local source-checkout development. +- Do not fail startup solely because the agent source is writable. +- Do not replace the runtime adapter or Hermes Agent API in this document-only + inventory slice. diff --git a/tests/test_issue2453_agent_source_boundary.py b/tests/test_issue2453_agent_source_boundary.py new file mode 100644 index 00000000..8aa93b36 --- /dev/null +++ b/tests/test_issue2453_agent_source_boundary.py @@ -0,0 +1,67 @@ +"""Regression coverage for issue #2453 agent-source boundary docs/warnings.""" + +from __future__ import annotations + +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] + + +def test_agent_source_boundary_rfc_inventories_import_coupling(): + """The #2453 source-boundary work must keep a concrete import inventory. + + The risk in #2453 is not just a Docker mount mode; it is that WebUI behavior + still relies on Hermes Agent internals. Pinning these rows prevents the docs + from degrading into a vague security note without the follow-up task list. + """ + doc = REPO / "docs" / "rfcs" / "agent-source-boundary.md" + assert doc.exists(), "#2453 needs a durable source/API boundary RFC" + src = doc.read_text(encoding="utf-8") + + required_terms = [ + "run_agent.AIAgent", + "hermes_cli.profiles", + "hermes_cli.goals", + "hermes_cli.commands", + "hermes_cli.plugins", + "hermes_cli.models", + "hermes_cli.auth", + "agent.credential_pool", + "agent.redact.redact_sensitive_text", + "state.db", + ] + for term in required_terms: + assert term in src, f"agent source-boundary RFC must inventory {term}" + + for api_phrase in ( + "Run lifecycle API", + "Profile management API", + "Command/plugin capability discovery API", + "Provider registry, model catalog, auth status", + "Session listing/transcript/metadata API", + ): + assert api_phrase in src, f"RFC must name replacement contract: {api_phrase}" + + +def test_docker_startup_warns_when_agent_source_mount_is_writable(): + """Mutable WebUI-side agent source mounts should be visibly discouraged. + + The compose default is read-only, but custom bind mounts can still make the + WebUI's agent source path writable. The entrypoint should warn instead of + silently accepting a weakened boundary. + """ + src = (REPO / "docker_init.bash").read_text(encoding="utf-8") + + assert "agent source mount is writable" in src + assert "read-only mount" in src + assert "$_agent_src" in src + assert "-w \"$_agent_src\"" in src + + +def test_docker_docs_link_source_boundary_inventory(): + """Docker docs should link the #2453 inventory from the boundary section.""" + src = (REPO / "docs" / "docker.md").read_text(encoding="utf-8") + + assert "agent-source-boundary.md" in src + assert "source/API boundary inventory" in src + assert "#2453" in src