# How Background Agents Handle Token Refresh for Model Providers and SCM Integrations

> Discover how background agents manage token refresh for model providers and SCM integrations. Learn about static API keys and automated OAuth refresh flows.

- Repository: [Cole Murray/background-agents](https://github.com/ColeMurray/background-agents)
- Tags: how-to-guide
- Published: 2026-07-13

---

**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`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/auth/github.ts) and `ParticipantService.refreshToken` in [`packages/control-plane/src/session/participant-service.ts`](https://github.com/ColeMurray/background-agents/blob/main/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`](https://github.com/ColeMurray/background-agents/blob/main/docs/OPENAI_MODELS.md) and [`packages/web/src/lib/model-selection.ts`](https://github.com/ColeMurray/background-agents/blob/main/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

1. **Initiate Refresh**: `ParticipantService.refreshToken` retrieves the encrypted `scm_refresh_token_encrypted` from the D1 database.

2. **Exchange Token**: The method calls `refreshAccessToken` (defined in [`packages/control-plane/src/auth/github.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/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.

3. **Update Storage**: Upon success, the function returns a new `access_token`, optional new `refresh_token`, and `expires_in` value. These are encrypted and written back to the `participants` table via an atomic compare-and-swap update to prevent race conditions.

4. **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:

```typescript
// 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:

```typescript
// 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`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/auth/github.ts)**: Implements `refreshAccessToken` for OAuth token exchange.
- **[`packages/control-plane/src/session/participant-service.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/session/participant-service.ts)**: Contains `refreshToken` method for centralized token management.
- **[`docs/OPENAI_MODELS.md`](https://github.com/ColeMurray/background-agents/blob/main/docs/OPENAI_MODELS.md)**: Documents the static API key approach for model providers.
- **[`packages/web/src/lib/model-selection.ts`](https://github.com/ColeMurray/background-agents/blob/main/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 `refreshAccessToken` function in [`packages/control-plane/src/auth/github.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/auth/github.ts) handles the OAuth exchange.
- `ParticipantService.refreshToken` in [`packages/control-plane/src/session/participant-service.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/session/participant-service.ts) coordinates 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`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/auth/github.ts) (the `refreshAccessToken` function) and the orchestration layer in [`packages/control-plane/src/session/participant-service.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/session/participant-service.ts) (the `refreshToken` method).