Skip to content

feat(keycardai-a2a): typed accessor for the verified caller in executors - #314

Merged
Larry-Osakwe merged 1 commit into
mainfrom
devin/1789585442-a2a-keycard-user
Sep 16, 2026
Merged

Larry-Osakwe merged 1 commit into
mainfrom
devin/1789585442-a2a-keycard-user

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

SDK-2, accessor half only. KeycardServerCallContextBuilder stashed the verified caller under two bare string keys in a2a-sdk's ServerCallContext.state, and the README told executor authors to read context.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:

def keycard_user(context: RequestContext | ServerCallContext | None) -> KeycardUser | None

exported from keycardai.a2a and keycardai.a2a.server. It takes what an executor actually holds (RequestContext, unwrapping .call_context) or a ServerCallContext directly, and returns None when there is no call context or the request was unauthenticated. Executors get access_token, client_id, scopes, and zone_id typed 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 writes state["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) became request.scope.get("user"). Same result whenever AuthenticationMiddleware ran (which is the only case the builder is wired for); the difference is that a request with no user in scope now yields no state entries instead of Request.user raising its assertion, which the old getattr did 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 the None cases (unauthenticated request, bare ServerCallContext, None).

Verification: just check and just typecheck passed (All checks passed!), just test-package a2a 47 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

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>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 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-Osakwe Larry-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.

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.

@Larry-Osakwe
Larry-Osakwe merged commit 3a3ede7 into main Sep 16, 2026
14 checks passed
@Larry-Osakwe
Larry-Osakwe deleted the devin/1789585442-a2a-keycard-user branch September 16, 2026 20:24
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