feat(keycardai-a2a): typed accessor for the verified caller in executors - #314
Merged
Merged
Conversation
Executors read the verified caller as keycard_user(context) -> KeycardUser | None instead of context.call_context.state["access_token"], so the token, client_id, scopes, and zone_id come back typed and the state key appears once, as a module constant shared by the builder and the accessor. The legacy access_token state entry is still written for code built against 0.4.x. Co-Authored-By: Larry Osakwe <larry@keycard.ai>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Larry-Osakwe
approved these changes
Sep 16, 2026
Larry-Osakwe
left a comment
Contributor
There was a problem hiding this comment.
Accessor half of SDK-2 as specified: one typed function, state keys as constants, legacy key still written, docs and example switched, round-trip plus None-path tests. Verified the builder change independently: Starlette's Request.user asserts without AuthenticationMiddleware and getattr's default does not catch AssertionError, so scope.get is the graceful equivalent. CI green including typecheck. Merging; feat scope releases keycardai-a2a 0.5.0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SDK-2, accessor half only.
KeycardServerCallContextBuilderstashed the verified caller under two bare string keys in a2a-sdk'sServerCallContext.state, and the README told executor authors to readcontext.call_context.state["access_token"]back out: a magic string at the seam, the pattern ECO-133 removed from FastMCP. This adds the typed read side:exported from
keycardai.a2aandkeycardai.a2a.server. It takes what an executor actually holds (RequestContext, unwrapping.call_context) or aServerCallContextdirectly, and returnsNonewhen there is no call context or the request was unauthenticated. Executors getaccess_token,client_id,scopes, andzone_idtyped for free.The two state keys are now module constants (
KEYCARD_USER_STATE_KEY,ACCESS_TOKEN_STATE_KEY) used by both the builder and the accessor, so each string appears once. The builder still writesstate["access_token"]for executors built against 0.4.x; the README documents only the accessor plus one sentence noting the legacy key exists.Also in the builder:
getattr(request, "user", None)becamerequest.scope.get("user"). Same result wheneverAuthenticationMiddlewareran (which is the only case the builder is wired for); the difference is that a request with nouserin scope now yields no state entries instead ofRequest.userraising its assertion, which the oldgetattrdid not swallow either way.Not done here, per the prompt: no grant helper, exchange wrapper, or new noun. The grant-shaped exchange surface stays with ECO-80.
Example executor and both READMEs updated to call the accessor. Tests: round trip through the builder and back out through
keycard_user(RequestContext(...))for an authenticated request (all four fields), and theNonecases (unauthenticated request, bareServerCallContext,None).Verification:
just checkandjust typecheckpassed (All checks passed!),just test-package a2a47 passed.Link to Devin session: https://app.devin.ai/sessions/bbc062cf32b548ef9c4b8b444d2ec67f
Open in Devin Desktop: https://app.devin.ai/desktop/session/bbc062cf32b548ef9c4b8b444d2ec67f?variant=devin
Requested by: @Larry-Osakwe