diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index e38ab1cb613..007e598102e 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -1224,15 +1224,24 @@ def _default_hermes_root_is_opt_data() -> bool: return root == _HOSTED_MANAGED_FILES_ROOT -def _dashboard_hosted_agent_mode() -> bool: - """Return true for the hosted/container dashboard layout. +def _dashboard_local_update_managed_externally() -> bool: + """Return true when the dashboard should not offer ``hermes update``. - Hosted agent dashboards run with the Hermes root at ``/opt/data``. This is - the same signal the Files page uses to lock browsing to the managed data - directory, and it keeps local remote-auth dashboards from being mistaken for - hosted service instances. + Hosted agent dashboards run with the Hermes root at ``/opt/data``. Generic + containerized dashboards may not use that exact root, but their lifecycle is + still owned by the outer launcher/image, not by an in-browser local update + action. Keep this dashboard capability separate from install-method + detection: manual git/pip installs inside containers can still behave like + their actual install method in the CLI. """ - return _default_hermes_root_is_opt_data() + if _default_hermes_root_is_opt_data(): + return True + try: + from hermes_constants import is_container + + return is_container() + except Exception: + return False def _managed_files_policy(request: Request, *, create_root: bool = True) -> ManagedFilesPolicy: @@ -1665,7 +1674,7 @@ async def get_status(): "release_date": __release_date__, "config_version": current_ver, "latest_config_version": latest_ver, - "can_update_hermes": not _dashboard_hosted_agent_mode(), + "can_update_hermes": not _dashboard_local_update_managed_externally(), "gateway_running": gateway_running, "gateway_state": gateway_state, "gateway_platforms": gateway_platforms, @@ -2177,19 +2186,20 @@ async def restart_gateway(): @app.post("/api/hermes/update") async def update_hermes(): """Kick off ``hermes update`` in the background.""" - if _dashboard_hosted_agent_mode(): + if _dashboard_local_update_managed_externally(): message = ( - "Hermes updates are managed by the hosted agent service for this " - "dashboard. The built-in local updater is disabled here." + "Hermes updates are managed outside this dashboard for hosted or " + "containerized environments. The built-in local updater is " + "disabled here." ) _record_completed_action("hermes-update", message, exit_code=1) return { "ok": False, "pid": None, "name": "hermes-update", - "error": "hosted_update_managed", + "error": "dashboard_update_managed_externally", "message": message, - "update_command": "managed by hosted agent service", + "update_command": "managed outside dashboard", } install_method = detect_install_method(PROJECT_ROOT) @@ -2291,15 +2301,18 @@ async def check_hermes_update(force: bool = False): desktop's remote update overlay renders this as "what's changed". Additive: existing consumers ignore it. """ - if _dashboard_hosted_agent_mode(): + if _dashboard_local_update_managed_externally(): return { - "install_method": "hosted", + "install_method": "managed-runtime", "current_version": __version__, "behind": None, "update_available": False, "can_apply": False, - "update_command": "managed by hosted agent service", - "message": "Hermes updates are managed by the hosted agent service.", + "update_command": "managed outside dashboard", + "message": ( + "Hermes updates are managed outside this dashboard for hosted " + "or containerized environments." + ), } install_method = detect_install_method(PROJECT_ROOT) diff --git a/tests/hermes_cli/test_dashboard_admin_endpoints.py b/tests/hermes_cli/test_dashboard_admin_endpoints.py index 933615e8974..b87489e7f39 100644 --- a/tests/hermes_cli/test_dashboard_admin_endpoints.py +++ b/tests/hermes_cli/test_dashboard_admin_endpoints.py @@ -775,7 +775,7 @@ class TestUpdateCheckEndpoint: def test_hosted_dashboard_is_not_applyable(self, monkeypatch): import hermes_cli.web_server as ws - monkeypatch.setattr(ws, "_dashboard_hosted_agent_mode", lambda: True) + monkeypatch.setattr(ws, "_dashboard_local_update_managed_externally", lambda: True) monkeypatch.setattr( ws, "detect_install_method", @@ -785,11 +785,11 @@ class TestUpdateCheckEndpoint: ) body = self.client.get("/api/hermes/update/check").json() - assert body["install_method"] == "hosted" + assert body["install_method"] == "managed-runtime" assert body["can_apply"] is False assert body["update_available"] is False assert body["behind"] is None - assert "hosted agent service" in body["message"] + assert "managed outside this dashboard" in body["message"] def test_check_failure_is_soft(self, monkeypatch): import hermes_cli.web_server as ws diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 1ad0277dbe8..2bc3138d4f8 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -250,12 +250,21 @@ class TestWebServerEndpoints: def test_get_status_hides_update_capability_in_hosted_mode(self, monkeypatch): import hermes_cli.web_server as web_server - monkeypatch.setattr(web_server, "_dashboard_hosted_agent_mode", lambda: True) + monkeypatch.setattr(web_server, "_dashboard_local_update_managed_externally", lambda: True) resp = self.client.get("/api/status") assert resp.status_code == 200 assert resp.json()["can_update_hermes"] is False + def test_dashboard_update_capability_detects_generic_container(self, monkeypatch): + import hermes_constants + import hermes_cli.web_server as web_server + + monkeypatch.setattr(web_server, "_default_hermes_root_is_opt_data", lambda: False) + monkeypatch.setattr(hermes_constants, "is_container", lambda: True) + + assert web_server._dashboard_local_update_managed_externally() is True + # ── GET /api/media (remote image display) ─────────────────────────── def test_get_media_serves_image_in_root(self): @@ -938,7 +947,7 @@ class TestWebServerEndpoints: detected = True raise AssertionError("hosted update guard should not detect install method") - monkeypatch.setattr(web_server, "_dashboard_hosted_agent_mode", lambda: True) + monkeypatch.setattr(web_server, "_dashboard_local_update_managed_externally", lambda: True) monkeypatch.setattr(web_server, "detect_install_method", fail_detect) monkeypatch.setattr(web_server, "_spawn_hermes_action", fail_spawn) web_server._ACTION_PROCS.pop("hermes-update", None) @@ -951,8 +960,8 @@ class TestWebServerEndpoints: assert data["ok"] is False assert data["name"] == "hermes-update" assert data["pid"] is None - assert data["error"] == "hosted_update_managed" - assert "hosted agent service" in data["message"] + assert data["error"] == "dashboard_update_managed_externally" + assert "managed outside this dashboard" in data["message"] assert spawned is False assert detected is False @@ -962,7 +971,7 @@ class TestWebServerEndpoints: assert status_data["running"] is False assert status_data["exit_code"] == 1 assert status_data["pid"] is None - assert any("hosted agent service" in line for line in status_data["lines"]) + assert any("managed outside this dashboard" in line for line in status_data["lines"]) def test_update_hermes_spawns_on_non_docker_install(self, monkeypatch): import hermes_cli.web_server as web_server diff --git a/web/src/pages/SystemPage.tsx b/web/src/pages/SystemPage.tsx index f22bb553218..24cb68894b4 100644 --- a/web/src/pages/SystemPage.tsx +++ b/web/src/pages/SystemPage.tsx @@ -420,7 +420,7 @@ export default function SystemPage() { setUpdateConfirmOpen(false); if (status?.can_update_hermes === false) { showToast( - "Hermes updates are managed by the hosted agent service.", + "Hermes updates are managed outside this dashboard.", "success", ); return;