diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c1bc1..7077833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,13 @@ Newest entries on top, within each tool. ## aikit +### 1.15.5 — 2026-07-03 +- **Fix: invalid virtual key no longer silently accepted during model discovery** (POK-88). + Gateway HTTP clients (`gateway models`, `gateway on`, passthrough probes, doctor + reachability) sent both `Authorization: Bearer` and `x-api-key`. On gateways that + enforce Bearer but accept any `x-api-key`, a typo'd key looked valid. Clients now + send Bearer only, matching LiteLLM virtual-key semantics. + ### 1.15.4 — 2026-07-02 - **Fix: five POK-65 agents falsely showed "needs auth" when authenticated** (POK-78). `auggie`, `devin`, `qodo`, `openinterpreter`, and `plandex` were registered without diff --git a/aikit b/aikit index 8ca4a2b..245d08f 100755 --- a/aikit +++ b/aikit @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -aikit — AI Coding Agent CLI Installer & Manager v1.15.4 +aikit — AI Coding Agent CLI Installer & Manager v1.15.5 Install, update, authenticate, and manage 31 AI coding agent CLIs from one tool. Architecture: @@ -52,7 +52,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn from rich.table import Table from rich.text import Text -__version__ = "1.15.4" +__version__ = "1.15.5" import scriptkit as sk @@ -3222,6 +3222,20 @@ def _normalize_gateway_url(url): # --- model discovery client ------------------------------------------------ +def _gateway_auth_headers(key): + """Auth headers for gateway HTTP clients. + + LiteLLM virtual keys authenticate via ``Authorization: Bearer``. Also sending + ``x-api-key`` can mask a rejected key on gateways that enforce Bearer but accept + any ``x-api-key`` (POK-88). + """ + return { + "Authorization": f"Bearer {key}", + "Accept": "application/json", + "User-Agent": f"aikit/{__version__}", + } + + def _gateway_get_json(url, key, timeout=20): """GET ``url`` with gateway auth headers; return parsed JSON. @@ -3230,12 +3244,7 @@ def _gateway_get_json(url, key, timeout=20): """ import requests - headers = { - "Authorization": f"Bearer {key}", - "x-api-key": key, - "Accept": "application/json", - "User-Agent": f"aikit/{__version__}", - } + headers = _gateway_auth_headers(key) try: resp = requests.get(url, headers=headers, timeout=timeout) except requests.exceptions.RequestException as e: @@ -3346,12 +3355,7 @@ def _probe_status(url, key, *, timeout=6): """ import requests - headers = { - "Authorization": f"Bearer {key}", - "x-api-key": key, - "Accept": "application/json", - "User-Agent": f"aikit/{__version__}", - } + headers = _gateway_auth_headers(key) try: return requests.get(url, headers=headers, timeout=timeout).status_code except requests.exceptions.RequestException: diff --git a/tests/test_aikit_gateway.py b/tests/test_aikit_gateway.py index 3eb5672..67315e0 100644 --- a/tests/test_aikit_gateway.py +++ b/tests/test_aikit_gateway.py @@ -148,6 +148,33 @@ def fake_getter(url, key): aikit.discover_models("https://gw", "k", getter=fake_getter) +def test_gateway_auth_headers_are_bearer_only(aikit): + headers = aikit._gateway_auth_headers("sk-test") + assert headers["Authorization"] == "Bearer sk-test" + assert "x-api-key" not in headers + + +def test_gateway_get_json_rejects_invalid_key_without_x_api_key_mask(aikit, monkeypatch): + """POK-88: a lenient x-api-key must not bypass Bearer enforcement.""" + import requests + + def fake_get(url, headers=None, timeout=None): + auth = (headers or {}).get("Authorization", "") + if (headers or {}).get("x-api-key"): + # Gateway accepts any x-api-key — would have masked a bad bearer if sent. + return type("Resp", (), {"status_code": 200, "ok": True, + "json": lambda self: {"data": []}})() + if auth == "Bearer sk-valid": + return type("Resp", (), {"status_code": 200, "ok": True, + "json": lambda self: {"data": []}})() + return type("Resp", (), {"status_code": 401, "ok": False})() + + monkeypatch.setattr(requests, "get", fake_get) + with pytest.raises(aikit.AikitError, match="rejected the key"): + aikit._gateway_get_json("https://gw/v1/models", "sk-bogus") + aikit._gateway_get_json("https://gw/v1/models", "sk-valid") + + def test_providers_in_use(aikit): models = ["openai/gpt-4o", "anthropic/claude", "bogusprov/x", "bare-model"] detail = {"openai/gpt-4o": {"provider": "openai"}}