From f0b08547739ad473ef33ba58de6b121a656a6a9d Mon Sep 17 00:00:00 2001 From: Charanis Date: Sun, 24 May 2026 16:52:59 +0200 Subject: [PATCH] fix: preserve webui launcher environment (cherry picked from commit 2297ab4db854b52b20cdd34731cd82e8cc5bdb72) --- bootstrap.py | 12 ++++++++++-- ctl.sh | 4 +++- tests/test_bootstrap_dotenv.py | 12 ++++++++++++ tests/test_ctl_script.py | 7 +++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/bootstrap.py b/bootstrap.py index 92d08245..a1b7ea62 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -28,8 +28,8 @@ def _load_repo_dotenv() -> None: ``python3 bootstrap.py`` directly behaves identically to ``./start.sh``. Variables are set unconditionally (matching shell source semantics), so a value in .env overrides one already present in the shell environment. - To keep a CLI-supplied value, unset it from .env or launch via start.sh - and override there. + ``ctl.sh`` sets HERMES_WEBUI_PRESERVE_ENV=1 when it has already resolved + launcher-specific values such as HERMES_HOME or HERMES_WEBUI_STATE_DIR. Only loads the webui repo .env — not ~/.hermes/.env, which the server loads independently at startup for provider credentials. @@ -41,6 +41,12 @@ def _load_repo_dotenv() -> None: if not env_path.exists(): return try: + preserve_existing = os.getenv("HERMES_WEBUI_PRESERVE_ENV", "").strip().lower() in { + "1", + "true", + "yes", + "on", + } for raw_line in env_path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: @@ -52,6 +58,8 @@ def _load_repo_dotenv() -> None: k = k[7:].strip() v = v.strip().strip('"').strip("'") if k: + if preserve_existing and k in os.environ: + continue os.environ[k] = v except Exception as exc: import sys as _sys diff --git a/ctl.sh b/ctl.sh index 132a146b..bba8c58f 100755 --- a/ctl.sh +++ b/ctl.sh @@ -219,7 +219,9 @@ start_cmd() { : >> "${LOG_FILE}" ( cd "${REPO_ROOT}" - exec "${python_exe}" "${REPO_ROOT}/bootstrap.py" --no-browser --foreground --host "${CTL_HOST}" "${CTL_PORT}" ${CTL_BOOTSTRAP_ARGS[@]+"${CTL_BOOTSTRAP_ARGS[@]}"} + trap '' HUP + export HERMES_WEBUI_PRESERVE_ENV=1 + exec nohup "${python_exe}" "${REPO_ROOT}/bootstrap.py" --no-browser --foreground --host "${CTL_HOST}" "${CTL_PORT}" ${CTL_BOOTSTRAP_ARGS[@]+"${CTL_BOOTSTRAP_ARGS[@]}"} ) >> "${LOG_FILE}" 2>&1 & pid=$! diff --git a/tests/test_bootstrap_dotenv.py b/tests/test_bootstrap_dotenv.py index da8715bd..2f354d1f 100644 --- a/tests/test_bootstrap_dotenv.py +++ b/tests/test_bootstrap_dotenv.py @@ -112,6 +112,18 @@ class TestLoadRepoDotenv: self._run(tmp_path, "HERMES_WEBUI_HOST=0.0.0.0\n") assert os.environ.get("HERMES_WEBUI_HOST") == "0.0.0.0" + def test_preserve_existing_env_keeps_ctl_overrides(self, tmp_path): + """ctl.sh can ask bootstrap.py to keep wrapper-provided env values.""" + os.environ["HERMES_WEBUI_PRESERVE_ENV"] = "1" + os.environ["HERMES_HOME"] = "/runtime/hermesOne" + os.environ["HERMES_WEBUI_PASSWORD"] = "" + self._run( + tmp_path, + "HERMES_HOME=/repo/default\nHERMES_WEBUI_PASSWORD=repo-password\n", + ) + assert os.environ.get("HERMES_HOME") == "/runtime/hermesOne" + assert os.environ.get("HERMES_WEBUI_PASSWORD") == "" + def test_does_not_set_empty_values(self, tmp_path): """A key whose value is empty after stripping is not set to a non-empty string.""" os.environ.pop("HERMES_EMPTY_KEY", None) diff --git a/tests/test_ctl_script.py b/tests/test_ctl_script.py index 95134fc9..526d5ded 100644 --- a/tests/test_ctl_script.py +++ b/tests/test_ctl_script.py @@ -138,6 +138,13 @@ def test_start_writes_pid_under_hermes_home_runs_foreground_no_browser_and_logs( assert not pid_file.exists() +def test_start_uses_nohup_so_daemon_survives_launcher_exit(): + ctl_text = CTL.read_text(encoding="utf-8") + + assert "trap '' HUP" in ctl_text + assert 'exec nohup "${python_exe}"' in ctl_text + + def test_start_loads_dotenv_but_inline_overrides_win(tmp_path): repo_root = tmp_path / "repo" repo_root.mkdir()