Skip to content

fix(security): read provider host allowlist from KV on the honesty tip - #607

Closed
cursor[bot] wants to merge 18 commits into
mainfrom
cursor/bc-43bd5124-6de2-4813-a8ae-c3a4f3ee88bf-10c1
Closed

fix(security): read provider host allowlist from KV on the honesty tip#607
cursor[bot] wants to merge 18 commits into
mainfrom
cursor/bc-43bd5124-6de2-4813-a8ae-c3a4f3ee88bf-10c1

Conversation

@cursor

@cursor cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown

Summary

Successor to #601 at 7e9a339 (seed/stop/n/logprobs, omit-real null tool.function fields, streamed-route temperature). Unique commit dfb6ca1 rebases #598 a50911f onto that tip.

ModelClient._validate_provider no longer reads os.environ.get(CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS) at request time. The allowlist is provider_egress.allowed_provider_hosts in the process KV. Env is bootstrap transport only (seed_provider_egress_from_environ at process start, and only when the KV key is empty).

Buyer next action

Seed provider_egress.allowed_provider_hosts in the KV (CSV of approved provider hostnames), or start the process with CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS set 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 — printed ok (env ignored; KV CSV; seed-once; https://api.openai.com rejected when KV is example.com)
  • python3 tests/test_security_hardening.py — printed ok
  • python3 tests/test_passthrough_request_knobs_http_honesty.py — printed ok
  • python3 tests/test_self_check.py test_paper_contracts.py test_conventions.py test_api_contract.py test_sales_readiness.py — printed ok
  • CI Full unit + Semgrep

Docs

  • docs/kv-credentials.md, docs/architecture.md, docs/papers/README.md, CHANGELOG.md, conductor/tracks.md
  • APA 7th: Joint Task Force. (2020). Security and privacy controls for information systems and organizations (NIST SP 800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5 (SC-7)
  • International Organization for Standardization. (2022). Information security, cybersecurity and privacy protection — Information security controls (ISO/IEC 27001:2022). https://www.iso.org/standard/27001 (A.8.20; cite+link, no ISO PDF)

Next loop

SSE stream passthrough for tools (still invalid_stream on this tip), then remaining __main__.py bootstrap tokens.

Open in Web View Automation 

seonghobae and others added 17 commits August 16, 2026 21:18
…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>
@cursor
cursor Bot requested a review from seonghobae August 16, 2026 15:45
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_allowlist fuzz seam. Docs now say the process-wide InMemoryConfigStore is not a detached Postgres com_config write.

Merger next action

Prefer #611. Do not merge this draft in parallel. Do not open a third ALLOWED_PROVIDER_HOSTS PR. Independent non-author APPROVE still required on #611. I am not approving this draft.

View PR

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

@seonghobae
seonghobae marked this pull request as ready for review August 16, 2026 15:59
@seonghobae
seonghobae enabled auto-merge (squash) August 16, 2026 15:59
@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Author

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Unique slice 7e9a339..6ebb1f8 — COMMENT, not APPROVE

The unique work matches the stated contract. ModelClient._validate_provider reads allowed_provider_hosts() only (orchestrator.py). seed_provider_egress_from_environ() runs from __main__.main and serve(), not from the request handler. A second seed after an env edit keeps the first KV set. Empty KV + env=example.com does not reject https://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 InMemoryConfigStore unless bootstrap calls set_runtime_config_store(). A detached get_config_store() / Postgres com_config write 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_allowlist still yields an empty set (fail-open public HTTPS).
  • test_validate_provider_ignores_process_environment_allowlist reaches live DNS for api.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, and exercise_host_allowlist. Prefer #611 over this head for allowlist-only extras.
  • #606 tip is now 0717921 (SSE tools proxy + tool_calls extra-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_hosts in the process KV (CSV of approved hostnames), or start the process with CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS set 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 --agent timed out here (authenticated: false). This review covers the unique 15-file slice only, not the inherited honesty stack.

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

Comment thread contextual_orchestrator/kv_config.py
Comment thread tests/test_provider_host_allowlist_kv.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_allowlist hits live getaddrinfo("api.openai.com").
  • Process-local InMemoryConfigStore is not the credential/Postgres KV; there is no register-config write path.
  • docs/papers/README.md overclaims 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.

Open in Web View Automation 

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, ""):

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/papers/README.md
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

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.

@seonghobae seonghobae closed this Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants