Skip to content

Commit 81a4577

Browse files
author
Mateusz
committed
fix(gemini): use Windows persistent env fallback and numbered-key precedence for GEMINI_API_KEY
The gemini backend was reading GEMINI_API_KEY directly from the process environment, which on Windows can contain stale values from previous sessions. This caused leaked/disabled API keys to be sent instead of fresh GEMINI_API_KEY_1..N numbered variants. Changes: - from_env_part3.py: replace raw env.get(GEMINI_API_KEY) with get_env_value_with_windows_persistent_fallback() and guard with _has_numbered_env_variants() so numbered keys take precedence - When GEMINI_API_KEY_1..N exist, the base gemini backend no longer binds GEMINI_API_KEY, preventing stale keys from being used - Add 17 regression tests covering Windows fallback, numbered-key discovery, precedence, and end-to-end integration
1 parent c6129b0 commit 81a4577

3 files changed

Lines changed: 824 additions & 2 deletions

File tree

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
"""Demo script proving the Gemini API key loading fix.
2+
3+
Simulates the exact production bug:
4+
- Stale GEMINI_API_KEY in process env (old leaked key)
5+
- Fresh GEMINI_API_KEY_1..3 set (never used, cannot leak)
6+
7+
Demonstrates that:
8+
1. Without the fix: proxy would send the stale leaked key
9+
2. With the fix: proxy correctly uses the numbered instances
10+
11+
Run: .venv/Scripts/python.exe dev/scripts/demo_gemini_api_key_loading_fix.py
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import sys
17+
from pathlib import Path
18+
from types import SimpleNamespace
19+
from unittest.mock import patch
20+
21+
_project_root = Path(__file__).resolve().parents[2]
22+
if str(_project_root) not in sys.path:
23+
sys.path.insert(0, str(_project_root))
24+
25+
26+
# ── Test data ──────────────────────────────────────────────────────────────
27+
28+
OLD_LEAKED_KEY = "AIzaSy-old-leaked-key-disabled-by-google"
29+
FRESH_KEY_1 = "AIzaSy-fresh-never-used-1"
30+
FRESH_KEY_2 = "AIzaSy-fresh-never-used-2"
31+
FRESH_KEY_3 = "AIzaSy-fresh-never-used-3"
32+
33+
34+
def _simulate_windows_env(monkeypatch, stale_key: str | None) -> None:
35+
"""Set up a fake Windows registry with a stale key different from process env."""
36+
monkeypatch.setattr(sys, "platform", "win32")
37+
38+
# Registry has a different key than process env
39+
registry_key = OLD_LEAKED_KEY if stale_key is None else stale_key
40+
41+
class _Key:
42+
def __init__(self, hive: str, subkey: str) -> None:
43+
self.hive = hive
44+
self.subkey = subkey
45+
46+
def __enter__(self) -> "_Key":
47+
return self
48+
49+
def __exit__(self, exc_type, exc, tb) -> None:
50+
return None
51+
52+
def _query(key: _Key, name: str) -> tuple[str, int]:
53+
if name == "GEMINI_API_KEY":
54+
return (registry_key, 1)
55+
raise OSError("not found")
56+
57+
fake_winreg = SimpleNamespace(
58+
HKEY_CURRENT_USER="HKCU",
59+
HKEY_LOCAL_MACHINE="HKLM",
60+
OpenKey=lambda hive, subkey: _Key(hive, subkey),
61+
QueryValueEx=_query,
62+
)
63+
monkeypatch.setitem(sys.modules, "winreg", fake_winreg)
64+
65+
66+
def demo_broken_scenario(monkeypatch) -> None:
67+
"""Show what the OLD code would do: pick the stale leaked key."""
68+
print("=" * 72)
69+
print("SCENARIO 1: OLD behavior (broken) - raw env.get('GEMINI_API_KEY')")
70+
print("=" * 72)
71+
72+
env = {
73+
"LLM_BACKEND": "gemini",
74+
"GEMINI_API_KEY": OLD_LEAKED_KEY,
75+
"GEMINI_API_KEY_1": FRESH_KEY_1,
76+
"GEMINI_API_KEY_2": FRESH_KEY_2,
77+
"GEMINI_API_KEY_3": FRESH_KEY_3,
78+
}
79+
80+
_simulate_windows_env(monkeypatch, stale_key=OLD_LEAKED_KEY)
81+
82+
# Simulate old broken code path
83+
if env.get("GEMINI_API_KEY"):
84+
picked_key = env["GEMINI_API_KEY"]
85+
print(f" Raw env.get('GEMINI_API_KEY') returned: {picked_key[:30]}...")
86+
print(f" This is the LEAKED, DISABLED key!")
87+
print(f" Result: Remote API would reject this key with 403")
88+
print()
89+
assert picked_key == OLD_LEAKED_KEY
90+
print(f" FAIL: Proxy would send stale key: {picked_key[:20]}***")
91+
print()
92+
93+
94+
def demo_fixed_scenario(monkeypatch) -> None:
95+
"""Show what the NEW code does: ignores stale key when numbered variants exist."""
96+
print("=" * 72)
97+
print("SCENARIO 2: NEW behavior (fixed) - Windows fallback + numbered guard")
98+
print("=" * 72)
99+
100+
env = {
101+
"LLM_BACKEND": "gemini",
102+
"GEMINI_API_KEY": OLD_LEAKED_KEY,
103+
"GEMINI_API_KEY_1": FRESH_KEY_1,
104+
"GEMINI_API_KEY_2": FRESH_KEY_2,
105+
"GEMINI_API_KEY_3": FRESH_KEY_3,
106+
}
107+
108+
_simulate_windows_env(monkeypatch, stale_key=OLD_LEAKED_KEY)
109+
110+
# Simulate fixed code path
111+
from src.core.common.env_utils import get_env_value_with_windows_persistent_fallback
112+
import src.core.config.env.from_env_part3 as _part3
113+
114+
gemini_key, gemini_source = get_env_value_with_windows_persistent_fallback(
115+
"GEMINI_API_KEY", environ=env
116+
)
117+
has_variants = _part3._has_numbered_env_variants(env, "GEMINI_API_KEY") # noqa: SLF001
118+
119+
print(f" Windows fallback resolved key from: {gemini_source}")
120+
print(f" Numbered variants exist: {has_variants}")
121+
print()
122+
123+
if gemini_key and not has_variants:
124+
print(f" Would bind base gemini with key: {gemini_key[:30]}...")
125+
else:
126+
print(f" Numbered variants present => skipping base key binding")
127+
print(f" Instances will be created from numbered keys instead:")
128+
print(f" gemini.1 => {FRESH_KEY_1[:20]}...")
129+
print(f" gemini.2 => {FRESH_KEY_2[:20]}...")
130+
print(f" gemini.3 => {FRESH_KEY_3[:20]}...")
131+
print()
132+
print(f" PASS: Stale leaked key is correctly ignored")
133+
print()
134+
135+
136+
def demo_full_integration(monkeypatch) -> None:
137+
"""Full AppConfig.from_env integration test."""
138+
print("=" * 72)
139+
print("SCENARIO 3: Full integration - AppConfig.from_env with numbered keys")
140+
print("=" * 72)
141+
142+
env = {
143+
"LLM_BACKEND": "gemini",
144+
"GEMINI_API_KEY": OLD_LEAKED_KEY,
145+
"GEMINI_API_KEY_1": FRESH_KEY_1,
146+
"GEMINI_API_KEY_2": FRESH_KEY_2,
147+
"GEMINI_API_KEY_3": FRESH_KEY_3,
148+
}
149+
150+
with (
151+
patch(
152+
"src.core.config.sources.backend_instances.backend_registry.get_registered_backends",
153+
return_value=["gemini"],
154+
),
155+
patch(
156+
"src.core.config.models.backends.backend_registry.get_registered_backends",
157+
return_value=["gemini"],
158+
),
159+
):
160+
from src.core.config.app_config import AppConfig
161+
162+
cfg = AppConfig.from_env(environ=env)
163+
164+
base_cfg = cfg.backends.lookup("gemini")
165+
instance_1 = cfg.backends.lookup("gemini.1")
166+
instance_2 = cfg.backends.lookup("gemini.2")
167+
instance_3 = cfg.backends.lookup("gemini.3")
168+
169+
print(f" Backends discovered:")
170+
171+
if base_cfg is not None:
172+
print(f" gemini (base) => api_key: {base_cfg.api_key or 'None'}")
173+
else:
174+
print(f" gemini (base) => not bound (correct!)")
175+
176+
if instance_1 and instance_1.api_key:
177+
print(f" gemini.1 => api_key: {instance_1.api_key[:20]}...")
178+
if instance_2 and instance_2.api_key:
179+
print(f" gemini.2 => api_key: {instance_2.api_key[:20]}...")
180+
if instance_3 and instance_3.api_key:
181+
print(f" gemini.3 => api_key: {instance_3.api_key[:20]}...")
182+
183+
print()
184+
185+
# Verify invariants
186+
errors = []
187+
188+
if base_cfg is not None and base_cfg.api_key == OLD_LEAKED_KEY:
189+
errors.append("CRITICAL: base gemini still has the leaked key!")
190+
191+
if not instance_1:
192+
errors.append("gemini.1 instance missing")
193+
elif instance_1.api_key != FRESH_KEY_1:
194+
assert instance_1.api_key is not None
195+
errors.append(f"gemini.1 has wrong key: {instance_1.api_key[:20]}...")
196+
197+
if not instance_2:
198+
errors.append("gemini.2 instance missing")
199+
elif instance_2.api_key != FRESH_KEY_2:
200+
assert instance_2.api_key is not None
201+
errors.append(f"gemini.2 has wrong key: {instance_2.api_key[:20]}...")
202+
203+
if not instance_3:
204+
errors.append("gemini.3 instance missing")
205+
elif instance_3.api_key != FRESH_KEY_3:
206+
assert instance_3.api_key is not None
207+
errors.append(f"gemini.3 has wrong key: {instance_3.api_key[:20]}...")
208+
209+
if errors:
210+
for e in errors:
211+
print(f" FAIL: {e}")
212+
else:
213+
print(" PASS: All invariants hold - numbered instances are correct,")
214+
print(" stale base key is ignored")
215+
print()
216+
217+
218+
def demo_single_base_key_only(monkeypatch) -> None:
219+
"""When only GEMINI_API_KEY is set, it should still work."""
220+
print("=" * 72)
221+
print("SCENARIO 4: Only GEMINI_API_KEY set (no numbered variants)")
222+
print("=" * 72)
223+
224+
env = {
225+
"LLM_BACKEND": "gemini",
226+
"GEMINI_API_KEY": FRESH_KEY_1,
227+
}
228+
229+
with (
230+
patch(
231+
"src.core.config.sources.backend_instances.backend_registry.get_registered_backends",
232+
return_value=["gemini"],
233+
),
234+
patch(
235+
"src.core.config.models.backends.backend_registry.get_registered_backends",
236+
return_value=["gemini"],
237+
),
238+
):
239+
from src.core.config.app_config import AppConfig
240+
241+
cfg = AppConfig.from_env(environ=env)
242+
243+
base_cfg = cfg.backends.lookup("gemini")
244+
instance_1 = cfg.backends.lookup("gemini.1")
245+
246+
print(f" gemini (base) => api_key: {base_cfg.api_key[:20] if base_cfg and base_cfg.api_key else 'None'}...")
247+
print(f" gemini.1 => {'present' if instance_1 else 'not created'}")
248+
print()
249+
250+
if base_cfg and base_cfg.api_key == FRESH_KEY_1 and instance_1 is None:
251+
print(" PASS: Single base key works correctly")
252+
else:
253+
print(" FAIL: Expected base key binding with no numbered instances")
254+
print()
255+
256+
257+
def demo_single_numbered_key_only(monkeypatch) -> None:
258+
"""When only GEMINI_API_KEY_1 is set, only gemini.1 should be created."""
259+
print("=" * 72)
260+
print("SCENARIO 5: Only GEMINI_API_KEY_1 set (no base key)")
261+
print("=" * 72)
262+
263+
env = {
264+
"LLM_BACKEND": "gemini",
265+
"GEMINI_API_KEY_1": FRESH_KEY_1,
266+
}
267+
268+
with (
269+
patch(
270+
"src.core.config.sources.backend_instances.backend_registry.get_registered_backends",
271+
return_value=["gemini"],
272+
),
273+
patch(
274+
"src.core.config.models.backends.backend_registry.get_registered_backends",
275+
return_value=["gemini"],
276+
),
277+
):
278+
from src.core.config.app_config import AppConfig
279+
280+
cfg = AppConfig.from_env(environ=env)
281+
282+
base_cfg = cfg.backends.lookup("gemini")
283+
instance_1 = cfg.backends.lookup("gemini.1")
284+
285+
print(f" gemini (base) => api_key: {base_cfg.api_key[:20] if base_cfg and base_cfg.api_key else 'None'}...")
286+
print(f" gemini.1 => api_key: {instance_1.api_key[:20] if instance_1 and instance_1.api_key else 'None'}...")
287+
print()
288+
289+
has_base_key = base_cfg and base_cfg.api_key and base_cfg.api_key != OLD_LEAKED_KEY
290+
has_instance_1 = instance_1 and instance_1.api_key == FRESH_KEY_1
291+
292+
if has_instance_1:
293+
print(" PASS: Single numbered key creates correct instance")
294+
if has_base_key:
295+
print(" Note: base key also set (acceptable, will be from env fallback)")
296+
else:
297+
print(" FAIL: Expected gemini.1 instance")
298+
print()
299+
300+
301+
# ── Main ───────────────────────────────────────────────────────────────────
302+
303+
def main() -> None:
304+
print()
305+
print("*" * 72)
306+
print("* Gemini API Key Loading Fix - Demonstration")
307+
print("*" * 72)
308+
print()
309+
310+
# We use a simple monkeypatch approach
311+
import sys as _sys
312+
313+
saved_modules = {}
314+
315+
def _run(scenario_fn) -> None:
316+
"""Run a scenario with isolated module state."""
317+
# Remove winreg if present from previous run
318+
saved_modules["winreg"] = _sys.modules.get("winreg")
319+
saved_modules["platform"] = _sys.modules.get("platform")
320+
321+
scenario_fn(_Monkeypatch())
322+
323+
# Cleanup
324+
if "winreg" in _sys.modules:
325+
del _sys.modules["winreg"]
326+
if saved_modules["winreg"] is not None:
327+
_sys.modules["winreg"] = saved_modules["winreg"]
328+
329+
_run(demo_broken_scenario)
330+
_run(demo_fixed_scenario)
331+
_run(demo_full_integration)
332+
_run(demo_single_base_key_only)
333+
_run(demo_single_numbered_key_only)
334+
335+
print("*" * 72)
336+
print("* Demonstration complete")
337+
print("*" * 72)
338+
print()
339+
340+
341+
class _Monkeypatch:
342+
"""Minimal monkeypatch implementation for the demo."""
343+
344+
def setattr(self, obj: object, name: str, value: object) -> None:
345+
setattr(obj, name, value)
346+
347+
def setitem(self, mapping: dict, key: str, value: object) -> None:
348+
mapping[key] = value
349+
350+
351+
if __name__ == "__main__":
352+
main()

src/core/config/env/from_env_part3.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,19 @@ def apply_config_part3(
137137
origin="OPENROUTER_API_KEY",
138138
)
139139

140-
if env.get("GEMINI_API_KEY"):
140+
gemini_key, gemini_key_source = get_env_value_with_windows_persistent_fallback(
141+
"GEMINI_API_KEY", environ=env
142+
)
143+
if gemini_key and not _has_numbered_env_variants(env, "GEMINI_API_KEY"):
144+
if logger.isEnabledFor(logging.INFO):
145+
logger.info(
146+
"Gemini key diagnostics [from_env_part3]: env_type=%s source=%s",
147+
type(env).__name__,
148+
gemini_key_source,
149+
)
150+
141151
config_backends["gemini"] = config_backends.get("gemini", {})
142-
config_backends["gemini"]["api_key"] = env["GEMINI_API_KEY"]
152+
config_backends["gemini"]["api_key"] = gemini_key
143153
config_backends["gemini"]["api_url"] = _get_env_value(
144154
env,
145155
"GEMINI_API_BASE_URL",

0 commit comments

Comments
 (0)