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 <name>` (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.
This commit is contained in:
John Paul Soliva
2026-09-23 14:23:30 +09:00
committed by Teknium
parent 1769024ca3
commit bd112bc8b3
3 changed files with 60 additions and 5 deletions

View File

@@ -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 <profile> 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 "$@"

View File

@@ -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

View File

@@ -307,3 +307,31 @@ class TestGeneralizedSupervisorMarkers:
plist = generate_launchd_plist()
assert "<key>HERMES_SUPERVISED_CHILD</key>" 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