diff --git a/services/hermes/scripts/claude_oauth_broker.py b/services/hermes/scripts/claude_oauth_broker.py index 2718f97f..f0eeca4c 100644 --- a/services/hermes/scripts/claude_oauth_broker.py +++ b/services/hermes/scripts/claude_oauth_broker.py @@ -67,11 +67,6 @@ _auth_probe_value: dict[str, Any] = {} HEALTH_POLL_SECONDS: Final = max( 30, int(os.environ.get("HERMES_CLAUDE_HEALTH_POLL_SECONDS", "60")) ) -AUTH_PROBE_GRACE_SECONDS: Final = max( - 60, int(os.environ.get("HERMES_CLAUDE_AUTH_PROBE_GRACE_SECONDS", "300")) -) - - def _read_secret(file_env_name: str) -> str: """Read a secret only from its runtime-mounted file.""" path = os.environ.get(file_env_name, "").strip() @@ -296,32 +291,28 @@ def _subscription_health(force: bool = False) -> dict[str, Any]: ) api_provider = raw.get("apiProvider") if isinstance(raw, dict) else None auth_method = raw.get("authMethod") if isinstance(raw, dict) else None - # A concurrent inference can make Claude's secondary auth-status - # process time out or return no JSON. A real, recent inference is - # stronger evidence than that inconclusive probe. Preserve the last - # proven-good auth state briefly, but never mask an explicit logged-out - # response from the CLI. - recent_success = False - try: - last_success = datetime.fromisoformat( - str(previous.get("last_success_at") or "").replace("Z", "+00:00") + # Cold starts and concurrent inference can make Claude's secondary + # auth-status process time out or return no JSON. Once this broker has + # proven first-party access, only an explicit logged-out response or an + # authoritative inference auth failure may revoke that evidence. + # Otherwise periodic probe timeouts would repeatedly eject a working + # subscription from routing. + known_authenticated = bool( + ( + previous.get("authenticated") is True + and previous.get("api_provider") == "firstParty" ) - if last_success.tzinfo is None: - last_success = last_success.replace(tzinfo=timezone.utc) - age = (checked_time - last_success.astimezone(timezone.utc)).total_seconds() - recent_success = 0 <= age <= AUTH_PROBE_GRACE_SECONDS - except (TypeError, ValueError): - pass + or previous.get("last_authenticated_at") + or previous.get("last_success_at") + ) probe_stale = bool( not probe_conclusive - and recent_success - and previous.get("authenticated") is True - and previous.get("api_provider") == "firstParty" + and known_authenticated ) if probe_stale: authenticated = True - api_provider = previous.get("api_provider") - auth_method = previous.get("auth_method") + api_provider = previous.get("api_provider") or "firstParty" + auth_method = previous.get("auth_method") or "oauth_token" value = { "transport": "claude-code-cli-subscription", "state": "available" if authenticated else "unavailable", @@ -332,8 +323,16 @@ def _subscription_health(force: bool = False) -> dict[str, Any]: "auth_probe_stale": probe_stale, "subscription_type": raw.get("subscriptionType") if isinstance(raw, dict) - else None, + else previous.get("subscription_type"), } + if authenticated: + value["last_authenticated_at"] = ( + checked_at + if probe_conclusive + else previous.get("last_authenticated_at") + or previous.get("last_success_at") + or checked_at + ) # Readiness probes must not erase the most recently observed native # usage window or successful route metadata. They only refresh auth. for key in ( @@ -447,6 +446,11 @@ def _invoke(payload: dict[str, Any]) -> tuple[dict[str, Any], dict[str, int], st health.update( { "state": "available", + "authenticated": True, + "api_provider": "firstParty", + "auth_method": "oauth_token", + "auth_probe_stale": False, + "last_authenticated_at": datetime.now(timezone.utc).isoformat(), "last_success_at": datetime.now(timezone.utc).isoformat(), "latency_ms": int((time.monotonic() - started) * 1000), "model": actual_model, diff --git a/testing/tests/test_hermes_claude_broker.py b/testing/tests/test_hermes_claude_broker.py index 1f9fb7a9..5e29a105 100644 --- a/testing/tests/test_hermes_claude_broker.py +++ b/testing/tests/test_hermes_claude_broker.py @@ -154,6 +154,36 @@ def test_transient_auth_probe_keeps_recent_proven_access(monkeypatch, tmp_path) assert health["auth_probe_stale"] is True +def test_transient_auth_probe_keeps_older_proven_access(monkeypatch, tmp_path) -> None: + module = _module(monkeypatch) + module.HEALTH_PATH = tmp_path / "claude.json" + module.HEALTH_PATH.write_text( + json.dumps( + { + "authenticated": False, + "last_success_at": "2026-01-01T00:00:00+00:00", + } + ), + encoding="utf-8", + ) + monkeypatch.setattr( + module.subprocess, + "run", + lambda *_args, **_kwargs: SimpleNamespace( + returncode=1, stdout="", stderr="transient timeout" + ), + ) + + health = module._subscription_health(force=True) + + assert health["authenticated"] is True + assert health["state"] == "available" + assert health["api_provider"] == "firstParty" + assert health["auth_method"] == "oauth_token" + assert health["auth_probe_stale"] is True + assert health["last_authenticated_at"] == "2026-01-01T00:00:00+00:00" + + def test_explicit_logged_out_probe_clears_recent_access(monkeypatch, tmp_path) -> None: module = _module(monkeypatch) module.HEALTH_PATH = tmp_path / "claude.json" @@ -183,3 +213,4 @@ def test_explicit_logged_out_probe_clears_recent_access(monkeypatch, tmp_path) - assert health["authenticated"] is False assert health["state"] == "unavailable" assert health["auth_probe_stale"] is False + assert "last_authenticated_at" not in health