Skip to content

feat(buzz-agent): refresh OAuth bearer per request, not per process - #5093

Open
manuarraya wants to merge 2 commits into
block:mainfrom
manuarraya:agent-oauth-session-refresh
Open

feat(buzz-agent): refresh OAuth bearer per request, not per process#5093
manuarraya wants to merge 2 commits into
block:mainfrom
manuarraya:agent-oauth-session-refresh

Conversation

@manuarraya

Copy link
Copy Markdown

The problem

buzz-agent read its bearer from OPENAI_COMPAT_API_KEY once, at startup. An
environment 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 --user after the units had moved to
system 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: they
accepted work, failed the LLM call, retried, and dropped it.

The change

Add a session-file-backed TokenSource, selected when OPENAI_COMPAT_SESSION_FILE
is set. 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.

Refresh tokens rotate, so a naive version 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 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() required OPENAI_COMPAT_API_KEY for
openai-compat, so an agent authenticating from a session file exited during
config 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

  • RFC3339 handling is hand-rolled rather than adding 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.
  • Behaviour is unchanged when no session file is configured.
  • Covered by tests for skew boundaries, rotation persistence, concurrent refresh
    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 under
parallel execution. I confirmed they fail on unmodified origin/main at the same
rate, 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_at advanced — 0 auth errors.

🤖 Generated with Claude Code

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>
@manuarraya
manuarraya requested a review from a team as a code owner August 6, 2026 18:32
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.

1 participant