Hide OAuth dummy API keys until connected

Remove the static OAuth API-key placeholder from provider defaults and gate the runtime dummy key on provider connection status. This keeps unconnected OAuth providers blank in API-key surfaces while preserving the local proxy shim for connected account-backed providers. Update OAuth docs and regressions for the new contract.

Alessandro committed Jun 4, 2026 at 11:31 UTC ca4c9306c130c949ca9aad9fe7736b0216e78f29
6 files changed +58 -23
plugins/_oauth/AGENTS.md
+1
@@ -27,6 +27,7 @@
27 - OAuth account surfaces in settings, discovery, and onboarding must use the provider registry/status summary rather than Codex-only frontend state.
28 - OAuth settings model slots must keep provider choice editable per slot, list only connected OAuth account providers, and persist the selected provider IDs into `chat_model.provider` and `utility_model.provider`.
29 - `helpers/providers/registry.py` is the source of truth for connectable OAuth providers.
30 +- OAuth provider config must not expose the dummy `oauth` API key in `conf/model_providers.yaml`; the dummy key is a runtime-only shim supplied by the `get_api_key` extension after the account provider reports connected.
31 - Usage-plan metadata belongs only to connectable providers. Do not add metadata-only subscription families for providers this plugin cannot connect.
32 - API handlers should remain provider-aware. Missing or blank `provider_id` defaults to Codex only for existing backward compatibility; falsey non-string IDs must not silently default.
33 - Codex success contracts must preserve legacy fields such as `account_id` while allowing newer fields such as `account_label`.
plugins/_oauth/README.md
+2
@@ -8,6 +8,8 @@ The settings UI groups providers as account-backed connections. More than one ac
8
9 Each model slot has its own provider selector. The selector lists connected OAuth accounts only, so Main and Utility can use different account-backed providers when more than one account is connected.
10
11 +OAuth-backed model providers do not require users to enter API keys. Agent Zero supplies a local dummy key only at runtime after the selected account provider is connected, so unconnected providers stay blank in API-key surfaces.
12 +
13 ## Providers
14
15 ### Codex/ChatGPT (`codex_oauth`)
plugins/_oauth/conf/model_providers.yaml
-4
@@ -7,7 +7,6 @@ chat:
7 endpoint_url: "/models"
8 kwargs:
9 api_base: "http://127.0.0.1/oauth/codex/v1"
10 - api_key: "oauth"
10 github_copilot_oauth:
11 name: GitHub Copilot Account
12 litellm_provider: openai
@@ -16,7 +15,6 @@ chat:
15 endpoint_url: "/models"
16 kwargs:
17 api_base: "http://127.0.0.1/oauth/github-copilot/v1"
19 - api_key: "oauth"
18 gemini_api_oauth:
19 name: Google Gemini API Account
20 litellm_provider: openai
@@ -25,7 +23,6 @@ chat:
23 endpoint_url: "/models"
24 kwargs:
25 api_base: "http://127.0.0.1/oauth/gemini-api/v1"
28 - api_key: "oauth"
26 xai_grok_oauth:
27 name: xAI Grok Account
28 litellm_provider: openai
@@ -34,4 +31,3 @@ chat:
31 endpoint_url: "/models"
32 kwargs:
33 api_base: "http://127.0.0.1/oauth/xai-grok/v1"
37 - api_key: "oauth"
plugins/_oauth/extensions/python/_functions/models/get_api_key/end/_20_oauth_account_dummy_key.py
+12 -2
@@ -1,7 +1,7 @@
1 from __future__ import annotations
2
3 from helpers.extension import Extension
4 -from plugins._oauth.helpers.providers import DUMMY_API_KEY, oauth_provider_ids
4 +from plugins._oauth.helpers.providers import DUMMY_API_KEY, get_provider, oauth_provider_ids
5
6
7 class OAuthAccountDummyKey(Extension):
@@ -22,5 +22,15 @@ class OAuthAccountDummyKey(Extension):
22 return
23
24 result = str(data.get("result") or "").strip()
25 - if not result or result == "None":
25 + if (not result or result == "None") and oauth_provider_is_connected(service):
26 data["result"] = DUMMY_API_KEY
27 +
28 +
29 +def oauth_provider_is_connected(provider_id: str) -> bool:
30 + try:
31 + status = get_provider(provider_id).status()
32 + except Exception:
33 + return False
34 + if not isinstance(status, dict):
35 + return False
36 + return bool(status.get("connected"))
tests/test_oauth_codex.py
+15 -5
@@ -15,8 +15,8 @@ import yaml
15 sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
16 from plugins._oauth.helpers import codex
17 from plugins._oauth.helpers import routes
18 -from plugins._oauth.extensions.python._functions.models.get_api_key.end._20_oauth_account_dummy_key import (
19 - OAuthAccountDummyKey,
18 +from plugins._oauth.extensions.python._functions.models.get_api_key.end import (
19 + _20_oauth_account_dummy_key as oauth_dummy_key,
20 )
21
22
@@ -686,10 +686,20 @@ def test_provider_config_uses_container_local_agent_zero_origin():
686 assert "50001" not in json.dumps(codex_provider)
687
688
689 -def test_codex_provider_reports_dummy_api_key_when_missing():
689 +def test_codex_provider_leaves_api_key_empty_until_connected(monkeypatch):
690 + monkeypatch.setattr(oauth_dummy_key, "oauth_provider_is_connected", lambda _provider_id: False)
691 data = {"args": ("codex_oauth",), "kwargs": {}, "result": "None"}
692
692 - OAuthAccountDummyKey(agent=None).execute(data=data)
693 + oauth_dummy_key.OAuthAccountDummyKey(agent=None).execute(data=data)
694 +
695 + assert data["result"] == "None"
696 +
697 +
698 +def test_codex_provider_reports_dummy_api_key_when_connected(monkeypatch):
699 + monkeypatch.setattr(oauth_dummy_key, "oauth_provider_is_connected", lambda _provider_id: True)
700 + data = {"args": ("codex_oauth",), "kwargs": {}, "result": "None"}
701 +
702 + oauth_dummy_key.OAuthAccountDummyKey(agent=None).execute(data=data)
703
704 assert data["result"] == "oauth"
705
@@ -697,6 +707,6 @@ def test_codex_provider_reports_dummy_api_key_when_missing():
707 def test_codex_provider_preserves_configured_api_key():
708 data = {"args": ("codex_oauth",), "kwargs": {}, "result": "configured"}
709
700 - OAuthAccountDummyKey(agent=None).execute(data=data)
710 + oauth_dummy_key.OAuthAccountDummyKey(agent=None).execute(data=data)
711
712 assert data["result"] == "configured"
tests/test_oauth_providers.py
+28 -12
@@ -55,8 +55,8 @@ from plugins._oauth.api import poll_device_login as poll_device_login_api
55 from plugins._oauth.api import start_device_login as start_device_login_api
56 from plugins._oauth.api import start_login as start_login_api
57 from plugins._oauth.api.models import Models
58 -from plugins._oauth.extensions.python._functions.models.get_api_key.end._20_oauth_account_dummy_key import (
59 - OAuthAccountDummyKey,
58 +from plugins._oauth.extensions.python._functions.models.get_api_key.end import (
59 + _20_oauth_account_dummy_key as oauth_dummy_key,
60 )
61 from plugins._oauth.helpers import state
62 from plugins._oauth.helpers.providers import base as provider_base
@@ -665,10 +665,25 @@ def test_poll_device_login_unknown_provider_returns_structured_error():
665 [CODEX_PROVIDER_ID, GITHUB_COPILOT_PROVIDER_ID, GEMINI_API_PROVIDER_ID, XAI_GROK_PROVIDER_ID],
666 )
667 @pytest.mark.parametrize("initial", [None, "None"])
668 -def test_oauth_providers_report_dummy_api_key_when_missing(provider_id, initial):
668 +def test_oauth_providers_leave_api_key_empty_until_connected(monkeypatch, provider_id, initial):
669 + monkeypatch.setattr(oauth_dummy_key, "oauth_provider_is_connected", lambda _provider_id: False)
670 data = {"args": (provider_id,), "kwargs": {}, "result": initial}
671
671 - OAuthAccountDummyKey(agent=None).execute(data=data)
672 + oauth_dummy_key.OAuthAccountDummyKey(agent=None).execute(data=data)
673 +
674 + assert data["result"] == initial
675 +
676 +
677 +@pytest.mark.parametrize(
678 + "provider_id",
679 + [CODEX_PROVIDER_ID, GITHUB_COPILOT_PROVIDER_ID, GEMINI_API_PROVIDER_ID, XAI_GROK_PROVIDER_ID],
680 +)
681 +@pytest.mark.parametrize("initial", [None, "None"])
682 +def test_oauth_providers_report_dummy_api_key_when_connected(monkeypatch, provider_id, initial):
683 + monkeypatch.setattr(oauth_dummy_key, "oauth_provider_is_connected", lambda _provider_id: True)
684 + data = {"args": (provider_id,), "kwargs": {}, "result": initial}
685 +
686 + oauth_dummy_key.OAuthAccountDummyKey(agent=None).execute(data=data)
687
688 assert data["result"] == DUMMY_API_KEY
689
@@ -677,12 +692,13 @@ def test_oauth_providers_report_dummy_api_key_when_missing(provider_id, initial)
692 "provider_id",
693 [CODEX_PROVIDER_ID, GITHUB_COPILOT_PROVIDER_ID, GEMINI_API_PROVIDER_ID, XAI_GROK_PROVIDER_ID],
694 )
680 -def test_oauth_providers_report_dummy_api_key_when_result_missing(provider_id):
695 +def test_oauth_providers_leave_missing_result_unset_when_disconnected(monkeypatch, provider_id):
696 + monkeypatch.setattr(oauth_dummy_key, "oauth_provider_is_connected", lambda _provider_id: False)
697 data = {"args": (provider_id,), "kwargs": {}}
698
683 - OAuthAccountDummyKey(agent=None).execute(data=data)
699 + oauth_dummy_key.OAuthAccountDummyKey(agent=None).execute(data=data)
700
685 - assert data["result"] == DUMMY_API_KEY
701 + assert "result" not in data
702
703
704 @pytest.mark.parametrize(
@@ -692,7 +708,7 @@ def test_oauth_providers_report_dummy_api_key_when_result_missing(provider_id):
708 def test_oauth_providers_preserve_configured_api_key(provider_id):
709 data = {"args": (provider_id,), "kwargs": {}, "result": "configured"}
710
695 - OAuthAccountDummyKey(agent=None).execute(data=data)
711 + oauth_dummy_key.OAuthAccountDummyKey(agent=None).execute(data=data)
712
713 assert data["result"] == "configured"
714
@@ -708,10 +724,10 @@ def test_model_provider_config_contains_all_oauth_providers():
724 GEMINI_API_PROVIDER_ID,
725 XAI_GROK_PROVIDER_ID,
726 }
711 - assert chat[CODEX_PROVIDER_ID]["kwargs"]["api_key"] == DUMMY_API_KEY
712 - assert chat[GITHUB_COPILOT_PROVIDER_ID]["kwargs"]["api_key"] == DUMMY_API_KEY
713 - assert chat[GEMINI_API_PROVIDER_ID]["kwargs"]["api_key"] == DUMMY_API_KEY
714 - assert chat[XAI_GROK_PROVIDER_ID]["kwargs"]["api_key"] == DUMMY_API_KEY
727 + assert "api_key" not in chat[CODEX_PROVIDER_ID]["kwargs"]
728 + assert "api_key" not in chat[GITHUB_COPILOT_PROVIDER_ID]["kwargs"]
729 + assert "api_key" not in chat[GEMINI_API_PROVIDER_ID]["kwargs"]
730 + assert "api_key" not in chat[XAI_GROK_PROVIDER_ID]["kwargs"]
731 assert chat[CODEX_PROVIDER_ID]["kwargs"]["api_base"] == "http://127.0.0.1/oauth/codex/v1"
732 assert (
733 chat[GITHUB_COPILOT_PROVIDER_ID]["kwargs"]["api_base"]