Follow-up from #93 (shared gateway client package). Raised by the security review on PR #321 as a medium advisory, accepted for that PR because the diff was a no-behavior-change extraction.
build_gateway_client passes settings.llm_gateway_base_url straight into AsyncOpenAI(base_url=...) and attaches the real gateway bearer token via default_headers. It validates neither the scheme nor the host. A misconfigured or attacker-influenced LLM_GATEWAY_URL therefore sends the credential to whatever origin the value names, and every chat, embedding, and moderation call routes there.
Two things changed the shape of this risk in #93, without making it reachable today:
- The builder moved into a shared package, so any future service can call it, not just retriever.
GatewaySettings is a structural typing.Protocol, so any object exposing the four members type-checks where retriever.config.Settings was previously required. The type system no longer constrains what reaches base_url.
Nothing reachable exploits this now: llm_gateway_base_url is resolved in retriever.config from operator-set env vars, not from user input. This is hardening ahead of a second consumer, and the right place for the check is the builder, so every consumer inherits it.
Acceptance Criteria
build_gateway_client raises on a plain-http non-loopback base URL.
build_gateway_client raises on a host outside the allowlist.
- A loopback
http URL still builds, so local dev is unaffected.
- Tests cover all three cases in
packages/llm/tests/.
- Retriever's existing gateway behavior is unchanged for a correctly configured gateway.
Follow-up from #93 (shared gateway client package). Raised by the security review on PR #321 as a medium advisory, accepted for that PR because the diff was a no-behavior-change extraction.
build_gateway_clientpassessettings.llm_gateway_base_urlstraight intoAsyncOpenAI(base_url=...)and attaches the real gateway bearer token viadefault_headers. It validates neither the scheme nor the host. A misconfigured or attacker-influencedLLM_GATEWAY_URLtherefore sends the credential to whatever origin the value names, and every chat, embedding, and moderation call routes there.Two things changed the shape of this risk in #93, without making it reachable today:
GatewaySettingsis a structuraltyping.Protocol, so any object exposing the four members type-checks whereretriever.config.Settingswas previously required. The type system no longer constrains what reachesbase_url.Nothing reachable exploits this now:
llm_gateway_base_urlis resolved inretriever.configfrom operator-set env vars, not from user input. This is hardening ahead of a second consumer, and the right place for the check is the builder, so every consumer inherits it.base_urlwhose scheme is nothttps(allowhttponly for loopback, so local dev against a stub gateway still works).LLM_GATEWAY_URLhost.ValueErrorat client construction, matching howllm_gateway_base_urlalready fails fast when no gateway is configured.build_gateway_clientor in a validator on the settings side, and note the reasoning inpackages/llm/README.md.Acceptance Criteria
build_gateway_clientraises on a plain-httpnon-loopback base URL.build_gateway_clientraises on a host outside the allowlist.httpURL still builds, so local dev is unaffected.packages/llm/tests/.