diff --git a/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/__init__.py index 1ca0adeb91d..b9679cfd42e 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/__init__.py @@ -46,6 +46,10 @@ def initialize_guardrail( init_kwargs["block_message"] = litellm_params.block_message if litellm_params.debug is not None: init_kwargs["debug"] = litellm_params.debug + if litellm_params.allow_request_metadata_override is not None: + init_kwargs["allow_request_metadata_override"] = ( + litellm_params.allow_request_metadata_override + ) wonderfence_guardrail = WonderFenceGuardrail(**init_kwargs) diff --git a/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/alice_wonderfence.py b/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/alice_wonderfence.py index 979d71520c7..de4abc2b5f5 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/alice_wonderfence.py +++ b/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/alice_wonderfence.py @@ -59,20 +59,27 @@ class WonderFenceBlockedError(Exception): class WonderFenceGuardrail(CustomGuardrail): """Alice WonderFence guardrail handler using the V2 SDK client. - ``api_key`` and ``app_id`` are resolved per request from request metadata, - API-key metadata, or team metadata. ``api_key`` falls back to a configured - default; ``app_id`` has no default and must be supplied per request. + ``api_key`` and ``app_id`` are resolved per request from API-key metadata, + team metadata, optionally request metadata, with ``api_key`` falling back + to a configured default. ``app_id`` has no default. Resolution order for ``api_key``: - 1. Request metadata: ``metadata.alice_wonderfence_api_key`` - 2. API key metadata: ``user_api_key_metadata.alice_wonderfence_api_key`` - 3. Team metadata: ``user_api_key_team_metadata.alice_wonderfence_api_key`` + 1. API key metadata: ``user_api_key_metadata.alice_wonderfence_api_key`` + 2. Team metadata: ``user_api_key_team_metadata.alice_wonderfence_api_key`` + 3. Request metadata: ``metadata.alice_wonderfence_api_key`` (only when + ``allow_request_metadata_override=True``) 4. Default: configured ``api_key`` or ``ALICE_API_KEY`` env var Resolution order for ``app_id`` (no default — error if missing): - 1. Request metadata: ``metadata.alice_wonderfence_app_id`` - 2. API key metadata: ``user_api_key_metadata.alice_wonderfence_app_id`` - 3. Team metadata: ``user_api_key_team_metadata.alice_wonderfence_app_id`` + 1. API key metadata: ``user_api_key_metadata.alice_wonderfence_app_id`` + 2. Team metadata: ``user_api_key_team_metadata.alice_wonderfence_app_id`` + 3. Request metadata: ``metadata.alice_wonderfence_app_id`` (only when + ``allow_request_metadata_override=True``) + + Admin-pinned credentials (key/team metadata) always win over request + metadata so a caller cannot bypass their assigned WonderFence app. + ``allow_request_metadata_override`` defaults to False; enable only for + trusted-gateway deployments that need request-level overrides. A V2 SDK client is cached per resolved ``api_key`` (LRU). """ @@ -89,6 +96,7 @@ class WonderFenceGuardrail(CustomGuardrail): debug: bool = False, max_cached_clients: Optional[int] = None, connection_pool_limit: Optional[int] = None, + allow_request_metadata_override: bool = False, event_hook: Optional[ Union[GuardrailEventHooks, List[GuardrailEventHooks], Mode] ] = None, @@ -113,6 +121,11 @@ class WonderFenceGuardrail(CustomGuardrail): keyed by api_key). Default 10. Env: ALICE_MAX_CACHED_CLIENTS. connection_pool_limit: Max connections per SDK client HTTP pool. Env: ALICE_CONNECTION_POOL_LIMIT. + allow_request_metadata_override: When True, allow per-request + ``metadata.alice_wonderfence_api_key`` / + ``metadata.alice_wonderfence_app_id`` as a last-resort source + (after API-key and team metadata). Defaults to False so + caller-controlled fields cannot bypass admin-pinned credentials. event_hook: Event hook mode. default_on: Whether the guardrail is enabled by default. """ @@ -142,6 +155,7 @@ class WonderFenceGuardrail(CustomGuardrail): self.platform = platform self.fail_open = fail_open self.block_message = block_message + self.allow_request_metadata_override = allow_request_metadata_override if debug: logger.setLevel(logging.DEBUG) @@ -216,7 +230,13 @@ class WonderFenceGuardrail(CustomGuardrail): ) def _resolve_api_key(self, request_data: dict) -> str: - """Resolve api_key from request → key → team metadata, falling back to default. + """Resolve api_key from key → team → (request, when opt-in) → default. + + Admin-pinned sources (API-key and team metadata) take precedence over + request-body metadata so a caller cannot bypass their assigned + WonderFence credentials. Request metadata is consulted only when + ``allow_request_metadata_override`` is True, and even then only after + the admin-controlled sources. The LiteLLM framework copies key/team metadata from ``UserAPIKeyAuth`` into ``data['metadata']`` under ``user_api_key_metadata`` and @@ -225,10 +245,6 @@ class WonderFenceGuardrail(CustomGuardrail): """ metadata = self._get_metadata(request_data) - req_api_key = metadata.get("alice_wonderfence_api_key") - if req_api_key: - return req_api_key - key_metadata = metadata.get("user_api_key_metadata") or {} if isinstance(key_metadata, dict) and key_metadata.get( "alice_wonderfence_api_key" @@ -241,21 +257,28 @@ class WonderFenceGuardrail(CustomGuardrail): ): return team_metadata["alice_wonderfence_api_key"] + if self.allow_request_metadata_override: + req_api_key = metadata.get("alice_wonderfence_api_key") + if req_api_key: + return req_api_key + if self.api_key: return self.api_key raise WonderFenceMissingSecrets( - "No alice_wonderfence_api_key found in request metadata, API-key " - "metadata, team metadata, or default config (ALICE_API_KEY)." + "No alice_wonderfence_api_key found in API-key metadata, team " + "metadata, request metadata (when allow_request_metadata_override " + "is enabled), or default config (ALICE_API_KEY)." ) def _resolve_app_id(self, request_data: dict) -> str: - """Resolve app_id from request → key → team metadata. No default — raise if missing.""" - metadata = self._get_metadata(request_data) + """Resolve app_id from key → team → (request, when opt-in). No default. - req_app_id = metadata.get("alice_wonderfence_app_id") - if req_app_id: - return req_app_id + Admin-pinned sources win over request-body metadata; request metadata + is only consulted when ``allow_request_metadata_override`` is True. + Raises ``WonderFenceMissingSecrets`` when nothing resolves. + """ + metadata = self._get_metadata(request_data) key_metadata = metadata.get("user_api_key_metadata") or {} if isinstance(key_metadata, dict) and key_metadata.get( @@ -269,9 +292,15 @@ class WonderFenceGuardrail(CustomGuardrail): ): return team_metadata["alice_wonderfence_app_id"] + if self.allow_request_metadata_override: + req_app_id = metadata.get("alice_wonderfence_app_id") + if req_app_id: + return req_app_id + raise WonderFenceMissingSecrets( - "No alice_wonderfence_app_id found in request metadata, API-key " - "metadata, or team metadata. app_id must be provided per request." + "No alice_wonderfence_app_id found in API-key metadata, team " + "metadata, or request metadata (when allow_request_metadata_override " + "is enabled). app_id must be provided per request." ) def _build_analysis_context(self, request_data: dict) -> Any: diff --git a/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/example_config.yaml b/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/example_config.yaml index 91de2f8bdb1..6535b56ab60 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/example_config.yaml +++ b/litellm/proxy/guardrails/guardrail_hooks/alice_wonderfence/example_config.yaml @@ -9,9 +9,15 @@ # ALICE_CONNECTION_POOL_LIMIT - Optional: HTTP pool size per client # OPENAI_API_KEY - API key for OpenAI # -# Per-request / per-key / per-team metadata keys: +# Per-key / per-team metadata keys (admin-controlled): # alice_wonderfence_api_key - overrides default API key (optional) -# alice_wonderfence_app_id - REQUIRED — must be set on request, key, or team +# alice_wonderfence_app_id - REQUIRED — must be set on key or team +# +# Per-request metadata keys (caller-controlled, OFF by default): +# metadata.alice_wonderfence_api_key / metadata.alice_wonderfence_app_id are +# ignored unless allow_request_metadata_override is True on the guardrail. +# Even when enabled, key/team metadata still wins — request metadata is a +# last-resort source only. model_list: - model_name: gpt-4 @@ -22,7 +28,7 @@ model_list: guardrails: # Combined pre + post with advanced knobs - - guardrail_name: "alice-wonderfence-full-guard" + - guardrail_name: "alice-wonderfence" litellm_params: guardrail: alice_wonderfence mode: ["pre_call", "post_call"] @@ -37,9 +43,41 @@ guardrails: # connection_pool_limit: 20 + # Enable only for trusted-gateway deployments that need to forward a + # per-tenant app_id/api_key from the request body. Even with this on, + # admin-pinned key/team metadata still wins; request metadata is a + # last-resort source only. Default is False — leave it off unless your + # callers are themselves trusted infrastructure. + allow_request_metadata_override: true + # Example usage # -# 1. Request-level app_id override (every request must supply app_id somewhere): +# 1. Per-API-key app_id (set at key creation — recommended default): +# +# curl -X POST http://localhost:4000/key/generate \ +# -H "Authorization: Bearer sk-admin" \ +# -H "Content-Type: application/json" \ +# -d '{ +# "metadata": { +# "alice_wonderfence_app_id": "tenant-A-app", +# "alice_wonderfence_api_key": "wf-key-for-tenant-A" +# } +# }' +# +# 2. Per-team app_id (set at team creation): +# +# curl -X POST http://localhost:4000/team/new \ +# -H "Authorization: Bearer sk-admin" \ +# -H "Content-Type: application/json" \ +# -d '{ +# "team_alias": "team-billing", +# "metadata": { +# "alice_wonderfence_app_id": "team-billing-app" +# } +# }' +# +# 3. Request-level app_id (only when allow_request_metadata_override: true on +# the guardrail config — and even then, key/team metadata still wins): # # curl -X POST http://localhost:4000/chat/completions \ # -H "Authorization: Bearer sk-xxx" \ @@ -53,29 +91,7 @@ guardrails: # } # }' # -# 2. Per-API-key app_id (set at key creation, no per-request metadata needed): -# -# curl -X POST http://localhost:4000/key/generate \ -# -H "Authorization: Bearer sk-admin" \ -# -H "Content-Type: application/json" \ -# -d '{ -# "metadata": { -# "alice_wonderfence_app_id": "tenant-A-app", -# "alice_wonderfence_api_key": "wf-key-for-tenant-A" -# } -# }' -# -# 3. Per-team app_id (set at team creation): -# -# curl -X POST http://localhost:4000/team/new \ -# -H "Authorization: Bearer sk-admin" \ -# -H "Content-Type: application/json" \ -# -d '{ -# "team_alias": "team-billing", -# "metadata": { -# "alice_wonderfence_app_id": "team-billing-app" -# } -# }' -# -# Resolution priority (highest first): request metadata > key metadata > team metadata > config default. -# api_key falls back to config / ALICE_API_KEY env. app_id has NO default. +# Resolution priority (highest first): key metadata > team metadata > +# request metadata (only if allow_request_metadata_override=true) > +# config default. api_key falls back to config / ALICE_API_KEY env; +# app_id has NO default. diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/alice_wonderfence.py b/litellm/types/proxy/guardrails/guardrail_hooks/alice_wonderfence.py index db35b1d606f..6785fc5fb08 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/alice_wonderfence.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/alice_wonderfence.py @@ -10,15 +10,25 @@ from .base import GuardrailConfigModel class WonderFenceGuardrailConfigModel(GuardrailConfigModel): """Configuration parameters for the Alice WonderFence guardrail. - Per-request ``api_key`` and ``app_id`` are read from request / API-key / - team metadata using these keys: ``alice_wonderfence_api_key``, - ``alice_wonderfence_app_id``. ``api_id`` has no default. ``api_key`` falls - back to the value below or the ``ALICE_API_KEY`` env var. + Resolution order for ``api_key`` and ``app_id`` (highest first): + API-key metadata → team metadata → request metadata (only when + ``allow_request_metadata_override`` is True) → ``api_key`` falls back to + the configured default below or the ``ALICE_API_KEY`` env var; ``app_id`` + has no default. + + By default, request-body metadata is ignored so a caller cannot bypass + an admin-pinned WonderFence ``app_id`` / ``api_key`` on their virtual + key. Enable ``allow_request_metadata_override`` for trusted-gateway + deployments that legitimately need request-level overrides. """ api_key: Optional[str] = Field( default=None, - description="Default API key for WonderFence (overridable per request via metadata.alice_wonderfence_api_key). Env: ALICE_API_KEY.", + description="Default API key for WonderFence. Overridable via API-key / team metadata, or via request metadata (alice_wonderfence_api_key) only when allow_request_metadata_override is True. Env: ALICE_API_KEY.", + ) + allow_request_metadata_override: Optional[bool] = Field( + default=False, + description="When True, allow alice_wonderfence_api_key and alice_wonderfence_app_id in request metadata as a last-resort source (after API-key and team metadata). Default False so caller-controlled request fields cannot bypass admin-pinned credentials.", ) api_base: Optional[str] = Field( default=None, diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_alice_wonderfence.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_alice_wonderfence.py index 828188a3a95..eaea12b7a79 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_alice_wonderfence.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_alice_wonderfence.py @@ -56,9 +56,18 @@ def _make_guardrail(monkeypatch, **overrides): def _request_data(**overrides): + """Build a request-data dict. + + Default metadata pins ``alice_wonderfence_app_id`` on + ``user_api_key_metadata`` (admin-controlled) so the request resolves + cleanly under the safe-by-default precedence model. Tests that want to + drive the value through request metadata must (a) construct a guardrail + with ``allow_request_metadata_override=True`` and (b) pass the value via + the ``metadata`` kwarg explicitly. + """ metadata = overrides.pop("metadata", None) if metadata is None: - metadata = {"alice_wonderfence_app_id": "test-app"} + metadata = {"user_api_key_metadata": {"alice_wonderfence_app_id": "test-app"}} base = {"model": "gpt-4", "metadata": metadata} base.update(overrides) return base @@ -67,12 +76,26 @@ def _request_data(**overrides): # ----------------------------- resolver tests ----------------------------- -def test_resolve_app_id_from_request_metadata(monkeypatch): - guardrail, _ = _make_guardrail(monkeypatch) +def test_resolve_app_id_from_request_metadata_requires_override_flag(monkeypatch): + guardrail, _ = _make_guardrail(monkeypatch, allow_request_metadata_override=True) data = _request_data(metadata={"alice_wonderfence_app_id": "from-req"}) assert guardrail._resolve_app_id(data) == "from-req" +def test_resolve_app_id_request_metadata_ignored_when_override_disabled(monkeypatch): + """Request metadata is caller-controlled and must not satisfy app_id when + the override flag is off — otherwise a user could bypass admin-pinned + credentials by sending their own app_id in the request body.""" + from litellm.proxy.guardrails.guardrail_hooks.alice_wonderfence.alice_wonderfence import ( + WonderFenceMissingSecrets, + ) + + guardrail, _ = _make_guardrail(monkeypatch) # override defaults False + data = _request_data(metadata={"alice_wonderfence_app_id": "from-req"}) + with pytest.raises(WonderFenceMissingSecrets, match="alice_wonderfence_app_id"): + guardrail._resolve_app_id(data) + + def test_resolve_app_id_from_key_metadata(monkeypatch): guardrail, _ = _make_guardrail(monkeypatch) data = _request_data( @@ -93,8 +116,10 @@ def test_resolve_app_id_from_team_metadata(monkeypatch): assert guardrail._resolve_app_id(data) == "from-team" -def test_resolve_app_id_priority_request_over_key_over_team(monkeypatch): - guardrail, _ = _make_guardrail(monkeypatch) +def test_resolve_app_id_key_beats_request_even_when_override_enabled(monkeypatch): + """With the override flag on, request metadata is still only a last-resort + source — admin-pinned key metadata wins.""" + guardrail, _ = _make_guardrail(monkeypatch, allow_request_metadata_override=True) data = _request_data( metadata={ "alice_wonderfence_app_id": "from-req", @@ -102,7 +127,19 @@ def test_resolve_app_id_priority_request_over_key_over_team(monkeypatch): "user_api_key_team_metadata": {"alice_wonderfence_app_id": "from-team"}, } ) - assert guardrail._resolve_app_id(data) == "from-req" + assert guardrail._resolve_app_id(data) == "from-key" + + +def test_resolve_app_id_team_beats_request_when_override_enabled(monkeypatch): + """Team metadata beats request metadata even with the override flag on.""" + guardrail, _ = _make_guardrail(monkeypatch, allow_request_metadata_override=True) + data = _request_data( + metadata={ + "alice_wonderfence_app_id": "from-req", + "user_api_key_team_metadata": {"alice_wonderfence_app_id": "from-team"}, + } + ) + assert guardrail._resolve_app_id(data) == "from-team" def test_resolve_app_id_priority_key_over_team(monkeypatch): @@ -127,12 +164,37 @@ def test_resolve_app_id_missing_raises(monkeypatch): guardrail._resolve_app_id(data) -def test_resolve_api_key_from_request_metadata(monkeypatch): - guardrail, _ = _make_guardrail(monkeypatch, api_key="default") +def test_resolve_api_key_from_request_metadata_requires_override_flag(monkeypatch): + guardrail, _ = _make_guardrail( + monkeypatch, api_key="default", allow_request_metadata_override=True + ) data = _request_data(metadata={"alice_wonderfence_api_key": "from-req"}) assert guardrail._resolve_api_key(data) == "from-req" +def test_resolve_api_key_request_metadata_ignored_when_override_disabled(monkeypatch): + """With override off, a caller-supplied api_key must not be honored; + falls back to the configured default instead.""" + guardrail, _ = _make_guardrail(monkeypatch, api_key="default") + data = _request_data(metadata={"alice_wonderfence_api_key": "from-req"}) + assert guardrail._resolve_api_key(data) == "default" + + +def test_resolve_api_key_key_beats_request_even_when_override_enabled(monkeypatch): + """Admin-pinned key metadata wins over request metadata even with the + override flag enabled.""" + guardrail, _ = _make_guardrail( + monkeypatch, api_key="default", allow_request_metadata_override=True + ) + data = _request_data( + metadata={ + "alice_wonderfence_api_key": "from-req", + "user_api_key_metadata": {"alice_wonderfence_api_key": "from-key"}, + } + ) + assert guardrail._resolve_api_key(data) == "from-key" + + def test_resolve_api_key_from_key_metadata(monkeypatch): guardrail, _ = _make_guardrail(monkeypatch, api_key="default") data = _request_data( @@ -172,10 +234,15 @@ def test_resolve_api_key_missing_everywhere_raises(monkeypatch): def test_resolve_reads_litellm_metadata_when_metadata_absent(monkeypatch): + """``_get_metadata`` falls back to ``litellm_metadata`` when ``metadata`` + is missing. Use admin-controlled key metadata so it resolves without + needing the request-override flag.""" guardrail, _ = _make_guardrail(monkeypatch) data = { "model": "gpt-4", - "litellm_metadata": {"alice_wonderfence_app_id": "from-litellm-md"}, + "litellm_metadata": { + "user_api_key_metadata": {"alice_wonderfence_app_id": "from-litellm-md"} + }, } assert guardrail._resolve_app_id(data) == "from-litellm-md" @@ -487,7 +554,9 @@ async def test_apply_guardrail_passes_app_id_per_call(guardrail_and_client): await guardrail.apply_guardrail( inputs={"texts": ["hi"]}, - request_data=_request_data(metadata={"alice_wonderfence_app_id": "tenant-A"}), + request_data=_request_data( + metadata={"user_api_key_metadata": {"alice_wonderfence_app_id": "tenant-A"}} + ), input_type="request", ) kwargs = client.evaluate_prompt.call_args.kwargs @@ -508,7 +577,9 @@ async def test_apply_guardrail_response_path_passes_app_id(monkeypatch): await guardrail.apply_guardrail( inputs={"texts": ["resp"]}, - request_data=_request_data(metadata={"alice_wonderfence_app_id": "tenant-B"}), + request_data=_request_data( + metadata={"user_api_key_metadata": {"alice_wonderfence_app_id": "tenant-B"}} + ), input_type="response", ) kwargs = client.evaluate_response.call_args.kwargs @@ -660,7 +731,9 @@ async def test_post_call_recovers_app_id_via_logging_obj_stash(monkeypatch): """Reproduces the framework gap: request body metadata is dropped before post_call. The logging_obj stash from the prior `input_type="request"` call must be used to resolve app_id.""" - guardrail, client = _make_guardrail(monkeypatch) + guardrail, client = _make_guardrail( + monkeypatch, allow_request_metadata_override=True + ) guardrail._client_cache["default-api-key"] = client request_obj = Mock() request_obj.action = "NO_ACTION" @@ -702,7 +775,9 @@ async def test_post_call_recovers_app_id_via_logging_obj_stash(monkeypatch): async def test_post_call_prefers_request_data_over_stash(monkeypatch): """If post_call's request_data still resolves (e.g. app_id from key/team metadata), use it — don't fall back to the stash.""" - guardrail, client = _make_guardrail(monkeypatch) + guardrail, client = _make_guardrail( + monkeypatch, allow_request_metadata_override=True + ) guardrail._client_cache["default-api-key"] = client request_obj = Mock() request_obj.action = "NO_ACTION" @@ -770,9 +845,17 @@ async def test_post_call_recovers_via_sibling_stash(monkeypatch): `guardrails` array, LiteLLM only invokes one's during_call — but every instance runs post_call. The instance whose during_call did NOT fire must recover the stash written by the sibling that did.""" - g_writer, c_writer = _make_guardrail(monkeypatch, guardrail_name="writer") + g_writer, c_writer = _make_guardrail( + monkeypatch, + guardrail_name="writer", + allow_request_metadata_override=True, + ) g_writer._client_cache["default-api-key"] = c_writer - g_reader, c_reader = _make_guardrail(monkeypatch, guardrail_name="reader") + g_reader, c_reader = _make_guardrail( + monkeypatch, + guardrail_name="reader", + allow_request_metadata_override=True, + ) g_reader._client_cache["default-api-key"] = c_reader for c in (c_writer, c_reader): result = Mock() @@ -807,9 +890,17 @@ async def test_post_call_recovers_via_sibling_stash(monkeypatch): async def test_stash_keyed_per_guardrail_name(monkeypatch): """Two alice_wonderfence instances on the same logging_obj must not overwrite each other's stash — they're keyed by guardrail_name.""" - g1, c1 = _make_guardrail(monkeypatch, guardrail_name="alice-a") + g1, c1 = _make_guardrail( + monkeypatch, + guardrail_name="alice-a", + allow_request_metadata_override=True, + ) g1._client_cache["default-api-key"] = c1 - g2, c2 = _make_guardrail(monkeypatch, guardrail_name="alice-b") + g2, c2 = _make_guardrail( + monkeypatch, + guardrail_name="alice-b", + allow_request_metadata_override=True, + ) g2._client_cache["default-api-key"] = c2 for c in (c1, c2): result = Mock() @@ -898,6 +989,7 @@ def test_initialize_guardrail_forwards_all_params(monkeypatch): debug=True, max_cached_clients=5, connection_pool_limit=20, + allow_request_metadata_override=True, default_on=True, ) guardrail = {"guardrail_name": "wf-init-test"} @@ -912,6 +1004,14 @@ def test_initialize_guardrail_forwards_all_params(monkeypatch): assert g.block_message == "custom block" assert g._client_cache_maxsize == 5 assert g._connection_pool_limit == 20 + assert g.allow_request_metadata_override is True + + +def test_allow_request_metadata_override_defaults_false(monkeypatch): + """New flag must default to False so request-body metadata cannot + bypass admin-pinned credentials out of the box.""" + guardrail, _ = _make_guardrail(monkeypatch) + assert guardrail.allow_request_metadata_override is False def test_initialize_guardrail_missing_name_raises(monkeypatch):