fix(security): read provider host allowlist from KV at request time - #611
fix(security): read provider host allowlist from KV at request time#611cursor[bot] wants to merge 18 commits into
Conversation
…closed otherwise Chat history: message-level audio and legacy function_call are null/empty omit no-ops; non-empty fail closed with named errors (including tools passthrough). Tip substrate from #577 assistant refusal/annotations honesty. Local full unit: 940 passed.
…ed otherwise OpenAI fine-tune style message weight is not applied on this gateway. Accept null/0/1 as honest no-ops; reject other types and values with invalid_message_weight. Tip substrate from #578. Local full unit: 943 passed.
…ion role Reject unsupported message keys with named unknown_message_fields (not silent strip or tools-passthrough smuggle). Reject legacy function role with invalid_message_role migration to tool. Tip substrate from #579. Local full unit: 947 passed.
OpenAI partial-assistant prefix flag is not applied on this gateway. null/false are honest no-ops; true and non-booleans fail closed with invalid_message_prefix. Tip substrate from #580. Local full unit: 950 passed.
…therwise Named invalid_max_tool_calls on /v1/chat/completions instead of opaque unknown_fields. Aligns with Responses max_tool_calls honesty; gateway has no multi-step tool loop.
…losed otherwise Legacy /v1/completions treated max_tool_calls as unknown_fields. Accept the key for named invalid_max_tool_calls (null/empty/whitespace omit-equivalent), matching chat/Responses honesty so SDKs get a clear migration path.
SDK clients often send include_usage/include_obfuscation as JSON null. Drop null flag values before validation so null (and null+false mixes) match omit / all-false no-ops on chat, Completions, and Responses. True flags remain fail-closed with invalid_stream_options.
…age honesty Null include_usage/include_obfuscation stay omit-equivalent, but unknown stream_options keys no longer become no-ops just because their value is null. Weight, prefix, refusal, annotations, developer role, empty user/system content, and participant name now use the same named errors on the tools passthrough path as on orchestration. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Hoist stream, required model, stream_options, and temperature/top_p range checks before proxy_completion so a tools or response_format body cannot return a billed JSON completion when the SDK asked for SSE, or silently pick a pool model when model is omitted. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Call the orchestration message, max_tokens, attribution, and routing validators before proxy_completion so a tools or response_format body cannot bill a completion with no prompt, crash on a non-object entry, or silently drop unknown spend/routing keys. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Passthrough has no batch job plane. Reject routing.channel=batch and latency_tolerant=true before proxy_completion so a tool-calling body cannot bill a silent sync completion. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Hoist the orchestration request-knob validators before proxy_completion so a tools or response_format body cannot bill a completion that silently drops seed, stop, n>1, logprobs, logit_bias, or out-of-range penalties. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
stream_chat defaulted to 0.2 even after the HTTP path wrote default_temperature from the request. A streamed invoice summary at temperature=0.8 no longer silently falls back to 0.2. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
… proxy SDK optional defaults serialize omitted fields as JSON null. Accepting those keys without popping them is not omit-equivalent: proxy_completion forwards the body and several providers reject a null JSON Schema object. Pop the keys in place, keep non-null wrong types on invalid_tools, and assert omit-real via mock echo on chat and Responses. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Stop reading CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS in ModelClient._validate_provider. The allowlist lives in provider_egress.allowed_provider_hosts; env is bootstrap transport via seed_provider_egress_from_environ only. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Prove a second seed() after an env edit keeps the first KV set, that _validate_provider ignores env-only allowlists, and that a detached get_config_store() write is not the request-time source. Hold the runtime lock across seed read+write and fuzz the host CSV parser. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Parallel successor note — rebase onto #613, do not land this stack beside #601
This KV allowlist slice (
c91629eon #6017e9a339) is still the right unique work. Do not merge #598 or draft #607 in parallel.The mode/trace fail-open that was listed as the next honesty slice is now #613 (
37f1224):_validate_chat_passthrough_orchestration_controlsbeforeproxy_completion. Live HTTP on #601:tools+mode=explodebilled achat.completion.Merger next action: land #613 as the honesty tip, then rebase these two unique commits (
af26158,c91629e) onto that tip. Do not open a third ALLOWED_PROVIDER_HOSTS PR. Independent non-author APPROVE + Full unit / Semgrep still required. This automation will not approve or merge.Buyer next action after this lands: seed
provider_egress.allowed_provider_hostsin the KV (or start once withCONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTSso bootstrap can copy it). Do not expect a later env edit to change egress on a live process.Sent by Cursor Automation: Fix Issues
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
There was a problem hiding this comment.
Unique slice (7e9a339..c91629e) — ready for an independent reviewer
The two unique commits are the right ALLOWED_PROVIDER_HOSTS work. ModelClient._validate_provider reads allowed_provider_hosts() only. seed_provider_egress_from_environ() copies env into provider_egress.allowed_provider_hosts once, under _runtime_lock. Detached get_config_store() writes are not the request-time source. Local scripts printed ok: tests/test_provider_host_allowlist_kv.py, tests/test_security_hardening.py, tests/test_conventions.py.
Do not merge #598 or draft #607 in parallel. Do not open another allowlist PR.
Landing order
- Prefer #616 (
fe444d2on this tip) when landing allowlist plus trusted-trace email together. This PR remains the allowlist-only slice. - #613 (
37f1224) is the mode/trace honesty tip on the same #601 substrate. Rebase #613 onto this tip or onto #616 after the allowlist lands. Do not merge this stack and #613 ontomainside by side. - Independent non-author APPROVE plus Full unit / Semgrep are still required. This automation will not approve or merge.
Buyer next action
Seed provider_egress.allowed_provider_hosts on the process runtime ConfigStore (or start once with CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS so bootstrap can copy it). Do not edit the env var on a live process and expect egress to change. Do not write the key only into a new get_config_store(postgres_dsn=...) instance.
Next unique slice after this lands: move remaining __main__.py bootstrap tokens into the credential KV. Do not wait on queued Checks to start that work.
Sent by Cursor Automation: Fix Issues


Summary
a50911f) onto the current honesty-stack landing vehicle fix(api): fail-closed seed/stop/n/logprobs on tools passthrough #601 (7e9a339), which already includes omit-real tool.function nulls and streamed request temperature.provider_egress.allowed_provider_hostsfrom the process-wide runtime ConfigStore viaallowed_provider_hosts().CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTSis bootstrap transport only (seed_provider_egress_from_environin__main__.mainandserve()).seed()after an env edit must keep the first set;_validate_providerwith env-onlyexample.comand an empty process store must not rejecthttps://api.openai.com; a detachedget_config_store()write is not the request-time source; seed read+write holds_runtime_lock;_parse_host_allowlisthas a Hypothesis /exercise_host_allowlistfuzz seam.InMemoryConfigStoreunless bootstrap installed another backend withset_runtime_config_store(). Writes to a new Postgrescom_configinstance are ignored.Merger next action
Test plan
python3 tests/test_provider_host_allowlist_kv.pypython3 tests/test_security_hardening.pypython3 tests/test_self_check.pytest_paper_contracts.pytest_conventions.pytest_api_contract.pytest_product_planning_contract.pypython3 tests/test_provider_tls.pytest_provider_reliability.pytest_repository_security_metadata.pytest_host_allowlist_parser_never_crashesBuyer next action
Call
set_runtime_config("provider_egress", "allowed_provider_hosts", "api.example.com")(or start the process withCONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTSset so bootstrap can copy it once). Do not write the key only into a newget_config_store(postgres_dsn=...)instance. Do not expect a later env edit to change egress on a live process.References