Repository navigation
feat(providers): put LLM provider endpoints under an egress policy - #230
Conversation
A provider endpoint is configured as a plain string, and whoever terminates that connection reads every prompt, tool result and credential in the request body. The repository already had a domain policy for agent-initiated web fetches (core.network.safe_http, used by the web tools); model traffic bypassed it entirely. This adds the same idea for provider endpoints: * core/providers/egress.py - a pure evaluate_provider_egress() over the base URL, an EgressPolicy, and env/config parsing. DEEPCODE_EGRESS_MODE picks enforce (default) or warn. * ProviderSpec gains endpoint_class beside the existing is_gateway flag (first_party / gateway / aggregator / local / unknown), so the policy can describe what it is talking to instead of guessing from the hostname alone. A repointed base URL is never reported as first_party. * make_llm_provider applies the policy once per construction. A denial raises the module's existing ConfigError naming the host, the endpoint class and the variables that would allow it. Default behaviour is unchanged: an empty allow-list keeps the historical allow-unless-blocked meaning, so a deployment that configures nothing sees no new refusals. DEEPCODE_EGRESS_MODE=warn logs instead of raising, for rollout. Tests: tests/test_provider_egress.py (40) - the pure kernel, endpoint-class resolution, env and config parsing, and the make_llm_provider wiring.
|
@Zongwei9888 requesting security review per CONTRIBUTING.md. Same two fork-side housekeeping notes as #229, so they are not mistaken for a missing checklist: the Default behaviour is unchanged. An empty allow-list keeps the historical allow-unless-blocked meaning, so nothing that configures no policy sees a new refusal. Only One deliberate change worth your eye: a scheme-less or non-http(s) |
|
Merged into On the decision you asked for: the earlier failure for a scheme-less or non-http(s) |
Summary
A provider endpoint is configured as a plain string, and whoever terminates that connection reads every prompt, tool result and credential in the request body. The repository already had a domain policy for agent-initiated web fetches (
core.network.safe_http, used by the web tools); model traffic bypassed it entirely.This applies the same idea to provider endpoints.
Changes
core/providers/egress.pyevaluate_provider_egress()over the base URL, anEgressPolicy, and env/config parsingcore/providers/registry.pyProviderSpecgainsendpoint_classbeside the existingis_gatewayflag;resolve_endpoint_class()derives itcore/config.pyproviders.egressblock +_enforce_provider_egress()applied once inmake_llm_providerendpoint_classis one offirst_party/gateway/aggregator/local/unknown, so the policy can say what it is talking to rather than inferring from a hostname. A repointed base URL is never reported asfirst_party-resolve_endpoint_class()only trusts an explicit class while the resolved base still matches the spec's declared endpoint.Security Considerations
Attack surface changed. Which hosts the client will send prompts, tool output and credentials to is now a checked decision rather than an unexamined string.
Why this is fail-safe.
DEEPCODE_EGRESS_MODE=warnlogs instead of raising, as the rollout escape hatch.ConfigErrornaming the host, the endpoint class, the base URL, and the exact variables that would allow it - so the failure is actionable rather than mysterious.One deliberate behaviour change to flag. A scheme-less or non-
http(s)api_base(e.g.api.example.com/v1) now fails at provider construction with a clear error instead of failing later at request time. Those values never worked; the error is just earlier and clearer.warnmode is the escape hatch if any real configuration relies on the late failure.What this is not. It is not an integrity control. A host on the allow-list can still rewrite the tool calls the model returns - that needs provider-side signing, which nothing deploys today. This narrows where the client talks, not what it is told.
Security verification.
tests/test_provider_egress.py(40 cases): the pure kernel (allow / deny / blocked host / empty base / bad scheme), endpoint-class resolution per spec entry including the repointed-base case, allow/block list parsing from both env and config,warnmode not raising, and an end-to-end check thatmake_llm_providerraises for a host outside a configured allow-list whilehttps://api.deepseek.compasses. No network calls.Tests
Verified under the repository's own
pyproject.tomlpytest config.ruff checkandruff formatclean for the files this PR touches.Notes
is_gatewayflag is preserved unchanged;endpoint_classis additive so nothing that reads the current field breaks.rufffindings that remain incore/config.py(SIM102 x2, TRY004, RUF022) are pre-existing: the unsorted__all__is upstream's own ordering, and this PR does not touch those lines.--no-verifybecause the localpre-commitframework needs/bin/bash, which this Windows checkout lacks.ruffwas run directly.