Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 18 additions & 14 deletions aikit
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
27 changes: 27 additions & 0 deletions tests/test_aikit_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
Expand Down
Loading