Repository navigation
Conversation
…gh a failed save A grant with no refresh token now reconnects before the host asks the store, as it does on the AES path and as usable reports. The store's renewed seal is written under the claim before the host re-seals it, so an adapter failure in that window leaves a claim that resumes from the rotated token instead of the replaced one. The adapter URL may not carry a query, the URL error moves to contracts, and the docs say the encryption key is still read.
… grants skip the store The renewal scenario now fails the host's re-seal once after the issuer rotated the token and checks that the next call resumes from the adapter's new seal. The refusals scenario connects an account without a refresh token and checks that it reconnects without a request to the adapter. The fake counts an outage as no renewal and recognises a grant by all four of its keys.
… migration The adapter paragraph had split the accounts and deployments paragraph in two. It now follows that paragraph, wrapped like its neighbours, and says that turning the setting on or off after accounts exist leaves their records unreadable. The renew docs now say the store's encrypt must keep its own placeholders, and the comments state that a seal the host cannot open needs a new sign-in and that an interrupted store renewal can wait for four store calls.
The title claimed the host's responses and traces never carry the refresh token, which the AES path also satisfies. It now names what the scenario shows: the adapter makes every renewal and keeps the rotated token, a restarted host renews from the adapter's seal, and an interrupted renewal resumes from the adapter's new seal.
… refusal rule exactly The host's own refresh accepts "expires_in":"3600" through oauth4webapi, so the store path now does too; before, the renewal failed as incompatible after the service had already rotated the refresh token. The CredentialsRenewalRefused doc now says what the classifier does: invalid_grant ends the grant only with the service's status.
The fake adapter can now pass the service's expires_in on as a string. The renewal scenario checks that such a renewal succeeds and keeps the rotated refresh token and the lifetime. The 500 after a failed reseal is asserted by status only, and the scenario titles are shorter.
A credentials store that passed the service's body as it came, with expires_in: null or a scope array, failed renewal before the resealed grant was saved, so a rotated refresh token was lost. The store path now runs the tokens through the same normalizedTokens the host applies to its own token response, and refuses a negative expires_in as invalid_response as oauth4webapi does on the host path instead of reading it as no lifetime.
…a loose token body The previous commit dropped the CredentialsError assertion from the reseal outage step without cause: the failed decrypt of the store's new seal still reaches the caller as a CredentialsError, a declared 500 on tool calls, so the step again fails on any other 500. A new last step has the service answer the refresh with expires_in: null and a scope array, which the adapter passes on as it came. The renewal must still succeed with the token the rotated refresh token bought.
This was referenced Oct 9, 2026
This branch has not been deployed
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.
Tests for #2243. This branch carries that PR's commits because a fork PR cannot target a branch in this repo. Merge that one first and the diff shrinks to the e2e files.
A fake adapter in e2e/support/credential-adapter.ts seals grants with placeholders for the refresh token and client secret. Three self-host scenarios check that renewal through it keeps the rotated refresh token across a host restart and an interrupted save, including a numeric string expires_in and a raw token body with a null member and a scope array; that its refusals and outages are classified like the service's own answers, with only invalid_grant or a missing refresh token reconnecting; and that deleting the account revokes the token the adapter holds. Each assertion went red first against a one-line mutation (the renew branch removed, the revoke branch removed, the refusal status dropped, the save point removed, the renewable guard removed, the string expires_in decode reverted, the token normalisation removed) and green with the fix.
Not covered: destination_blocked on the store path, the revoke outcomes unsupported, no_token and failed, subject_changed and challenge answers, the adapter's 30 second timeout, client_credentials grants under a store, and the cloud target.
Claude Code agents wrote and iterated the spec against the v2 tree under my direction. On a Mac mini the 20 scenarios in the files below passed together with bun run e2e:self-host --test-name, the renewal scenario taking about 38 s against its 60 s deadline, the local scenario passed with bun run e2e:local --test-name, and bun run check was green.