fix(docker): per-session container isolation and session-scoped workspace mounts - #82731
Merged
Merged
Conversation
…pace mounts Two bugs reported on the docker terminal backend (desktop app, sandboxed profiles with container_persistent: false): 1. A NEW chat's container inherited the PREVIOUS session's workspace, bind-mounted rw at /workspace, because the mount source was the process-global TERMINAL_CWD env var (written by the workspace picker, outliving its session) and all sessions shared one 'default' container. 2. Every command failed with exit 126 because the desktop gateway recorded the HOST launch directory as the session cwd, and each command was prefixed with 'cd /Users/<user>/...' inside the container. Fixes (class-wide, single owners): - container_persistent: false + docker now keys containers PER SESSION: fresh container per chat, removed at session close/idle. delegate_task children share the parent's container via an explicit alias registry. container_persistent: true keeps the documented ONE-long-lived-container contract unchanged. - _resolve_task_host_cwd() is the single owner of the cwd->/workspace mount policy across all four env-creation sites; under isolation it refuses process-global cwd sources and mounts only the session's own attached workspace (tui_gateway now tags overrides with cwd_source). - _resolve_command_cwd() gains the same host-path guard the env-creation sites already had (#50636/#54447 sibling site): a recorded host cwd is discarded on container backends instead of cd-ing every command into a nonexistent path. E2E-tested against real Docker: distinct containers per session, no stale mount in a fresh session, no exit 126 from host cwd records, containers removed at session teardown.
Contributor
૮ >ﻌ< ა ci reviewran on 905b834 — fix(docker): per-session container isolation and session-sco all good! |
This was referenced Aug 10, 2026
4 tasks
benbarclay
added a commit
that referenced
this pull request
Aug 24, 2026
PR-review finding (andrexibiza, blocker 1): container_persistent: false is the repository-wide per-session isolation contract (#82731), but the per-session branch in _resolve_container_task_id applied to Docker only. Non-persistent Sprites still collapsed to 'default', resolved the same deterministic profile-scoped name, and unconditionally get_sprite()d it — so two independent ephemeral runs could attach one live VM and either cleanup could delete it out from under the other, and a crashed run's stale survivor was silently resumed. - terminal_tool: the isolation authority generalizes to _session_isolation_enabled() ({docker, sprites} + non-persistent); _docker_session_isolation_enabled() remains as the docker-gated view so docker-only paths (workspace mount selection, session-scoped container teardown) are unchanged. Delegated children still alias to their parent via the existing alias registry. - SpritesEnvironment: an ephemeral constructor now mints a unique hermes-eph-{task}-{nonce12} name and only ever CREATES — it never adopts a pre-existing Sprite. Persistent mode keeps resume-by-name with the race-safe create-or-adopt. - Tests: ephemeral-never-adopts, unique-per-construction, DNS-bounded ephemeral names, and the terminal_tool keying contract for sprites (per-session when non-persistent, shared 'default' when persistent; docker-only helper stays False for sprites). #82731's own suite is unchanged and green (27/27). The live-suite fixture pins the ephemeral naming path into the run-unique test namespace too. Docs state the single-use ephemeral behavior.
teknium1
added a commit
that referenced
this pull request
Aug 25, 2026
…er-session Commit a270c4a's session-key fallback in _resolve_container_task_id was added to stop cross-profile SSH environment reuse, but it wasn't backend- gated: persistent Docker silently fragmented into one container per gateway session, breaking the product contract (one long-lived container per profile, shared by CLI and every session of that profile). #93950's vanishing MEDIA attachments were downstream damage. - persistent Docker (container_persistent: true) now keys to the profile: literal 'default' for the default profile (same container as CLI), 'profile:<name>' for named profiles - SSH and non-persistent Docker keep session scoping (the original leak fix and the #82731 isolation contract are untouched) - gateway MEDIA translation follows the profile layout and keeps the legacy bug-window per-session sandboxes as fallback candidates, trying each until the file resolves — old sessions self-heal, no migration - /root/.hermes credential-surface refusal preserved across all layouts
teknium1
added a commit
that referenced
this pull request
Aug 25, 2026
…er-session Commit a270c4a's session-key fallback in _resolve_container_task_id was added to stop cross-profile SSH environment reuse, but it wasn't backend- gated: persistent Docker silently fragmented into one container per gateway session, breaking the product contract (one long-lived container per profile, shared by CLI and every session of that profile). #93950's vanishing MEDIA attachments were downstream damage. - persistent Docker (container_persistent: true) now keys to the profile: literal 'default' for the default profile (same container as CLI), 'profile:<name>' for named profiles - SSH and non-persistent Docker keep session scoping (the original leak fix and the #82731 isolation contract are untouched) - gateway MEDIA translation follows the profile layout and keeps the legacy bug-window per-session sandboxes as fallback candidates, trying each until the file resolves — old sessions self-heal, no migration - /root/.hermes credential-surface refusal preserved across all layouts
and7777
pushed a commit
to and7777/hermes-agent
that referenced
this pull request
Aug 27, 2026
…er-session Commit a270c4a's session-key fallback in _resolve_container_task_id was added to stop cross-profile SSH environment reuse, but it wasn't backend- gated: persistent Docker silently fragmented into one container per gateway session, breaking the product contract (one long-lived container per profile, shared by CLI and every session of that profile). NousResearch#93950's vanishing MEDIA attachments were downstream damage. - persistent Docker (container_persistent: true) now keys to the profile: literal 'default' for the default profile (same container as CLI), 'profile:<name>' for named profiles - SSH and non-persistent Docker keep session scoping (the original leak fix and the NousResearch#82731 isolation contract are untouched) - gateway MEDIA translation follows the profile layout and keeps the legacy bug-window per-session sandboxes as fallback candidates, trying each until the file resolves — old sessions self-heal, no migration - /root/.hermes credential-surface refusal preserved across all layouts
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
…er-session Commit a270c4a's session-key fallback in _resolve_container_task_id was added to stop cross-profile SSH environment reuse, but it wasn't backend- gated: persistent Docker silently fragmented into one container per gateway session, breaking the product contract (one long-lived container per profile, shared by CLI and every session of that profile). NousResearch#93950's vanishing MEDIA attachments were downstream damage. - persistent Docker (container_persistent: true) now keys to the profile: literal 'default' for the default profile (same container as CLI), 'profile:<name>' for named profiles - SSH and non-persistent Docker keep session scoping (the original leak fix and the NousResearch#82731 isolation contract are untouched) - gateway MEDIA translation follows the profile layout and keeps the legacy bug-window per-session sandboxes as fallback candidates, trying each until the file resolves — old sessions self-heal, no migration - /root/.hermes credential-surface refusal preserved across all layouts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Docker sessions with
container_persistent: falsenow get one fresh container per session, and a new chat can no longer inherit the previous session's workspace mount or fail every command with exit 126 from a host-pathcd.Reported by lpha3ch0 (Discord): sandboxed cybersecurity profile on the desktop app saw (1) a brand-new chat's container carrying a previous session's repo bind-mounted rw at
/workspace, and (2) every command prefixed withcd /Users/<user>/dev/<repo>inside the container → exit 126.Root causes
/workspacebind-mount source was the process-globalTERMINAL_CWDenv var (written by the desktop workspace picker, outlives its session), and all sessions collapsed onto one shared"default"container — so session B attached to session A's container and mounts._resolve_command_cwdused it verbatim, so every commandcd'd to a nonexistent host path inside the sandbox. The host-path sanitizer existed but only at the env-creation sites (fix(terminal): sanitize host/relative cwd override before it reaches docker run -w #50636/[Bug]: file tools build their Docker sandbox with an un-sanitized host cwd (search_files sees empty workspace); terminal-side fix #50636 left file_tools.py untouched #54447) — this was the unguarded sibling site.Changes
tools/terminal_tool.py: per-session container keying when docker +container_persistent: false(_docker_session_isolation_enabled); subagent→parent container alias registry (register_container_alias);_resolve_task_host_cwd()as the single owner of the cwd→/workspace mount policy across all 4 env-creation sites — refuses process-global cwd sources under isolation;_resolve_command_cwd()gains the container host-path guard (env_typeparam, wired at all 4 call sites); session-scoped containers disable cross-process persist and are removed at session close/idle instead of leaking one running container per chat ever opened.tools/delegate_tool.py: children register a container alias at spawn so subagents share the parent session's sandbox.tui_gateway/server.py:_terminal_task_cwd_with_source()tags cwd overridessessionvsprocess; a session-attached workspace now wins over the stale process env var.tools/file_tools.py,tools/code_execution_tool.py: mount resolution routed through the shared owner.website/docs/user-guide/configuration.md: per-session isolation mode documented under Docker Backend.tests/tools/test_docker_session_isolation.py: 27 tests — keying, alias chain (incl. cycle safety), mount policy, host-cwd-record guard, lifecycle flags.Default behavior (
container_persistent: true) is unchanged: ONE long-lived shared container, cross-process reuse, RL/benchmark override isolation all as before.Validation
/workspace(no leak from session A)cdstill persists across commandsInfographic