refactor(mcp/v2): drop mis-modeled PerUserEnvVar api_key key_source

Per-user env-vars are templated static headers (resolved alongside auth via the static-headers
path), not an api_key credential mode; the PerUserEnvVar key_source was unreachable (no server
config maps to it) and contradicted that model. ApiKeySource is now SharedKey | Byok and the
api_key arm matches the two real sources. Static/env-var header resolution reuses v1's
_resolve_static_headers_with_env_vars at the v2 manager (step 6); the clean v2 rewrite lands at v1
retirement alongside the caching model.
This commit is contained in:
Tin Chi Lo 2026-06-19 10:10:54 -07:00
parent 9fafa09666
commit 9256b5e620
3 changed files with 3 additions and 38 deletions

View file

@ -44,7 +44,6 @@ from .types import (
CredError,
NoneConfig,
PassthroughConfig,
PerUserEnvVar,
ServerSpec,
SharedKey,
Subject,
@ -112,18 +111,6 @@ class UpstreamCredentialProvider:
# The shared key is read straight from ServerSpec.config, not the per-user
# store; it is the same credential for every caller.
return Ok(_api_key_auth(config, source.value.get_secret_value()))
case PerUserEnvVar():
fetched = await self._per_user_value(subject, server)
if isinstance(fetched, Error):
return Error(fetched.error) # store/DB down -> 503
value = fetched.ok
if value is None:
return Error(
CredError.of_precondition_required(
"api_key: per-user env var not set for this subject"
)
)
return Ok(_api_key_auth(config, value))
case Byok():
fetched = await self._per_user_value(subject, server)
if isinstance(fetched, Error):

View file

@ -188,15 +188,6 @@ class SharedKey(BaseModel):
value: SecretStr
class PerUserEnvVar(BaseModel):
"""A per-user value the admin templated as an env var; the user fills it in. Pulled from
the credential store at resolve time. Missing means the user has not completed setup, a
precondition (412) rather than an auth failure."""
model_config = ConfigDict(frozen=True)
source: Literal["per_user_env_var"] = "per_user_env_var"
class Byok(BaseModel):
"""A key the user brings via the entry flow, stored per-user. Pulled from the credential
store at resolve time. Missing means the user must provide it, a 401 + WWW-Authenticate
@ -206,9 +197,7 @@ class Byok(BaseModel):
source: Literal["byok"] = "byok"
ApiKeySource = Annotated[
SharedKey | PerUserEnvVar | Byok, Field(discriminator="source")
]
ApiKeySource = Annotated[SharedKey | Byok, Field(discriminator="source")]
class ApiKeyConfig(BaseModel):

View file

@ -59,7 +59,6 @@ from litellm.proxy.gateway.mcp.outbound_credentials.types import (
CredError,
NoneConfig,
PassthroughConfig,
PerUserEnvVar,
ServerSpec,
SharedKey,
StaticKeys,
@ -295,13 +294,12 @@ def test_secret_fields_are_masked_in_serialization():
assert config.key_source.value.get_secret_value() == "SUPER-SECRET"
@pytest.mark.parametrize("source", [Byok(), PerUserEnvVar()])
async def test_api_key_per_user_pulls_the_subject_credential(source: object):
async def test_api_key_byok_pulls_the_subject_credential():
store = InMemoryCredentialStore(
{CredentialKey(tenant_id="t1", subject_id="u1", server_id="s1"): "user-secret"}
)
provider = _provider(credential_store=store)
result = await provider.resolve(SUBJECT, _spec(ApiKeyConfig(key_source=source))) # type: ignore[arg-type]
result = await provider.resolve(SUBJECT, _spec(ApiKeyConfig(key_source=Byok())))
assert isinstance(result, Ok)
assert _applied_headers(result.ok)["Authorization"] == "Bearer user-secret"
@ -313,15 +311,6 @@ async def test_api_key_byok_missing_returns_unauthorized():
assert result.error.tag == "unauthorized"
async def test_api_key_env_var_missing_returns_precondition_required():
# Missing per-user env var -> 412 (a setup precondition), distinct from BYOK's 401.
result = await PROVIDER.resolve(
SUBJECT, _spec(ApiKeyConfig(key_source=PerUserEnvVar()))
)
assert isinstance(result, Error)
assert result.error.tag == "precondition_required"
async def test_api_key_per_user_isolated_by_subject():
# The stored key belongs to (t1,u1,s1); a different subject must not receive it.
store = InMemoryCredentialStore(