fix(hermes): preserve proven Claude authentication
This commit is contained in:
parent
0682b2b813
commit
6b378fd410
@ -67,11 +67,6 @@ _auth_probe_value: dict[str, Any] = {}
|
|||||||
HEALTH_POLL_SECONDS: Final = max(
|
HEALTH_POLL_SECONDS: Final = max(
|
||||||
30, int(os.environ.get("HERMES_CLAUDE_HEALTH_POLL_SECONDS", "60"))
|
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:
|
def _read_secret(file_env_name: str) -> str:
|
||||||
"""Read a secret only from its runtime-mounted file."""
|
"""Read a secret only from its runtime-mounted file."""
|
||||||
path = os.environ.get(file_env_name, "").strip()
|
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
|
api_provider = raw.get("apiProvider") if isinstance(raw, dict) else None
|
||||||
auth_method = raw.get("authMethod") 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
|
# Cold starts and concurrent inference can make Claude's secondary
|
||||||
# process time out or return no JSON. A real, recent inference is
|
# auth-status process time out or return no JSON. Once this broker has
|
||||||
# stronger evidence than that inconclusive probe. Preserve the last
|
# proven first-party access, only an explicit logged-out response or an
|
||||||
# proven-good auth state briefly, but never mask an explicit logged-out
|
# authoritative inference auth failure may revoke that evidence.
|
||||||
# response from the CLI.
|
# Otherwise periodic probe timeouts would repeatedly eject a working
|
||||||
recent_success = False
|
# subscription from routing.
|
||||||
try:
|
known_authenticated = bool(
|
||||||
last_success = datetime.fromisoformat(
|
(
|
||||||
str(previous.get("last_success_at") or "").replace("Z", "+00:00")
|
previous.get("authenticated") is True
|
||||||
|
and previous.get("api_provider") == "firstParty"
|
||||||
)
|
)
|
||||||
if last_success.tzinfo is None:
|
or previous.get("last_authenticated_at")
|
||||||
last_success = last_success.replace(tzinfo=timezone.utc)
|
or previous.get("last_success_at")
|
||||||
age = (checked_time - last_success.astimezone(timezone.utc)).total_seconds()
|
)
|
||||||
recent_success = 0 <= age <= AUTH_PROBE_GRACE_SECONDS
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
probe_stale = bool(
|
probe_stale = bool(
|
||||||
not probe_conclusive
|
not probe_conclusive
|
||||||
and recent_success
|
and known_authenticated
|
||||||
and previous.get("authenticated") is True
|
|
||||||
and previous.get("api_provider") == "firstParty"
|
|
||||||
)
|
)
|
||||||
if probe_stale:
|
if probe_stale:
|
||||||
authenticated = True
|
authenticated = True
|
||||||
api_provider = previous.get("api_provider")
|
api_provider = previous.get("api_provider") or "firstParty"
|
||||||
auth_method = previous.get("auth_method")
|
auth_method = previous.get("auth_method") or "oauth_token"
|
||||||
value = {
|
value = {
|
||||||
"transport": "claude-code-cli-subscription",
|
"transport": "claude-code-cli-subscription",
|
||||||
"state": "available" if authenticated else "unavailable",
|
"state": "available" if authenticated else "unavailable",
|
||||||
@ -332,8 +323,16 @@ def _subscription_health(force: bool = False) -> dict[str, Any]:
|
|||||||
"auth_probe_stale": probe_stale,
|
"auth_probe_stale": probe_stale,
|
||||||
"subscription_type": raw.get("subscriptionType")
|
"subscription_type": raw.get("subscriptionType")
|
||||||
if isinstance(raw, dict)
|
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
|
# Readiness probes must not erase the most recently observed native
|
||||||
# usage window or successful route metadata. They only refresh auth.
|
# usage window or successful route metadata. They only refresh auth.
|
||||||
for key in (
|
for key in (
|
||||||
@ -447,6 +446,11 @@ def _invoke(payload: dict[str, Any]) -> tuple[dict[str, Any], dict[str, int], st
|
|||||||
health.update(
|
health.update(
|
||||||
{
|
{
|
||||||
"state": "available",
|
"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(),
|
"last_success_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"latency_ms": int((time.monotonic() - started) * 1000),
|
"latency_ms": int((time.monotonic() - started) * 1000),
|
||||||
"model": actual_model,
|
"model": actual_model,
|
||||||
|
|||||||
@ -154,6 +154,36 @@ def test_transient_auth_probe_keeps_recent_proven_access(monkeypatch, tmp_path)
|
|||||||
assert health["auth_probe_stale"] is True
|
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:
|
def test_explicit_logged_out_probe_clears_recent_access(monkeypatch, tmp_path) -> None:
|
||||||
module = _module(monkeypatch)
|
module = _module(monkeypatch)
|
||||||
module.HEALTH_PATH = tmp_path / "claude.json"
|
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["authenticated"] is False
|
||||||
assert health["state"] == "unavailable"
|
assert health["state"] == "unavailable"
|
||||||
assert health["auth_probe_stale"] is False
|
assert health["auth_probe_stale"] is False
|
||||||
|
assert "last_authenticated_at" not in health
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user