Skip to content

fix(security): resolve gateway Bearer tokens from the credential KV - #621

Closed
cursor[bot] wants to merge 20 commits into
mainfrom
cursor/bc-bf906600-0f14-4fe8-b2d5-93af1ddb66c1-8a3f
Closed

fix(security): resolve gateway Bearer tokens from the credential KV#621
cursor[bot] wants to merge 20 commits into
mainfrom
cursor/bc-bf906600-0f14-4fe8-b2d5-93af1ddb66c1-8a3f

Conversation

@cursor

@cursor cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown

Unique slice

3cfc201 on #616 fe444d2 (which already includes #611 allowlist + trusted-trace email).

Gateway Bearer authenticators resolve from the credential KV as gateway_auth_token, admin_auth_token, and inference_auth_token. --auth-token and the split pair still win. CONTEXTUAL_ORCHESTRATOR_TOKEN / _ADMIN_TOKEN / _INFERENCE_TOKEN are copied once at process start (seed_server_auth_from_environ). A later env edit does not change who can call the API.

Local scripts printed ok: tests/test_server_auth_kv.py plus self-check, paper/API/admin/conventions, security hardening, allowlist, trusted-trace email, sales readiness, plugin artifacts, and product planning.

Merger next action

Prefer this over merging #611 or #616 alone when landing allowlist + email + token KV together. Do not merge #598 or draft #607. Rebase #613 (mode/trace) and #618 (verify/conduct) onto this tip after it lands — do not merge those stacks onto main beside this one.

Independent non-author APPROVE plus Full unit / Semgrep are required. This automation will not approve or merge.

Buyer next action

Pass --auth-token (or the split admin/inference pair), or start once with CONTEXTUAL_ORCHESTRATOR_TOKEN so bootstrap can copy it, then send that Bearer value. Do not edit the env var on a live process and expect authorization to change.

Grounding: Joint Task Force (2020) NIST SP 800-53 Rev. 5 IA-5; Grassi et al. (2017) NIST SP 800-63B.

Open in Web View Automation 

seonghobae and others added 20 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>
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>
Irreversible email [REDACTED] on opt-in traces made invoice and HR
tickets unworkable. Credential shapes stay redacted. Access control
plus audit is the PII control (NIST SP 800-122; SP 800-53 AC-3/AU-2).

Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
CONTEXTUAL_ORCHESTRATOR_TOKEN and the split admin/inference vars are
bootstrap transport via seed_server_auth_from_environ. Explicit
--auth-token still wins. A later env edit does not change who can call
the API.

Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
@cursor
cursor Bot requested a review from seonghobae August 16, 2026 16:06
@seonghobae
seonghobae marked this pull request as ready for review August 16, 2026 16:11
@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.

Unique slice (3cfc201 on #616 fe444d2)

SOUND for KV seed-once, CLI-wins, and HTTP 200-vs-401. serve_security_tokens seeds once then reads CLI values and get_credential only. ModelClient is untouched. Argparse token defaults are "", so a later env edit cannot become a CLI winner. --serve still fail-closes when no token is resolved.

Merger next action

Do not merge #611, #616, #598, or draft #607 beside this stack. Do not approve from this automation. Two Important items keep this tip from being the final token landing:

  1. Seed stores and resolve returns unstripped KV values. A Docker/K8s secret with a trailing newline copies successfully, then every Bearer call is 401 because SecurityConfig.authorize strips the presented token and compare_digest fails.
  2. docs/kv-credentials.md says a later env edit does nothing "until the next bootstrap." That is false for PostgresCredentialBackend: seed skips any non-empty key, so restart does not recopy. Rotate with --auth-token or register-credential.

This run lands those two fixes on a successor from this tip. Prefer that successor over this head for the token slice. #637 remains the sqlite/Clearfolio/TLS path landing — rebase it onto the strip tip. Independent non-author APPROVE + Full unit / Semgrep still required.

Buyer next action

Pass --auth-token (or the split admin/inference pair), or start once with CONTEXTUAL_ORCHESTRATOR_TOKEN so bootstrap can copy it, then send that Bearer value. Do not edit the env var on a live process. After the successor lands, a mounted secret with a trailing newline will authorize the stripped value.

Grounding: Joint Task Force (2020) NIST SP 800-53 Rev. 5 IA-5; Grassi et al. (2017) NIST SP 800-63B.

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

continue
raw = os.environ.get(env_name, "")
if raw.strip():
backend.set(credential_name, raw)

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.

Store raw.strip(), not raw. seed_server_auth_from_environ only checks raw.strip() before write. SecurityConfig.authorize strips the presented Bearer (server.py:172). A Docker/K8s secret or echo newline therefore seeds a value that never matches, so bootstrap "succeeds" and every call is 401. register-credential --value-stdin already strips.

the split pair) or seed the credential KV, then send that Bearer value.
"""
return (
(auth_token or "").strip() or get_credential(GATEWAY_AUTH_TOKEN) or "",

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.

resolve_server_auth_tokens strips CLI values but not KV values. Strip the get_credential(...) result too so an already-persisted newline still authorizes Bearer <token> after upgrade.

Comment thread docs/kv-credentials.md
`CONTEXTUAL_ORCHESTRATOR_TOKEN`, `CONTEXTUAL_ORCHESTRATOR_ADMIN_TOKEN`,
and `CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN` into those KV names
**once**, when the name is still empty. After that, changing the env var
does nothing until the next bootstrap. `python -m contextual_orchestrator

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.

"Until the next bootstrap" is false for a persistent KV. Seed skips any non-empty key (credentials.py:247-248). Rotating CONTEXTUAL_ORCHESTRATOR_TOKEN and restarting does not recopy; the old authenticator stays valid. Say: copy only when the KV name is empty; rotate with --auth-token or register-credential. A later env edit does not change a live process or a persisted key.

@seonghobae

Copy link
Copy Markdown
Contributor

Deferring to tip #691 for product-gate focus (Full unit + Semgrep). Reopen if this delta is not on tip substrate.

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