feat(buzz-agent): refresh OAuth bearer per request, not per process - #5093
Open
manuarraya wants to merge 2 commits into
Open
feat(buzz-agent): refresh OAuth bearer per request, not per process#5093manuarraya wants to merge 2 commits into
manuarraya wants to merge 2 commits into
Conversation
buzz-agent took its bearer from OPENAI_COMPAT_API_KEY, read once at startup. An env var cannot be changed from outside a running process, so restarting the agent was the only way to pick up a renewed token — restarts were the refresh mechanism, not a maintenance detail. That makes uptime and validity the same variable. A fleet running against an OIDC provider whose access tokens live ~6h has to be restarted more often than it runs, and every scheduler bug becomes an auth outage. One such bug (a reload unit calling `systemctl --user` after the units had moved to system scope) left six agents running ~10 hours on a token that died after six. Every unit reported `active (running)` the whole time: they accepted work, failed the LLM call, retried, and dropped it. Add GrokSessionTokenSource, selected when a session file is configured. It re-reads the session on every request and exchanges the refresh_token when the access token is within 10 minutes of expiry, so validity no longer depends on process lifetime. This is the shape OpenClaw and Hermes already use, and the reason neither logs out. Refresh tokens rotate, so a naive implementation loses the credential when two agents refresh at once — the second writes a refresh_token the provider has already invalidated. Writes take a cross-process O_EXCL lock with stale-steal, and both entry points re-check under it (the token is often already fresh, making the refresh a no-op). Sharing one session file across agents is what makes rotation safe: N private copies drift until each holds a refresh_token that has been rotated away. RFC3339 handling is hand-rolled rather than pulling in chrono for two fields, and validates ranges — an earlier version parsed 2026-13-99T99:99:99Z happily and would have treated a corrupt session as valid forever. OPENAI_COMPAT_API_KEY becomes optional when a session file is set; the session file is then the credential. Covered by 10 tests: skew boundaries, rotation persistence, concurrent refresh under the lock, stale-lock recovery, and malformed/out-of-range timestamps. Signed-off-by: Manu Arraya <mmarraya31@gmail.com>
…dential resolve_provider() required OPENAI_COMPAT_API_KEY for the openai-compat provider, so an agent authenticating from a session file exited during config parsing — before build_token_source() could select the session-backed source. The session-file path was unreachable in exactly the deployment it was written for. A session file is an alternative credential, not an extra. Accept either, and name both in the error when neither is present. Two existing assertions matched the old sentence verbatim; they now assert the invariant (which credential is missing) rather than the phrasing. Signed-off-by: Manu Arraya <mmarraya31@gmail.com>
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.
The problem
buzz-agentread its bearer fromOPENAI_COMPAT_API_KEYonce, at startup. Anenvironment variable cannot be changed from outside a running process, so the only
way to pick up a renewed token was to restart the agent — restarts were the
refresh mechanism, not a maintenance detail.
That makes uptime and credential validity the same variable. Against a provider
whose access tokens live ~6h, a long-running fleet has to be restarted more often
than it runs, and every scheduler bug becomes an auth outage.
One did. A reload unit kept calling
systemctl --userafter the units had moved tosystem scope, so it silently matched nothing. Six agents ran ~10 hours on a token
that died after six. Every unit reported
active (running)the whole time: theyaccepted work, failed the LLM call, retried, and dropped it.
The change
Add a session-file-backed
TokenSource, selected whenOPENAI_COMPAT_SESSION_FILEis set. It re-reads the session on every request and exchanges the
refresh_tokenwhen the access token is within 10 minutes of expiry, so validity no longer depends
on process lifetime.
Refresh tokens rotate, so a naive version loses the credential when two agents
refresh at once — the second writes a
refresh_tokenthe provider has alreadyinvalidated. Writes take a cross-process
O_EXCLlock with stale-steal, and bothentry points re-check under it (the token is usually already fresh, making the
refresh a no-op). Sharing one session file across agents is what makes rotation
safe: N private copies drift until each holds a token that has been rotated away.
Second commit:
resolve_provider()requiredOPENAI_COMPAT_API_KEYforopenai-compat, so an agent authenticating from a session file exited duringconfig parsing — before
build_token_source()could ever select the new source.The feature was unreachable in exactly the deployment it was written for. A session
file is an alternative credential, not an extra.
Notes for review
chronofor two fields, andvalidates ranges — an earlier version parsed
2026-13-99T99:99:99Zhappily andwould have treated a corrupt session as valid forever.
under the lock, stale-lock recovery, and malformed/out-of-range timestamps.
Testing
cargo test -p buzz-agent— 426 passing.Two integration tests (
steer_folds_into_active_turn_without_cancelling,cancelled_turn_with_usage_emits_notification_before_response) are flaky underparallel execution. I confirmed they fail on unmodified
origin/mainat the samerate, so they are pre-existing and unrelated to this change.
Also verified in production: with the access token forced to a dead value, an agent
answered a mention in 3s and the session's
expires_atadvanced — 0 auth errors.🤖 Generated with Claude Code