fix(onboarding): recognize credential_pool OAuth auth for openai-codex (#797)

fix(onboarding): recognize credential_pool OAuth auth for openai-codex (#797)

The onboarding readiness check in `api/onboarding.py` only looked at the legacy
`providers[provider]` key in `auth.json`. Hermes runtime resolves OAuth tokens from
`credential_pool[provider]` (device-code / OAuth flows), so WebUI could report "not ready"
while the runtime chatted successfully. The check now covers both storage locations with
a fail-closed helper. Adds three regression tests.

Reported in #796, fixed by @davidsben.

Co-authored-by: davidsben <davidsben@users.noreply.github.com>
This commit is contained in:
Dave Brown
2026-04-21 16:41:34 +01:00
committed by GitHub
parent 3f484aec33
commit 77ab63361f
3 changed files with 161 additions and 27 deletions

View File

@@ -41,6 +41,16 @@ def make_session(created_list):
return sid
def _make_auth_json_with_credential_pool(
provider_id: str, pool_entries: list[dict], tmp_dir: pathlib.Path
) -> pathlib.Path:
"""Write an auth.json with only credential_pool entries for provider_id."""
store = {"providers": {}, "credential_pool": {provider_id: pool_entries}}
auth_path = tmp_dir / "auth.json"
auth_path.write_text(json.dumps(store), encoding="utf-8")
return auth_path
# ── R1: uuid not imported in server.py (Sprint 10 split regression) ──────────
def test_chat_start_returns_stream_id(cleanup_test_sessions):
@@ -764,3 +774,100 @@ def test_reload_recovery_persists_durable_inflight_state(cleanup_test_sessions):
"messages.js must clear durable inflight snapshots when the run ends/errors/cancels"
assert "const stored=loadInflightState(sid, activeStreamId);" in sessions_src, \
"loadSession() must hydrate in-flight state from durable browser storage on reload"
# ── R18: OAuth onboarding must recognize credential_pool-only auth ───────────
def test_provider_oauth_authenticated_accepts_credential_pool_entries(
cleanup_test_sessions, tmp_path
):
"""R18a: pool-only OAuth auth.json should count as authenticated.
Hermes runtime resolves Codex credentials from credential_pool; onboarding
must not insist on stale or duplicated providers[provider_id] entries.
"""
_make_auth_json_with_credential_pool(
"openai-codex",
[
{
"id": "pool1",
"label": "device_code",
"source": "device_code",
"auth_type": "oauth",
"access_token": "***",
"refresh_token": "***",
"base_url": "https://chatgpt.com/backend-api/codex",
}
],
tmp_path,
)
from api.onboarding import _provider_oauth_authenticated
assert _provider_oauth_authenticated("openai-codex", tmp_path) is True
def test_provider_oauth_authenticated_rejects_flag_only_credential_pool_entries(
cleanup_test_sessions, tmp_path
):
"""R18a2: metadata flags alone must not count as usable OAuth auth."""
_make_auth_json_with_credential_pool(
"openai-codex",
[
{
"id": "pool1",
"label": "device_code",
"source": "device_code",
"auth_type": "oauth",
"has_access_token": True,
"has_refresh_token": True,
"base_url": "https://chatgpt.com/backend-api/codex",
}
],
tmp_path,
)
from api.onboarding import _provider_oauth_authenticated
assert _provider_oauth_authenticated("openai-codex", tmp_path) is False
def test_status_from_runtime_marks_openai_codex_ready_from_credential_pool(
cleanup_test_sessions, tmp_path
):
"""R18b: provider_ready should be true when auth lives only in credential_pool."""
_make_auth_json_with_credential_pool(
"openai-codex",
[
{
"id": "pool1",
"label": "device_code",
"source": "device_code",
"auth_type": "oauth",
"access_token": "***",
"refresh_token": "***",
"base_url": "https://chatgpt.com/backend-api/codex",
}
],
tmp_path,
)
from api.onboarding import _status_from_runtime
import api.onboarding as _ob
orig_home = _ob._get_active_hermes_home
orig_found = _ob._HERMES_FOUND
_ob._get_active_hermes_home = lambda: tmp_path
_ob._HERMES_FOUND = True
try:
result = _status_from_runtime(
{"model": {"provider": "openai-codex", "default": "codex-mini-latest"}},
True,
)
finally:
_ob._get_active_hermes_home = orig_home
_ob._HERMES_FOUND = orig_found
assert result["provider_configured"] is True
assert result["provider_ready"] is True
assert result["setup_state"] == "ready"

View File

@@ -35,6 +35,20 @@ def _make_auth_json(provider_id: str, tokens: dict, tmp_dir: pathlib.Path) -> pa
return auth_path
def _make_auth_json_with_credential_pool(
provider_id: str, pool_entries: list[dict], tmp_dir: pathlib.Path
) -> pathlib.Path:
"""Write an auth.json with only credential_pool entries for provider_id.
This reproduces setups where Hermes runtime resolves OAuth credentials from
credential_pool while providers[provider_id] is absent or stale.
"""
store = {"providers": {}, "credential_pool": {provider_id: pool_entries}}
auth_path = tmp_dir / "auth.json"
auth_path.write_text(json.dumps(store), encoding="utf-8")
return auth_path
# ── 13. _provider_oauth_authenticated unit tests ────────────────────────────
class TestProviderOAuthAuthenticated:
@@ -62,7 +76,7 @@ class TestProviderOAuthAuthenticated:
"""openai-codex with only a refresh_token -> still authenticated."""
_make_auth_json(
"openai-codex",
{"access_token": "", "refresh_token": "ref_only_token"},
{"access_token": "", "refresh_token": "***"},
tmp_path,
)
assert self._call("openai-codex", tmp_path) is True
@@ -141,7 +155,7 @@ class TestStatusFromRuntimeOAuth:
"""openai-codex configured + access_token -> provider_ready True."""
_make_auth_json(
"openai-codex",
{"access_token": "ey.test", "refresh_token": "ref"},
{"access_token": "***", "refresh_token": "***"},
tmp_path,
)
result = self._call("openai-codex", "codex-mini-latest", tmp_path)