When Background Agents Use User OAuth Tokens vs GitHub App Tokens for Pull Request Creation
Background agents default to user OAuth tokens when the triggering user has valid stored credentials, falling back to GitHub App tokens only when user authentication is unavailable or expired.
The ColeMurray/background-agents repository implements a hierarchical authentication strategy for pull request creation that prioritizes user identity while maintaining operational continuity for automated workflows. Understanding when the system selects user OAuth tokens versus GitHub App tokens is essential for debugging permission issues and maintaining proper audit trails in CI/CD pipelines.
Token Selection Logic in SessionPullRequestService
The authentication decision occurs in packages/control-plane/src/session/pull-request-service.ts within the createPullRequest method. The service uses a nullish coalescing operator to prioritize user credentials:
// Use user OAuth if available, otherwise fall back to GitHub App token
// (e.g. sessions triggered from Linear or other integrations without user GitHub OAuth)
const prAuth = input.promptingAuth ?? appAuth;
This logic executes at lines 74-78, where input.promptingAuth represents the user's OAuth token passed through CreatePullRequestInput, and appAuth represents the GitHub App installation token generated for the session.
User OAuth Token Flow
When User Tokens Are Selected
The system utilizes a user OAuth token when the prompting user has a valid GitHub OAuth token stored in the platform. This occurs in standard UI flows where the user has authenticated through the OAuth flow and the ParticipantService has successfully retrieved their credentials.
In packages/control-plane/src/session/participant-service.ts, the service resolves the participant's stored OAuth token, refreshing it if necessary, and returns it as a SourceControlAuthContext:
const input: CreatePullRequestInput = {
title: "Add new feature",
body: "Implemented XYZ.",
repoOwner: "acme",
repoName: "demo",
promptingUserId: user.id,
promptingAuth: { authType: "oauth", token: user.oauthToken }, // User token
sessionUrl: "https://app.example.com/sessions/123",
};
const result = await pullRequestService.createPullRequest(input);
Using the user token preserves the user's GitHub identity, respects their specific repository permissions, and maintains branch protection rules and audit logs under their personal credentials.
GitHub App Token Fallback
Integration and Expiration Scenarios
The GitHub App token serves as the fallback mechanism when no user OAuth context exists. This occurs when sessions originate from external integrations like Linear, Slack, or background jobs that lack user credentials.
The App token is generated through the source control provider's generatePushAuth() method:
const pushAuth = await this.deps.sourceControlProvider.generatePushAuth();
const appAuth: SourceControlAuthContext = {
authType: "app",
token: pushAuth.token,
};
This code appears at lines 90-101 in pull-request-service.ts. The resulting token carries the GitHub App installation permissions (typically repo and contents scopes) and enables the system to push branches and create pull requests without user intervention.
When handling Linear webhooks or similar integrations without user OAuth data, the input explicitly sets promptingAuth to null:
const input: CreatePullRequestInput = {
title: "Fix typo",
body: "Corrected spelling.",
repoOwner: "acme",
repoName: "demo",
promptingUserId: "linear‑bot",
promptingAuth: null, // No user token available
sessionUrl: "https://app.example.com/sessions/456",
};
Token Refresh Failures
If a user's OAuth token expires and cannot be refreshed (for example, when no OAuth client secret is configured), the system gracefully degrades to the App token. In packages/control-plane/src/session/participant-service.ts at lines 215-221, the token refresh logic logs a warning and returns null, triggering the fallback mechanism in the pull request service.
Key Implementation Files
The authentication flow spans several critical files in the control plane:
-
packages/control-plane/src/session/pull-request-service.ts– Contains the corecreatePullRequestmethod and the token selection logic using the nullish coalescing operator. -
packages/control-plane/src/session/participant-service.ts– Handles OAuth token resolution and refresh logic, returningSourceControlAuthContextobjects or null when tokens are unavailable. -
packages/control-plane/src/auth/github.ts– Defines GitHub OAuth configuration structures and error handling patterns. -
packages/control-plane/src/source-control/github-provider.ts– ImplementsgeneratePushAuth()to create App installation tokens for repository operations. -
packages/control-plane/test/integration/create-pr.test.ts– Contains integration tests verifying both authentication paths.
Summary
- User OAuth tokens are prioritized when the triggering user has valid stored credentials, preserving user identity and permissions.
- GitHub App tokens function as a fallback for external integrations (Linear, Slack) and expired user tokens, ensuring continuous operation.
- The token selection occurs at
packages/control-plane/src/session/pull-request-service.tslines 74-78 using the??operator. - ParticipantService manages token refresh and validation at
packages/control-plane/src/session/participant-service.tslines 215-221. - The
generatePushAuth()method in the source control provider generates App tokens when user authentication is absent.
Frequently Asked Questions
When does a background agent use a user OAuth token instead of a GitHub App token?
A background agent uses a user OAuth token when the promptingAuth field in CreatePullRequestInput contains a valid SourceControlAuthContext with the user's token. This occurs when the session is triggered by a logged-in user who has previously authenticated through GitHub's OAuth flow and whose token is stored in the system.
What happens if a user's OAuth token expires during pull request creation?
If the OAuth token expires and cannot be refreshed (due to missing OAuth client secrets or refresh token invalidation), the ParticipantService logs a warning and returns null. The pull request service then falls back to the GitHub App token, allowing the PR creation to proceed under the App's installation permissions rather than failing the operation.
How does the system handle pull requests from external integrations like Linear?
For external integrations that lack user OAuth contexts, the system explicitly sets promptingAuth to null in the CreatePullRequestInput object. The createPullRequest method detects this null value and uses the GitHub App token generated by generatePushAuth() to push the branch and create the pull request, maintaining functionality without requiring user credentials.
Where is the token selection logic implemented in the codebase?
The token selection logic is implemented in packages/control-plane/src/session/pull-request-service.ts at lines 74-78, where the code evaluates const prAuth = input.promptingAuth ?? appAuth;. This single line determines whether the subsequent GitHub API calls use the user's personal OAuth token or the installation's App token.
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 →