From 04bd50ca74c76af2181a40aa1d0abd8584ea6c0a Mon Sep 17 00:00:00 2001 From: jenkins Date: Mon, 10 Aug 2026 23:28:35 -0300 Subject: [PATCH] hermes(agent): recognize CLI runtime and shorten restarts --- services/hermes/agent-deployment.yaml | 24 +++++++++++- .../hermes/scripts/hermes_model_routing.py | 38 +++++++++++++++++++ testing/tests/test_hermes_coordinator.py | 18 +++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml index 9a40babc9..f676f96ce 100644 --- a/services/hermes/agent-deployment.yaml +++ b/services/hermes/agent-deployment.yaml @@ -131,7 +131,29 @@ spec: upsert_env GIT_ASKPASS /opt/coordinator/gitea_askpass.sh upsert_env GIT_TERMINAL_PROMPT 0 chmod 0600 "${env_file}" - chown -R 10000:10000 /opt/data + # Existing owner data is already written as uid/gid 10000. A + # recursive chown made every routine rollout walk the full 20Gi + # workspace while the dashboard had no endpoint. Own only the + # paths this init container creates or updates. + chown 10000:10000 \ + /opt/data \ + /opt/data/home \ + /opt/data/home/.claude \ + /opt/data/home/.codex \ + /opt/data/home/.kube \ + /opt/data/cli-lanes \ + /opt/data/logs \ + /opt/data/tools \ + /opt/data/tools/bin \ + /opt/data/workspace \ + /opt/data/workspace/coordinator \ + /opt/data/workspace/projects \ + /opt/data/workspace/skills \ + /opt/data/config.yaml \ + /opt/data/SOUL.md \ + /opt/data/workspace/AGENTS.md \ + /opt/data/workspace/START-HERE.md \ + "${env_file}" securityContext: allowPrivilegeEscalation: false runAsUser: 0 diff --git a/services/hermes/scripts/hermes_model_routing.py b/services/hermes/scripts/hermes_model_routing.py index bf8f80fb0..9b56a511a 100644 --- a/services/hermes/scripts/hermes_model_routing.py +++ b/services/hermes/scripts/hermes_model_routing.py @@ -6,6 +6,8 @@ from __future__ import annotations import copy import os import re +import shutil +import subprocess from dataclasses import dataclass from pathlib import Path from typing import Any, Iterable @@ -262,6 +264,28 @@ def _existing_model(config: dict[str, Any], provider: str, default: str) -> str: return default +def codex_cli_authenticated() -> bool: + """Return whether the installed Codex CLI has a usable local login.""" + codex = shutil.which("codex") + if codex: + try: + status = subprocess.run( + [codex, "login", "status"], + capture_output=True, + check=False, + text=True, + timeout=10, + ) + detail = f"{status.stdout}\n{status.stderr}".lower() + if status.returncode == 0 and ( + "logged in" in detail or "authenticated" in detail + ): + return True + except (OSError, subprocess.SubprocessError): + pass + return False + + def discover_codex_models() -> Catalog: """Use the authenticated Codex endpoint; catalogs are status-only fallback.""" token = "" @@ -288,6 +312,20 @@ def discover_codex_models() -> Catalog: known = _unique_models(provider_model_ids("openai-codex", force_refresh=True)) except Exception: known = [] + # The owner agent deliberately uses the local Codex app-server and the + # Codex CLI's ChatGPT login. That credential is not exported into Hermes' + # bearer-token store, so API discovery above may be empty even though the + # runtime is fully authenticated. Treat a successful CLI status check as a + # connected, degraded catalog: routing may use Codex while retaining the + # last-known working model names until app-server discovery is available. + if codex_cli_authenticated(): + return Catalog( + "openai-codex", + known, + False, + True, + "connected-cli", + ) state = "degraded" if token else "not-configured" return Catalog("openai-codex", known, False, bool(token), state) diff --git a/testing/tests/test_hermes_coordinator.py b/testing/tests/test_hermes_coordinator.py index dac194790..6bf9c0886 100644 --- a/testing/tests/test_hermes_coordinator.py +++ b/testing/tests/test_hermes_coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations import importlib.util import json +import subprocess import sys from pathlib import Path @@ -64,6 +65,23 @@ def test_empty_catalog_retains_current_models(): assert routing.choose_claude_model([], "claude-opus-5") == "claude-opus-5" +def test_codex_cli_login_counts_as_connected_runtime(monkeypatch): + """AUTO routing must recognize the authenticated app-server CLI lane.""" + monkeypatch.setattr(routing.shutil, "which", lambda name: "/usr/bin/codex") + monkeypatch.setattr( + routing.subprocess, + "run", + lambda *args, **kwargs: subprocess.CompletedProcess( + args[0], + 0, + stdout="Logged in using ChatGPT\n", + stderr="", + ), + ) + + assert routing.codex_cli_authenticated() is True + + def test_configure_routes_builds_cross_provider_fallback_profiles(tmp_path: Path): (tmp_path / "config.yaml").write_text( yaml.safe_dump(_base_config()), encoding="utf-8"