Skip to content

feat(keycardai-oauth): client_id form parameter on the client-credentials grant - #238

Merged
Larry-Osakwe merged 1 commit into
mainfrom
devin/1787791103-eco-306-client-id-cc-grant
Aug 27, 2026
Merged

feat(keycardai-oauth): client_id form parameter on the client-credentials grant#238
Larry-Osakwe merged 1 commit into
mainfrom
devin/1787791103-eco-306-client-id-cc-grant

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

ECO-306. ClientCredentialsRequest carried client_assertion / client_assertion_type but no client_id, so a federation-rule workload identity credential could not perform an as-itself grant: token federation application credentials are resolved by application ID, not by the assertion subject. TokenExchangeRequest already has the field (WorkloadIdentity.prepare_token_exchange_request sets it), which is the divergence recorded in keycard-sdk-specspecs/delegated-access/as-itself.md and the gate on ECO-240. RFC 7521 section 4.2 permits client_id alongside assertion-based client authentication.

 class ClientCredentialsRequest(BaseModel):
client_assertion: str | None = None
client_assertion_type: str | None = None
+ client_id: str | None = Field(default=None, description=...)
timeout: float | None = None

Wire encoding needed no change: build_client_credentials_http_request form-encodes request.model_dump(exclude_none=True, exclude={"timeout"}), identical to build_token_exchange_http_request. So the key is client_id, and it is omitted entirely when unset, matching token exchange by construction rather than by a parallel code path. Tests pin both behaviors so a future change to the encoder cannot silently drop or emit the key.

client_id is caller-supplied only; nothing derives it from the auth strategy. Added to the kwargs overloads and docstrings of AsyncClient.client_credentials_grant and Client.client_credentials_grant.

Scope is packages/oauth only: release automation keys off the squash commit's single package scope, so the packages/langchain lift of client_id into _client_auth_fields is a follow-up PR after this releases, with a keycardai-oauth floor bump there. The spec divergence-row flip is handled separately.

Docs

Did not regenerate docs/sdk/*.mdx. mdxify emits class and function docstrings, not pydantic field lists, and the generated keycardai-oauth-types-models.mdx / keycardai-oauth-client.mdx currently contain neither ClientCredentialsRequest nor client_credentials_grant, so this change produces no docs delta. Regenerating would instead sweep in unrelated churn (stale source line anchors) from drift that predates this branch.

Verification

packages/oauth suite passes (387 tests) via cd packages/oauth && uv run --extra test pytest tests/ (just is not installed on this box; this is the recipe body of just test-package oauth). uv run ruff check passes.

uv run ruff format is not run: the repo is not currently format-clean, and it rewrites 152 files including untouched packages. packages/oauth/src/keycardai/oauth/types/models.py was already non-format-clean on main. No formatting-only changes are included here.

Open questions

  • Should client_id also be accepted by the higher-level as-itself helpers, or is the raw grant enough until the langchain follow-up lands?
  • Is ruff format intended to be enforced repo-wide? A separate format-only pass would make just verification match the house rules.

Link to Devin session: https://app.devin.ai/sessions/bb57fb9373a747ab841119ed381ff9fa
Requested by: @Larry-Osakwe

…ials grant
Co-Authored-By: Larry Osakwe <larry@keycard.ai>
@devin-ai-integration

Copy link
Copy Markdown
ContributorAuthor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@Larry-OsakweLarry-Osakwe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified the wire behavior is carried by the model_dump(exclude_none=True) encoding shared with token exchange, and the exact-match form assertions pin both the present-when-set and absent-when-None behaviors, so no parallel encoding path was the right call.

On the open questions: no higher-level helper needed, the langchain follow-up is that layer (_client_auth_fields already lifts from the prepared request, which carries client_id from WorkloadIdentity; it just could not forward it until now). Repo-wide ruff format is a real but separate decision, since a format-only pass churns blame across the package; not blocking here.

@Larry-Osakwe
Larry-Osakwe merged commit 5b00c99 into mainAug 27, 2026
5 checks passed
@Larry-Osakwe
Larry-Osakwe deleted the devin/1787791103-eco-306-client-id-cc-grant branch August 27, 2026 00:52
Sign up for freeto 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

@Larry-Osakwe@devin-ai-keycard