fix(security): read provider host allowlist from KV on the honesty tip - #607
fix(security): read provider host allowlist from KV on the honesty tip#607cursor[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>
…allowlist A second seed_provider_egress_from_environ after the env var changes must not widen the KV set. _validate_provider must accept a public host when only the process environment lists a different allowlist. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Review (draft #607)
This draft is the same unique allowlist rebase onto #601 (
7e9a339) as #611, with only the seed-twice and request-path env-ignore tests.#611 (
c91629e) is the ready landing vehicle: same rebase, plus process-store isolation tests, seed_runtime_lock, listed-host accept, and a_parse_host_allowlistfuzz seam. Docs now say the process-wideInMemoryConfigStoreis not a detached Postgrescom_configwrite.Merger next action
Prefer #611. Do not merge this draft in parallel. Do not open a third
ALLOWED_PROVIDER_HOSTSPR. Independent non-author APPROVE still required on #611. I am not approving this draft.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.
Stale comment
Unique slice
7e9a339..6ebb1f8— COMMENT, not APPROVEThe unique work matches the stated contract.
ModelClient._validate_providerreadsallowed_provider_hosts()only (orchestrator.py).seed_provider_egress_from_environ()runs from__main__.mainandserve(), not from the request handler. A second seed after an env edit keeps the first KV set. Empty KV + env=example.comdoes not rejecthttps://api.openai.com.Do not merge this 157-file honesty carrier in parallel with #598, #605, #608, or #611. Independent non-author APPROVE + Full unit/Semgrep are still required. This automation will not APPROVE or merge.
Residual (non-blocking)
- The request-time store is process-local
InMemoryConfigStoreunless bootstrap callsset_runtime_config_store(). A detachedget_config_store()/ Postgrescom_configwrite does not constrain egress. Docs already call this the process KV.- A whitespace-only stored value is treated as populated, so later env seed is skipped, while
_parse_host_allowliststill yields an empty set (fail-open public HTTPS).test_validate_provider_ignores_process_environment_allowlistreaches live DNS forapi.openai.com. Offline/DNS failure looks like a product regression.- Sibling #611 (
c91629e) is the same unique fix plus seed lock, process-store isolation tests, andexercise_host_allowlist. Prefer #611 over this head for allowlist-only extras.- #606 tip is now
0717921(SSE tools proxy +tool_callsextra-key / mode / trace fail-closed) and still lacks the allowlist. Next landing vehicle: rebase the #611 extras onto #606. Do not open a third allowlist-only PR.Buyer next action
Seed
provider_egress.allowed_provider_hostsin the process KV (CSV of approved hostnames), or start the process withCONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTSset so bootstrap can copy it once. Changing the env var on a running process no longer changes egress policy.CodeRabbit CLI 0.7.3
auth login --agenttimed out here (authenticated: false). This review covers the unique 15-file slice only, not the inherited honesty stack.Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Unique slice 7e9a339..6ebb1f8 — COMMENT, not APPROVE
The unique work still matches the claim: ModelClient._validate_provider reads allowed_provider_hosts() only; os.getenv is not on the request path; env seed is bootstrap-only (__main__.py, serve()). Empty KV remains the same fail-open extra-hostname filter as 7e9a339 (public HTTPS + non-private DNS still apply). That is not a new hole.
Do not merge this PR. The allowlist landing vehicle is #611 (c91629e on the same #601 tip) — it adds store isolation, seed lock, listed-host accept, and a parse fuzz seam. Honesty-stack landing is #613 (mode/trace). SSE tools stay on #606. Do not land #607 and #611 in parallel.
Residuals on this slice (unchanged; threads left open):
- Whitespace-only KV is treated as populated, so a later env seed is skipped while the parsed set is empty.
test_validate_provider_ignores_process_environment_allowlisthits livegetaddrinfo("api.openai.com").- Process-local
InMemoryConfigStoreis not the credential/Postgres KV; there is noregister-configwrite path. docs/papers/README.mdoverclaims SC-7 as if the control were on by default.
Next buyer action: review #611 for the allowlist read path. After #613+#611, seed remaining __main__.py bootstrap tokens into KV, then persist provider_egress.allowed_provider_hosts on the same backend as credentials.
Independent non-author APPROVE + Full unit/Semgrep required. This automation will not approve or merge.
Sent by Cursor Automation: Fix Issues
| Request-time validation must call :func:`allowed_provider_hosts`. | ||
| """ | ||
| existing = get_runtime_config(PROVIDER_EGRESS_CATEGORY, ALLOWED_PROVIDER_HOSTS_KEY, None) | ||
| if existing not in (None, ""): |
There was a problem hiding this comment.
Residual, not blocking: existing not in (None, "") treats whitespace-only KV as populated, so a later env seed is skipped while _parse_host_allowlist still yields an empty set (fail-open public HTTPS). Treat stripped-empty as empty, or document that operators must write a real CSV. #611 holds _runtime_lock across this read+write — cherry-pick that when rebasing onto #613/#606.
| "public_openai_agent", "gpt-example", "https://api.openai.com/v1", "MODEL_KEY" | ||
| ) | ||
| try: | ||
| client._validate_provider(public_agent) |
There was a problem hiding this comment.
This env-ignore case is the right request-path lock, but _validate_provider still calls socket.getaddrinfo("api.openai.com"). An offline runner fails for DNS, not for an env leak. Keep the assertion that success is required; add a mocked resolver or accept only the allowlisted failure token so CI stays offline. Also add KV-restrict + env-widen on _validate_provider so KV-wins is proven at the request path, not only at the helper.
| National Institute of Standards and Technology. | ||
| https://doi.org/10.6028/NIST.SP.800-53r5 | ||
| Control **SC-7** (Boundary Protection) is the request-time hostname | ||
| allowlist: only approved provider hosts may receive a credentialed egress |
There was a problem hiding this comment.
SC-7 is overclaimed here. Empty KV still means “no extra hostname filter” — any public HTTPS agent host can receive a credentialed call until an operator seeds provider_egress.allowed_provider_hosts. docs/library_research.md already says that. Next action: say the control is operator-opt-in, then point buyers at the seed step.
| if _runtime_store is None: | ||
| with _runtime_lock: | ||
| if _runtime_store is None: | ||
| _runtime_store = InMemoryConfigStore() |
There was a problem hiding this comment.
Request-time allowlist lives in a process-local InMemoryConfigStore created here. A get_config_store() / Postgres com_config write does not constrain egress, and there is no register-config CLI. Buyer next action after #611 lands: persist this key on the same KV backend as credentials.
|
Superseded by #611. The current review on this PR explicitly identifies #611 as the ready landing vehicle for the same KV allowlist rebase and notes that #611 adds the stronger process-store isolation, seed lock, listed-host acceptance, and parser fuzz contracts. Keeping #607 open would preserve a weaker duplicate lane. No checks, reviews, or approvals transfer. |


Summary
Successor to #601 at
7e9a339(seed/stop/n/logprobs, omit-real nulltool.functionfields, streamed-route temperature). Unique commitdfb6ca1rebases #598a50911fonto that tip.ModelClient._validate_providerno longer readsos.environ.get(CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS)at request time. The allowlist isprovider_egress.allowed_provider_hostsin the process KV. Env is bootstrap transport only (seed_provider_egress_from_environat process start, and only when the KV key is empty).Buyer next action
Seed
provider_egress.allowed_provider_hostsin the KV (CSV of approved provider hostnames), or start the process withCONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTSset so bootstrap can copy it once. Changing the env var on a running process no longer changes egress policy.Do not merge #598, #128, #599, #605, or older honesty-stack heads in parallel. Prefer this head over #598 (same unique slice on a stale substrate). Independent non-author APPROVE + Full unit/Semgrep are still required. This automation will not APPROVE or merge.
Test plan
python3 tests/test_provider_host_allowlist_kv.py— printedok(env ignored; KV CSV; seed-once;https://api.openai.comrejected when KV isexample.com)python3 tests/test_security_hardening.py— printedokpython3 tests/test_passthrough_request_knobs_http_honesty.py— printedokpython3 tests/test_self_check.pytest_paper_contracts.pytest_conventions.pytest_api_contract.pytest_sales_readiness.py— printedokDocs
docs/kv-credentials.md,docs/architecture.md,docs/papers/README.md,CHANGELOG.md,conductor/tracks.mdNext loop
SSE stream passthrough for tools (still
invalid_streamon this tip), then remaining__main__.pybootstrap tokens.