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"]