Files
hermes-agent/tests/plugins/model_providers/test_solstice_profile.py
Teknium 22bc7eac71 refactor(solstice): rebuild the provider as a standard model-provider plugin
The whole provider now lives in plugins/model-providers/solstice/ on the
generic seams:
- auth.py: NAS config discovery plus the brokered exchange/refresh, given
  to the generic PKCE flow as token_request. A refresh that NAS rejects
  (it folds every Google failure into 502) is treated as a dead grant
  needing re-login.
- transport.py: a GeminiNativeClient subclass on the
  generateContentPerUserQuota methods with Bearer auth (~30 lines, where
  the PR duplicated a 257-line adapter).
- __init__.py: a hidden profile (create_client, fallback models,
  default_aux_model).

Removed from core: hermes_cli/solstice.py, agent/solstice_adapter.py, and
the provider's branches in main.py, auth_commands.py, models.py,
runtime_provider.py, model_setup_flows.py and agent_runtime_helpers.py.
The unused SOLSTICE_CLIENT_ID env read is gone too.
2026-10-05 23:33:41 -07:00

101 lines
5.2 KiB
Python

"""Solstice invariants: a pre-release provider stays off every discovery surface until signed in, and a
brokered OAuth row with an opaque bearer rotates on its stored expiry."""
from __future__ import annotations
import json
import time
import hermes_cli.auth
import providers
from hermes_cli import auth_oauth_pkce_plugin as pkce
from providers import register_provider
from providers.base import ProviderProfile
def _pool_row(provider: str, *, access: str, expires_at_ms: int) -> dict:
return {"id": "abc123", "label": provider, "auth_type": "oauth", "priority": 0, "source": pkce.POOL_SOURCE,
"access_token": access, "refresh_token": "1//rt-0", "expires_at_ms": expires_at_ms}
def _write_pool(home, provider: str, row: dict) -> None:
(home / "auth.json").write_text(json.dumps({"version": 1, "credential_pool": {provider: [row]}}))
def test_solstice_is_off_every_discovery_surface_until_signed_in_but_always_resolves(monkeypatch, tmp_path):
home = tmp_path / "hermes"
home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(home))
from hermes_cli.models import list_available_providers
from hermes_cli.models_catalog_static import listed_canonical_providers
from hermes_cli.provider_catalog import provider_catalog
from hermes_cli.web_routers.oauth import _build_oauth_catalog
def surfaces() -> dict[str, bool]:
return {
"canonical": any(p.slug == "solstice" for p in listed_canonical_providers()),
"provider_list": any(r["id"] == "solstice" for r in list_available_providers()),
"setup_catalog": any(d.slug == "solstice" for d in provider_catalog()),
"accounts_tab": any(r["id"] == "solstice" for r in _build_oauth_catalog()),
}
assert providers.get_provider_profile("solstice").hidden is True
assert surfaces() == dict.fromkeys(surfaces(), False)
# Typed paths never consult the gate: a user who names it can sign in and run it.
assert hermes_cli.auth.resolve_provider("solstice") == "solstice"
assert hermes_cli.auth.resolve_provider("solstice-oauth") == "solstice"
_write_pool(home, "solstice", _pool_row("solstice", access="ya29.x", expires_at_ms=int(time.time() * 1000) + 3_600_000))
assert surfaces() == dict.fromkeys(surfaces(), True)
def test_brokered_opaque_bearer_rotates_on_stored_expiry_and_keeps_the_refresh_token(monkeypatch, tmp_path, request):
"""Google access tokens are opaque (no JWT ``exp``): the pool must rotate on ``expires_at_ms``, through the
plugin's broker instead of a token URL, and keep the refresh token when the response omits it."""
from agent.credential_pool import load_pool
home = tmp_path / "hermes"
home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(home))
grants: list[dict] = []
def broker(grant: dict) -> dict:
grants.append(grant)
return {"access_token": f"ya29.fresh-{len(grants)}", "expires_in": 3600}
name = "example-brokered"
cfg = pkce.OAuthPKCEConfig(client_id="", authorize_url="", token_url="", token_request=broker)
register_provider(ProviderProfile(name=name, auth_type="oauth_external", base_url="https://example.invalid/v1",
auth_handler=pkce.pkce_auth_handler(cfg), refresh_credential=pkce.pkce_refresh_credential(cfg)))
request.addfinalizer(lambda: (providers._REGISTRY.pop(name, None), hermes_cli.auth.PROVIDER_REGISTRY.pop(name, None)))
now_ms = int(time.time() * 1000)
_write_pool(home, name, _pool_row(name, access="ya29.live", expires_at_ms=now_ms + 3_600_000))
assert load_pool(name).select().access_token == "ya29.live" and grants == [] # fresh: no broker call
_write_pool(home, name, _pool_row(name, access="ya29.stale", expires_at_ms=now_ms - 1000))
leased = load_pool(name).select()
assert leased.access_token == "ya29.fresh-1"
assert grants == [{"grant_type": "refresh_token", "refresh_token": "1//rt-0"}]
disk = json.loads((home / "auth.json").read_text())["credential_pool"][name][0]
assert (disk["access_token"], disk["refresh_token"]) == ("ya29.fresh-1", "1//rt-0")
assert disk["expires_at_ms"] > now_ms + 3_000_000
# A grant the broker rejects is DEAD and stays DEAD through the inference 401 that follows (never re-benched
# as EXHAUSTED, which would replay the dead token every cooldown).
from agent.credential_pool import STATUS_DEAD
from hermes_cli.auth_constants import AuthError
def dead_broker(grant: dict) -> dict:
raise AuthError("rejected", provider=name, code="invalid_grant", relogin_required=True)
cfg_dead = pkce.OAuthPKCEConfig(client_id="", authorize_url="", token_url="", token_request=dead_broker)
providers._REGISTRY[name] = ProviderProfile(name=name, auth_type="oauth_external", base_url="https://example.invalid/v1",
refresh_credential=pkce.pkce_refresh_credential(cfg_dead))
_write_pool(home, name, _pool_row(name, access="ya29.revoked", expires_at_ms=now_ms + 3_600_000))
pool = load_pool(name)
pool.select()
assert pool.try_refresh_current() is None
pool.mark_exhausted_and_rotate(status_code=401, api_key_hint="ya29.revoked")
assert json.loads((home / "auth.json").read_text())["credential_pool"][name][0]["last_status"] == STATUS_DEAD