From bd112bc8b31eba2ae705f070677a34c3fc64baf6 Mon Sep 17 00:00:00 2001 From: John Paul Soliva Date: Wed, 23 Sep 2026 14:23:30 +0900 Subject: [PATCH] fix(docker): the image's `gateway run` keeps the root profile, and a leading -p reaches hermes Inside the s6 image a bare `gateway run` (the image's CMD) redirects to the supervised slot of the current profile. The profile pre-parse applied the sticky active_profile first, so after `hermes profile use ` (or a dashboard profile switch) every container boot started that named slot: the one the boot reconciler had just registered down, because a started named slot is a second gateway beside the multiplexer. The redirected run now keeps the root identity like any supervised slot (#74872); --no-supervise keeps the foreground behaviour that follows active_profile. The obvious workaround, pinning the CMD to `-p default gateway run`, restart-looped the container: main-wrapper.sh probes `command -v "$1"`, and `command -v -p` parses -p as an option to `command` and succeeds, so the wrapper exec'd "-p". A leading flag now always goes to hermes. --- docker/main-wrapper.sh | 16 ++++++++--- hermes_cli/main.py | 21 +++++++++++++- .../hermes_cli/test_apply_profile_override.py | 28 +++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/docker/main-wrapper.sh b/docker/main-wrapper.sh index efabad9839..36934990e7 100755 --- a/docker/main-wrapper.sh +++ b/docker/main-wrapper.sh @@ -82,10 +82,18 @@ if [ $# -eq 0 ]; then drop hermes fi -if command -v "$1" >/dev/null 2>&1; then - # Bare executable — pass through directly. - drop "$@" -fi +# A leading flag is a hermes global option (`-p gateway run`), never an executable: +# `command -v -p` parses -p as an option to `command` itself and succeeds, so the wrapper exec'd +# "-p" and the container restart-looped. +case "$1" in + -*) ;; + *) + if command -v "$1" >/dev/null 2>&1; then + # Bare executable — pass through directly. + drop "$@" + fi + ;; +esac # Hermes subcommand pass-through. drop hermes "$@" diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 4026861eb2..231ff8f5be 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -577,6 +577,24 @@ def _desktop_ssh_backend(argv: list) -> bool: return "--ssh-session-token-file" in argv +def _s6_supervised_gateway_run(argv: list) -> bool: + """A bare ``gateway run`` inside the s6 image names the ``gateway-default`` slot too. + + ``_maybe_redirect_run_to_s6_supervision`` turns it into a start of the supervised slot for the + current profile, and it is the image's own CMD. Following the sticky ``active_profile`` there + started that profile's named slot on every container boot: the one the boot reconciler just + registered down, because a started named slot is a second gateway beside the multiplexer. + ``--no-supervise`` keeps the foreground run, which follows ``active_profile`` as before (#22502). + """ + words = [a for a in argv if not a.startswith("-")] + if words[:2] != ["gateway", "run"] or "--no-supervise" in argv: + return False + if os.environ.get("HERMES_GATEWAY_NO_SUPERVISE", "").lower() in ("1", "true", "yes"): + return False + from hermes_cli.service_manager import _s6_running + return _s6_running() + + def _apply_profile_override() -> None: """Pre-parse --profile/-p and set HERMES_HOME before imports.""" argv = sys.argv[1:] @@ -596,7 +614,8 @@ def _apply_profile_override() -> None: if profile_name is None and hermes_home_env and os.environ.get("HERMES_UPDATE_POST_SWAP") == "1": return - if profile_name is None and not _under_gateway_supervisor(argv) and not _desktop_ssh_backend(argv): + if (profile_name is None and not _under_gateway_supervisor(argv) and not _desktop_ssh_backend(argv) + and not _s6_supervised_gateway_run(argv)): try: from hermes_constants import get_default_hermes_root diff --git a/tests/hermes_cli/test_apply_profile_override.py b/tests/hermes_cli/test_apply_profile_override.py index 74f1add671..363c15aab2 100644 --- a/tests/hermes_cli/test_apply_profile_override.py +++ b/tests/hermes_cli/test_apply_profile_override.py @@ -307,3 +307,31 @@ class TestGeneralizedSupervisorMarkers: plist = generate_launchd_plist() assert "HERMES_SUPERVISED_CHILD" in plist + + +class TestS6ContainerGatewayRun: + """Inside the s6 image a bare ``gateway run`` (the image's CMD) redirects to the supervised + ``gateway-default`` slot. It must keep that root identity whatever ``active_profile`` says; + otherwise every container boot starts the named slot the reconciler registered down.""" + + def test_the_redirected_run_keeps_the_root_home_despite_the_active_profile( + self, tmp_path, monkeypatch + ): + monkeypatch.setattr("hermes_cli.service_manager._s6_running", lambda: True) + root = tmp_path / ".hermes" + result = _run_apply_profile_override( + tmp_path, monkeypatch, hermes_home=str(root), active_profile="coder", + argv=["hermes", "gateway", "run"], + ) + assert result == str(root) + + def test_a_foreground_run_and_other_verbs_still_follow_the_active_profile( + self, tmp_path, monkeypatch + ): + monkeypatch.setattr("hermes_cli.service_manager._s6_running", lambda: True) + root = tmp_path / ".hermes" + for argv in (["hermes", "gateway", "run", "--no-supervise"], ["hermes", "chat"]): + result = _run_apply_profile_override( + tmp_path, monkeypatch, hermes_home=str(root), active_profile="coder", argv=argv, + ) + assert result == str(root / "profiles" / "coder"), argv