Skip to content

feat: accept an OAuth bearer token, not only an API key - #17

Merged
hugo8xx merged 1 commit into
mainfrom
feat/bearer-credential
Aug 26, 2026
Merged

feat: accept an OAuth bearer token, not only an API key#17
hugo8xx merged 1 commit into
mainfrom
feat/bearer-credential

Conversation

@hugo8xx

@hugo8xx hugo8xx commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

The client could only send X-API-Key. The API has accepted Authorization: Bearer for a while — it is how the ChatGPT path authenticates — so the SDK was the piece that could not speak a credential the server already understood.

Why it blocks the remote transport

A remote MCP server receives the caller's OAuth token and should hand it straight to the API, so the token is verified in one place rather than the crypto and the JWKS living in two.

Without this it cannot. Putting a JWT in api_key sends it as X-API-Key, where the engine looks it up as a key, misses, and 401s — before the bearer path is ever reached. It fails pointing at the wrong thing.

After

Khwan(api_key="kwk_live_…")       # X-API-Key          — the process is yours
Khwan(bearer_token=access_token)  # Authorization: Bearer — you act for a user

Both clients, sync and async. core and user_id are unchanged and orthogonal — brain selection has nothing to do with which credential opened the door.

Exactly one is required. Passing both is refused rather than resolved: there is no obvious winner, and silently preferring one is how a service ends up authenticating as itself when it meant to act for a user.

Tests

10 new, pinning the header each credential produces — that difference is invisible until a request fails somewhere unrelated. Including the one that matters most: a bearer client sends no X-API-Key at all.

The existing 35 pass unchanged; nothing about api_key moved.

README gains a short "Two ways to authenticate" section, since a credential nobody knows about is half-shipped.

Unblocks the streamable_http_app() mount in khwan-mcp — the last piece of Phase 1.

The client could only send `X-API-Key`. The API has accepted
`Authorization: Bearer` for a while — it is how the ChatGPT path authenticates —
so the SDK was the piece that could not speak a credential the server already
understood.

That gap blocks anything acting on a user's behalf. A remote MCP server receives
the caller's OAuth token and should hand it straight to the API, so the token is
verified in one place instead of the crypto and the JWKS living in two. Without
this it cannot: putting a JWT in `api_key` sends it as `X-API-Key`, where it is
looked up as a key, misses, and 401s before the bearer path is reached. It fails
in a way that points at the wrong thing.

Both clients take `bearer_token=`, and exactly one credential is required.
Refusing both rather than picking a winner: passing an api_key and a token
together has no obvious right answer, and silently preferring one is how a
service ends up authenticating as itself when it meant to act for a user.

`core` and `user_id` are unchanged and orthogonal — brain selection has nothing
to do with which credential opened the door.

Ten tests pin the header each credential produces, because the difference is
invisible until a request fails somewhere unrelated. The existing 35 still pass;
nothing about `api_key` moved.
@hugo8xx
hugo8xx merged commit ede8560 into main Aug 26, 2026
2 checks passed
hugo8xx added a commit that referenced this pull request Aug 26, 2026
…led from (#18)

khwan-mcp installs khwan from PyPI, not from this repository, so merging #17
changed nothing it can reach: the MCP layer still cannot call
`Khwan(bearer_token=…)` until a version carrying it is published.

The same shape as the engine pin between khwan-api and khwan-worker that came up
twice today — merged is not deployed, and the boundary is a package registry
rather than a git ref.

Additive and backwards compatible: `api_key` behaves exactly as before, and the
existing 35 tests pass untouched.
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.

1 participant