How Background Agents Handle Token Refresh for Model Providers and SCM Integrations
Background agents use static API keys for model providers (no refresh required) and implement an automated OAuth refresh flow for SCM providers via refreshAccessToken in packages/control-plane/src/auth/github.ts and ParticipantService.refreshToken in packages/control-plane/src/session/participant-service.ts.
The Open-Inspect system distinguishes between two authentication patterns: long-lived API keys for AI model providers and expiring OAuth tokens for source control management (SCM) platforms. While model providers never require token rotation, background agents must actively manage SCM credentials to maintain persistent sessions.
Model Provider Authentication: Static API Keys
Model providers such as OpenAI, Anthropic, and Google do not implement token refresh mechanisms. Instead, the system relies on static API keys stored in environment variables (e.g., OPENAI_API_KEY). These keys do not expire, eliminating the need for refresh logic.
According to the source code in docs/OPENAI_MODELS.md and packages/web/src/lib/model-selection.ts, the agents simply read these keys at startup and inject them into API client configurations. There is no expiration check, no refresh endpoint, and no credential rotation for model providers.
SCM Token Refresh Architecture
For SCM providers (GitHub, GitLab, Bitbucket, Linear), access tokens carry limited lifetimes. The system implements a centralized refresh strategy using Cloudflare D1 as the token store and Durable Objects for coordination.
Detecting Expiration
Before making an authenticated request, the code checks the scm_token_expires_at field in the participant's D1 record. If the current time exceeds this timestamp, the agent triggers a refresh.
The Refresh Flow
-
Initiate Refresh:
ParticipantService.refreshTokenretrieves the encryptedscm_refresh_token_encryptedfrom the D1 database. -
Exchange Token: The method calls
refreshAccessToken(defined inpackages/control-plane/src/auth/github.ts), which POSTs to the provider's OAuth token endpoint (e.g.,https://github.com/login/oauth/access_token) with the client ID, client secret, and refresh token. -
Update Storage: Upon success, the function returns a new
access_token, optional newrefresh_token, andexpires_invalue. These are encrypted and written back to theparticipantstable via an atomic compare-and-swap update to prevent race conditions. -
Fallback Handling: If the provider does not support refresh tokens (e.g., GitLab PATs) or the refresh fails, the service clears the stored credentials and forces re-authentication.
Implementation Details
The refreshAccessToken function handles the low-level OAuth exchange:
// Located in packages/control-plane/src/auth/github.ts
export async function refreshAccessToken(
refreshToken: string,
{ clientId, clientSecret, tokenUrl }: { clientId: string; clientSecret: string; tokenUrl: string }
) {
const resp = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret,
}),
});
if (!resp.ok) throw new Error(`Token refresh failed: ${resp.status}`);
const data = await resp.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
};
}
The ParticipantService orchestrates this within the session lifecycle:
// Located in packages/control-plane/src/session/participant-service.ts
import { ParticipantService } from "./session/participant-service";
async function ensureValidToken(participantId: string) {
const service = new ParticipantService();
const participant = await service.getParticipant(participantId);
if (!participant) throw new Error("Participant not found");
// Automatically refreshes if expired and returns updated record
const refreshed = await service.refreshToken(participant);
return refreshed?.scm_access_token_encrypted;
}
Key Source Files
packages/control-plane/src/auth/github.ts: ImplementsrefreshAccessTokenfor OAuth token exchange.packages/control-plane/src/session/participant-service.ts: ContainsrefreshTokenmethod for centralized token management.docs/OPENAI_MODELS.md: Documents the static API key approach for model providers.packages/web/src/lib/model-selection.ts: Handles model selection logic without token refresh concerns.
Summary
- Model providers use static API keys that never expire; no refresh logic is implemented.
- SCM providers require active token management via OAuth refresh tokens.
- The
refreshAccessTokenfunction inpackages/control-plane/src/auth/github.tshandles the OAuth exchange. ParticipantService.refreshTokeninpackages/control-plane/src/session/participant-service.tscoordinates storage updates in D1.- Failed refreshes clear credentials and force re-authentication.
Frequently Asked Questions
Do AI model providers like OpenAI require token refresh in background agents?
No. Model providers utilize static API keys (e.g., OPENAI_API_KEY) that do not expire. The agents read these keys from environment variables at startup and never refresh them.
How does the system handle expired GitHub tokens for background agents?
When scm_token_expires_at passes, the ParticipantService.refreshToken method retrieves the encrypted refresh token from D1, calls refreshAccessToken to exchange it for a new access token, and updates the database record with the new credentials and expiration time.
What happens if an SCM refresh token is revoked or invalid?
The system catches the failed refresh response, clears the stored scm_refresh_token_encrypted and scm_access_token_encrypted fields, and forces the participant through the initial OAuth consent flow to generate new tokens.
Where is the token refresh logic implemented for SCM providers?
The core refresh logic resides in packages/control-plane/src/auth/github.ts (the refreshAccessToken function) and the orchestration layer in packages/control-plane/src/session/participant-service.ts (the refreshToken method).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →