diff --git a/Common/Server/API/AIAgentDataAPI.ts b/Common/Server/API/AIAgentDataAPI.ts index 770aa32a59..eed9cb753c 100644 --- a/Common/Server/API/AIAgentDataAPI.ts +++ b/Common/Server/API/AIAgentDataAPI.ts @@ -458,10 +458,18 @@ export default class AIAgentDataAPI { ); } - // Generate GitHub installation access token + // Generate GitHub installation access token with write permissions + // Required for AI Agent to push branches and create pull requests const tokenData: GitHubInstallationToken = await GitHubUtil.getInstallationAccessToken( codeRepository.gitHubAppInstallationId, + { + permissions: { + contents: "write", // Required for pushing branches + pull_requests: "write", // Required for creating PRs + metadata: "read", // Required for reading repository metadata + }, + }, ); const repositoryUrl: string = `https://github.com/${codeRepository.organizationName}/${codeRepository.repositoryName}.git`; diff --git a/Common/Server/Utils/CodeRepository/GitHub/GitHub.ts b/Common/Server/Utils/CodeRepository/GitHub/GitHub.ts index 60f60aac92..db193f7ad2 100644 --- a/Common/Server/Utils/CodeRepository/GitHub/GitHub.ts +++ b/Common/Server/Utils/CodeRepository/GitHub/GitHub.ts @@ -335,11 +335,20 @@ export default class GitHubUtil extends HostedCodeRepository { /** * Gets an installation access token for a GitHub App installation * @param installationId - The GitHub App installation ID + * @param options - Optional configuration for the token + * @param options.permissions - Specific permissions to request for the token * @returns Installation token and expiration date */ @CaptureSpan() public static async getInstallationAccessToken( installationId: string, + options?: { + permissions?: { + contents?: "read" | "write"; + pull_requests?: "read" | "write"; + metadata?: "read"; + }; + }, ): Promise { const jwt: string = GitHubUtil.generateAppJWT(); @@ -347,10 +356,17 @@ export default class GitHubUtil extends HostedCodeRepository { `https://api.github.com/app/installations/${installationId}/access_tokens`, ); + // Build request data with optional permissions + const requestData: JSONObject = {}; + + if (options?.permissions) { + requestData["permissions"] = options.permissions; + } + const result: HTTPErrorResponse | HTTPResponse = await API.post( { url: url, - data: {}, + data: requestData, headers: { Authorization: `Bearer ${jwt}`, Accept: "application/vnd.github+json", @@ -360,6 +376,22 @@ export default class GitHubUtil extends HostedCodeRepository { ); if (result instanceof HTTPErrorResponse) { + // Check if this is a permission error and provide helpful message + const errorMessage: string = + (result.data as JSONObject)?.["message"]?.toString() || ""; + + if ( + errorMessage.includes("permissions") || + result.statusCode === 403 || + result.statusCode === 422 + ) { + logger.error( + `GitHub App permission error: ${errorMessage}. ` + + `Please ensure the GitHub App is configured with the required permissions ` + + `(contents: write, pull_requests: write, metadata: read) in the GitHub App settings.`, + ); + } + throw result; } diff --git a/Docs/Content/self-hosted/github-integration.md b/Docs/Content/self-hosted/github-integration.md index 30be2f4181..e28d004151 100644 --- a/Docs/Content/self-hosted/github-integration.md +++ b/Docs/Content/self-hosted/github-integration.md @@ -34,7 +34,7 @@ In the "Permissions & events" section, configure the following permissions: | Permission | Access Level | Purpose | |------------|--------------|---------| -| Contents | Read | Read repository files and code | +| Contents | Read & Write | Read repository files, push branches (required for AI Agent) | | Pull requests | Read & Write | Create and manage pull requests | | Issues | Read & Write | Read and comment on issues | | Commit statuses | Read | Check build/CI status |