WP: fix(tools): evict stale credential and re-request auth on 401 in RestApiTool - #7087
Open
philipp-horstenkamp wants to merge 2 commits into
Open
philipp-horstenkamp wants to merge 2 commits into
philipp-horstenkamp wants to merge 2 commits into
Conversation
…ApiTool When an authenticated RestApiTool receives an HTTP 401 Unauthorized response from an upstream API (e.g. expired or revoked OAuth2/OIDC token where refresh is unavailable or failed), evict the cached credential from session state and return a pending status with an authorization request to prompt the user for re-authentication rather than looping indefinitely on the stale token. - Add State.__delitem__ and State.pop with delta tombstoning. - Add ToolAuthHandler.evict_credential and fix remove_credential in ToolContextCredentialStore. - Intercept HTTP 401 in RestApiTool.call when auth_scheme is configured.
Author
|
Self review is not yet as though as i would like. I plan to continue on this tomorrow. |
copybara-service Bot
pushed a commit
that referenced
this pull request
Sep 16, 2026
… credentials Fixes #7085 Credit to Philipp Horstenkamp, who reported the issue and first fixed it in PR #7087. That fix recovers a conversation started after the token went bad, but leaves the rejected credential in place for one already in progress, which is the half this CL adds; three of its negative tests are carried over here. Previously, when an OAuth2 API call returned HTTP 401 because an access token expired or was revoked, RestApiTool reported the error but left the stale credential in session state, permanently blocking subsequent tool calls. This change introduces automated recovery in ToolAuthHandler: - Reactive refresh: If a refresh token is present, automatically refreshes the access token and retries the API call once. Preserves rotated refresh tokens and prevents clobbering existing tokens during refresh. - User re-authorization: If refresh fails or is unavailable, evicts the stale credential and requests re-authorization for interactive OAuth2 flows. - Concurrency & recursion guard: Added claim_recovery() context manager to serialize recovery per credential across concurrent calls and bound retry recursion. - Budget limits: Caps re-authorization requests and reactive refreshes per credential to prevent infinite recovery loops when an API persistently returns 401. Non-401 responses refill the recovery budgets. PiperOrigin-RevId: 982498006
copybara-service Bot
pushed a commit
that referenced
this pull request
Sep 16, 2026
… credentials Fixes #7085 Credit to Philipp Horstenkamp, who reported the issue and first fixed it in PR #7087. That fix recovers a conversation started after the token went bad, but leaves the rejected credential in place for one already in progress, which is the half this CL adds; three of its negative tests are carried over here. Previously, when an OAuth2 API call returned HTTP 401 because an access token expired or was revoked, RestApiTool reported the error but left the stale credential in session state, permanently blocking subsequent tool calls. This change introduces automated recovery in ToolAuthHandler: - Reactive refresh: If a refresh token is present, automatically refreshes the access token and retries the API call once. Preserves rotated refresh tokens and prevents clobbering existing tokens during refresh. - User re-authorization: If refresh fails or is unavailable, evicts the stale credential and requests re-authorization for interactive OAuth2 flows. - Concurrency & recursion guard: Added claim_recovery() context manager to serialize recovery per credential across concurrent calls and bound retry recursion. - Budget limits: Caps re-authorization requests and reactive refreshes per credential to prevent infinite recovery loops when an API persistently returns 401. Non-401 responses refill the recovery budgets. PiperOrigin-RevId: 982498006
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.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
2. Or, if no issue exists, describe the change:
Problem:
When an authenticated
RestApiTool(e.g., fromOpenAPIToolsetusing OAuth2 or OpenID Connect) encounters anHTTP 401 Unauthorizedresponse because an access token has expired or was revoked and refresh is unavailable or fails,RestApiTool.call()catcheshttpx.HTTPStatusErrorgenerically and returns a text error string ({"error": "Tool ... execution failed ... Status Code: 401 ..."}) directly to the LLM agent.Crucially, the stale credential was not evicted from
tool_context.state. Because the dead token remains in session state across turns, subsequent user messages continue attempting to reuse the revoked token. The session enters an unrecoverable 401 loop and never re-prompts the user for authorization.Solution:
StateDeletion & Delta Tombstoning:__delitem__andpoptogoogle.adk.sessions.state.State.None) in_deltaso deletions persist across session checkpoints.KeyErrorif key does not exist to prevent phantom delta tombstones.__contains__,__getitem__, andto_dictto recognize deleted entries.Credential Eviction & Re-authentication:
ToolContextCredentialStore.remove_credential(key)to safely delete keys fromtool_context.state.ToolAuthHandler.evict_credential()which evicts only the specific stored credential key associated with the tool's authentication scheme (leaving unrelated session state intact) and triggers_request_credential().HTTP 401 Interception in
RestApiTool:RestApiTool.call(), when anHTTPStatusErrorwith status code 401 occurs on a tool with an activeauth_scheme, calltool_auth_handler.evict_credential()and return{"pending": True, "message": "Your authorization has expired. Needs your authorization to access your data."}.Testing Plan
Unit Tests:
Summary of test results (
pytest):Added tests cover:
State.__delitem__,State.pop, and delta tombstoning intests/unittests/sessions/test_state.py.tests/unittests/tools/openapi_tool/openapi_spec_parser/test_tool_auth_handler.py.tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py.Manual End-to-End (E2E) Tests:
tool_context.state.tool_context.request_credential()is dispatched and the tool returns a pending status, prompting the client/user for fresh consent rather than looping on the expired token.Checklist