fix: Handle expired tokens (#506)

* fix: Handle expired tokens

* cr
This commit is contained in:
Brace Sproul 2025-07-23 16:15:57 -07:00 • committed by GitHub
parent f6fa1fa4a4
commit 6ac4deacd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 327 additions and 182 deletions

View file

@ -6,6 +6,7 @@ import {
} from "@open-swe/shared/github/verify-user"; } from "@open-swe/shared/github/verify-user";
import { import {
API_KEY_REQUIRED_MESSAGE, API_KEY_REQUIRED_MESSAGE,
GITHUB_INSTALLATION_ID,
GITHUB_INSTALLATION_NAME, GITHUB_INSTALLATION_NAME,
GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_INSTALLATION_TOKEN_COOKIE,
GITHUB_TOKEN_COOKIE, GITHUB_TOKEN_COOKIE,
@ -143,6 +144,12 @@ export const auth = new Auth()
message: "GitHub installation name header missing", message: "GitHub installation name header missing",
}); });
} }
const installationIdHeader = request.headers.get(GITHUB_INSTALLATION_ID);
if (!installationIdHeader) {
throw new HTTPException(401, {
message: "GitHub installation ID header missing",
});
}
// We don't do anything with this token right now, but still confirm it // We don't do anything with this token right now, but still confirm it
// exists as it will cause issues later on if it's not present. // exists as it will cause issues later on if it's not present.

View file

@ -6,6 +6,7 @@ import {
GITHUB_USER_LOGIN_HEADER, GITHUB_USER_LOGIN_HEADER,
GITHUB_INSTALLATION_NAME, GITHUB_INSTALLATION_NAME,
GITHUB_PAT, GITHUB_PAT,
GITHUB_INSTALLATION_ID,
} from "@open-swe/shared/constants"; } from "@open-swe/shared/constants";
export function getDefaultHeaders(config: GraphConfig): Record<string, string> { export function getDefaultHeaders(config: GraphConfig): Record<string, string> {
@ -22,8 +23,13 @@ export function getDefaultHeaders(config: GraphConfig): Record<string, string> {
config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE]; config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE];
const githubInstallationName = const githubInstallationName =
config.configurable?.[GITHUB_INSTALLATION_NAME]; config.configurable?.[GITHUB_INSTALLATION_NAME];
const githubInstallationId = config.configurable?.[GITHUB_INSTALLATION_ID];
if (!githubInstallationTokenCookie || !githubInstallationName) { if (
!githubInstallationTokenCookie ||
!githubInstallationName ||
!githubInstallationId
) {
throw new Error("Missing required headers"); throw new Error("Missing required headers");
} }
@ -36,6 +42,7 @@ export function getDefaultHeaders(config: GraphConfig): Record<string, string> {
// Required headers // Required headers
[GITHUB_INSTALLATION_TOKEN_COOKIE]: githubInstallationTokenCookie, [GITHUB_INSTALLATION_TOKEN_COOKIE]: githubInstallationTokenCookie,
[GITHUB_INSTALLATION_NAME]: githubInstallationName, [GITHUB_INSTALLATION_NAME]: githubInstallationName,
[GITHUB_INSTALLATION_ID]: githubInstallationId,
// Optional headers // Optional headers
[GITHUB_TOKEN_COOKIE]: githubTokenCookie, [GITHUB_TOKEN_COOKIE]: githubTokenCookie,

View file

@ -1,6 +1,7 @@
import { import {
GITHUB_TOKEN_COOKIE, GITHUB_TOKEN_COOKIE,
GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_INSTALLATION_TOKEN_COOKIE,
GITHUB_INSTALLATION_ID,
} from "@open-swe/shared/constants"; } from "@open-swe/shared/constants";
import { GraphConfig } from "@open-swe/shared/open-swe/types"; import { GraphConfig } from "@open-swe/shared/open-swe/types";
import { decryptSecret } from "@open-swe/shared/crypto"; import { decryptSecret } from "@open-swe/shared/crypto";
@ -9,6 +10,7 @@ import { getGitHubPatFromConfig } from "./github-pat.js";
export function getGitHubTokensFromConfig(config: GraphConfig): { export function getGitHubTokensFromConfig(config: GraphConfig): {
githubAccessToken: string; githubAccessToken: string;
githubInstallationToken: string; githubInstallationToken: string;
installationId: string;
} { } {
if (!config.configurable) { if (!config.configurable) {
throw new Error("No configurable object found in graph config."); throw new Error("No configurable object found in graph config.");
@ -22,12 +24,20 @@ export function getGitHubTokensFromConfig(config: GraphConfig): {
const isProd = process.env.NODE_ENV === "production"; const isProd = process.env.NODE_ENV === "production";
const installationId = config.configurable[GITHUB_INSTALLATION_ID];
if (!installationId) {
throw new Error(
`Missing required ${GITHUB_INSTALLATION_ID} in configuration.`,
);
}
const githubPat = getGitHubPatFromConfig(config.configurable, encryptionKey); const githubPat = getGitHubPatFromConfig(config.configurable, encryptionKey);
if (githubPat && !isProd) { if (githubPat && !isProd) {
// check for PAT-only mode // check for PAT-only mode
return { return {
githubAccessToken: githubPat, githubAccessToken: githubPat,
githubInstallationToken: githubPat, githubInstallationToken: githubPat,
installationId,
}; };
} }
@ -49,5 +59,5 @@ export function getGitHubTokensFromConfig(config: GraphConfig): {
encryptionKey, encryptionKey,
); );
return { githubAccessToken, githubInstallationToken }; return { githubAccessToken, githubInstallationToken, installationId };
} }

View file

@ -2,43 +2,114 @@ import { Octokit } from "@octokit/rest";
import { createLogger, LogLevel } from "../logger.js"; import { createLogger, LogLevel } from "../logger.js";
import { GitHubIssue, GitHubIssueComment, GitHubPullRequest } from "./types.js"; import { GitHubIssue, GitHubIssueComment, GitHubPullRequest } from "./types.js";
import { getOpenSWELabel } from "./label.js"; import { getOpenSWELabel } from "./label.js";
import { getInstallationToken } from "@open-swe/shared/github/auth";
import { getConfig } from "@langchain/langgraph";
import { GITHUB_INSTALLATION_ID } from "@open-swe/shared/constants";
import { updateConfig } from "../update-config.js";
import { encryptSecret } from "@open-swe/shared/crypto";
const logger = createLogger(LogLevel.INFO, "GitHub-API"); const logger = createLogger(LogLevel.INFO, "GitHub-API");
async function getInstallationTokenAndUpdateConfig() {
try {
const config = getConfig();
const encryptionSecret = process.env.SECRETS_ENCRYPTION_KEY;
if (!encryptionSecret) {
throw new Error("Secrets encryption key not found");
}
const installationId = config.configurable?.[GITHUB_INSTALLATION_ID];
const appId = process.env.GITHUB_APP_ID;
const privateKey = process.env.GITHUB_APP_PRIVATE_KEY;
if (!installationId || !appId || !privateKey) {
throw new Error(
"GitHub installation ID, app ID, or private key not found",
);
}
const token = await getInstallationToken(installationId, appId, privateKey);
const encryptedToken = encryptSecret(token, encryptionSecret);
updateConfig(GITHUB_INSTALLATION_ID, encryptedToken);
return token;
} catch (e) {
logger.error("Failed to get installation token and update config", {
error: e,
});
return null;
}
}
/**
* Generic utility for handling GitHub API calls with automatic retry on 401 errors
*/
async function withGitHubRetry<T>(
operation: (token: string) => Promise<T>,
initialToken: string,
errorMessage: string,
additionalLogFields?: Record<string, any>,
numRetries = 1,
): Promise<T | null> {
try {
return await operation(initialToken);
} catch (error) {
const errorFields =
error instanceof Error
? {
name: error.name,
message: error.message,
stack: error.stack,
}
: {};
// Retry with a max retries of 2
if (errorFields && errorFields.message?.includes("401") && numRetries < 2) {
const token = await getInstallationTokenAndUpdateConfig();
if (!token) {
return null;
}
return withGitHubRetry(
operation,
token,
errorMessage,
additionalLogFields,
numRetries + 1,
);
}
logger.error(errorMessage, {
...additionalLogFields,
...(errorFields ?? { error }),
});
return null;
}
}
async function getExistingPullRequest( async function getExistingPullRequest(
owner: string, owner: string,
repo: string, repo: string,
branchName: string, branchName: string,
githubToken: string, githubToken: string,
numRetries = 1,
) { ) {
try { return withGitHubRetry(
const octokit = new Octokit({ async (token: string) => {
auth: githubToken, const octokit = new Octokit({
}); auth: token,
});
const { data: pullRequests } = await octokit.pulls.list({ const { data: pullRequests } = await octokit.pulls.list({
owner, owner,
repo, repo,
head: branchName, head: branchName,
}); });
if (pullRequests?.[0]) { return pullRequests?.[0] || null;
return pullRequests[0]; },
} githubToken,
} catch (e) { "Failed to get existing pull request",
logger.error(`Failed to get existing pull request`, { { branch: branchName, owner, repo },
branch: branchName, numRetries,
owner, );
repo,
...(e instanceof Error && {
name: e.name,
message: e.message,
stack: e.stack,
}),
});
}
return null;
} }
export async function createPullRequest({ export async function createPullRequest({
@ -162,30 +233,33 @@ export async function getIssue({
repo, repo,
issueNumber, issueNumber,
githubInstallationToken, githubInstallationToken,
numRetries = 1,
}: { }: {
owner: string; owner: string;
repo: string; repo: string;
issueNumber: number; issueNumber: number;
githubInstallationToken: string; githubInstallationToken: string;
numRetries?: number;
}): Promise<GitHubIssue | null> { }): Promise<GitHubIssue | null> {
const octokit = new Octokit({ return withGitHubRetry(
auth: githubInstallationToken, async (token: string) => {
}); const octokit = new Octokit({
auth: token,
});
try { const { data: issue } = await octokit.issues.get({
const { data: issue } = await octokit.issues.get({ owner,
owner, repo,
repo, issue_number: issueNumber,
issue_number: issueNumber, });
});
return issue; return issue;
} catch (error) { },
logger.error(`Failed to get issue`, { githubInstallationToken,
error, "Failed to get issue",
}); undefined,
return null; numRetries,
} );
} }
export async function getIssueComments({ export async function getIssueComments({
@ -194,39 +268,42 @@ export async function getIssueComments({
issueNumber, issueNumber,
githubInstallationToken, githubInstallationToken,
filterBotComments = true, filterBotComments = true,
numRetries = 1,
}: { }: {
owner: string; owner: string;
repo: string; repo: string;
issueNumber: number; issueNumber: number;
githubInstallationToken: string; githubInstallationToken: string;
filterBotComments?: boolean; filterBotComments?: boolean;
numRetries?: number;
}): Promise<GitHubIssueComment[] | null> { }): Promise<GitHubIssueComment[] | null> {
const octokit = new Octokit({ return withGitHubRetry(
auth: githubInstallationToken, async (token: string) => {
}); const octokit = new Octokit({
auth: token,
});
try { const { data: comments } = await octokit.issues.listComments({
const { data: comments } = await octokit.issues.listComments({ owner,
owner, repo,
repo, issue_number: issueNumber,
issue_number: issueNumber, });
});
if (!filterBotComments) { if (!filterBotComments) {
return comments; return comments;
} }
return comments.filter((comment) => { return comments.filter((comment) => {
return ( return (
comment.user?.type !== "Bot" || !comment.user?.name?.includes("[bot]") comment.user?.type !== "Bot" || !comment.user?.name?.includes("[bot]")
); );
}); });
} catch (error) { },
logger.error(`Failed to get issue comments`, { githubInstallationToken,
error, "Failed to get issue comments",
}); undefined,
return null; numRetries,
} );
} }
export async function createIssue({ export async function createIssue({
@ -256,9 +333,15 @@ export async function createIssue({
return issue; return issue;
} catch (error) { } catch (error) {
logger.error(`Failed to create issue`, { const errorFields =
error, error instanceof Error
}); ? {
name: error.name,
message: error.message,
stack: error.stack,
}
: { error };
logger.error(`Failed to create issue`, errorFields);
return null; return null;
} }
} }
@ -270,6 +353,7 @@ export async function updateIssue({
githubInstallationToken, githubInstallationToken,
body, body,
title, title,
numRetries = 1,
}: { }: {
owner: string; owner: string;
repo: string; repo: string;
@ -277,31 +361,33 @@ export async function updateIssue({
githubInstallationToken: string; githubInstallationToken: string;
body?: string; body?: string;
title?: string; title?: string;
numRetries?: number;
}) { }) {
if (!body && !title) { if (!body && !title) {
throw new Error("Must provide either body or title to update issue"); throw new Error("Must provide either body or title to update issue");
} }
const octokit = new Octokit({ return withGitHubRetry(
auth: githubInstallationToken, async (token: string) => {
}); const octokit = new Octokit({
auth: token,
});
try { const { data: issue } = await octokit.issues.update({
const { data: issue } = await octokit.issues.update({ owner,
owner, repo,
repo, issue_number: issueNumber,
issue_number: issueNumber, ...(body && { body }),
...(body && { body }), ...(title && { title }),
...(title && { title }), });
});
return issue; return issue;
} catch (error) { },
logger.error(`Failed to update issue`, { githubInstallationToken,
error, "Failed to update issue",
}); undefined,
return null; numRetries,
} );
} }
export async function createIssueComment({ export async function createIssueComment({
@ -310,6 +396,7 @@ export async function createIssueComment({
issueNumber, issueNumber,
body, body,
githubToken, githubToken,
numRetries = 1,
}: { }: {
owner: string; owner: string;
repo: string; repo: string;
@ -320,26 +407,28 @@ export async function createIssueComment({
* or an access token if creating a user comment. * or an access token if creating a user comment.
*/ */
githubToken: string; githubToken: string;
numRetries?: number;
}): Promise<GitHubIssueComment | null> { }): Promise<GitHubIssueComment | null> {
const octokit = new Octokit({ return withGitHubRetry(
auth: githubToken, async (token: string) => {
}); const octokit = new Octokit({
auth: token,
});
try { const { data: comment } = await octokit.issues.createComment({
const { data: comment } = await octokit.issues.createComment({ owner,
owner, repo,
repo, issue_number: issueNumber,
issue_number: issueNumber, body,
body, });
});
return comment; return comment;
} catch (error) { },
logger.error(`Failed to create issue comment`, { githubToken,
error, "Failed to create issue comment",
}); undefined,
return null; numRetries,
} );
} }
export async function updateIssueComment({ export async function updateIssueComment({
@ -348,30 +437,33 @@ export async function updateIssueComment({
commentId, commentId,
body, body,
githubInstallationToken, githubInstallationToken,
numRetries = 1,
}: { }: {
owner: string; owner: string;
repo: string; repo: string;
commentId: number; commentId: number;
body: string; body: string;
githubInstallationToken: string; githubInstallationToken: string;
numRetries?: number;
}): Promise<GitHubIssueComment | null> { }): Promise<GitHubIssueComment | null> {
const octokit = new Octokit({ return withGitHubRetry(
auth: githubInstallationToken, async (token: string) => {
}); const octokit = new Octokit({
auth: token,
});
try { const { data: comment } = await octokit.issues.updateComment({
const { data: comment } = await octokit.issues.updateComment({ owner,
owner, repo,
repo, comment_id: commentId,
comment_id: commentId, body,
body, });
});
return comment; return comment;
} catch (error) { },
logger.error(`Failed to update issue comment`, { githubInstallationToken,
error, "Failed to update issue comment",
}); undefined,
return null; numRetries,
} );
} }

View file

@ -0,0 +1,14 @@
import { getConfig } from "@langchain/langgraph";
export function updateConfig(key: string, value: unknown) {
try {
const config = getConfig();
if (!config.configurable) {
throw new Error("No configurable object found");
}
config.configurable[key] = value;
} catch {
// no-op
return;
}
}

View file

@ -46,7 +46,6 @@
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"esbuild-plugin-tailwindcss": "^2.0.1", "esbuild-plugin-tailwindcss": "^2.0.1",
"framer-motion": "^12.4.9", "framer-motion": "^12.4.9",
"jsonwebtoken": "^9.0.2",
"katex": "^0.16.21", "katex": "^0.16.21",
"langgraph-nextjs-api-passthrough": "^0.1.3", "langgraph-nextjs-api-passthrough": "^0.1.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View file

@ -4,6 +4,7 @@ import {
GITHUB_INSTALLATION_ID_COOKIE, GITHUB_INSTALLATION_ID_COOKIE,
GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_INSTALLATION_TOKEN_COOKIE,
GITHUB_INSTALLATION_NAME, GITHUB_INSTALLATION_NAME,
GITHUB_INSTALLATION_ID,
} from "@open-swe/shared/constants"; } from "@open-swe/shared/constants";
import { import {
getGitHubInstallationTokenOrThrow, getGitHubInstallationTokenOrThrow,
@ -72,6 +73,7 @@ export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, runtime } =
[GITHUB_TOKEN_COOKIE]: getGitHubAccessTokenOrThrow(req, encryptionKey), [GITHUB_TOKEN_COOKIE]: getGitHubAccessTokenOrThrow(req, encryptionKey),
[GITHUB_INSTALLATION_TOKEN_COOKIE]: installationToken, [GITHUB_INSTALLATION_TOKEN_COOKIE]: installationToken,
[GITHUB_INSTALLATION_NAME]: installationName, [GITHUB_INSTALLATION_NAME]: installationName,
[GITHUB_INSTALLATION_ID]: installationIdCookie,
}; };
}, },
}); });

View file

@ -1,4 +1,4 @@
import { getInstallationToken } from "@/utils/github"; import { getInstallationToken } from "@open-swe/shared/github/auth";
import { App } from "@octokit/app"; import { App } from "@octokit/app";
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants"; import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
import { encryptSecret } from "@open-swe/shared/crypto"; import { encryptSecret } from "@open-swe/shared/crypto";

View file

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getInstallationToken } from "../../../../../utils/github"; // Adjusted path import { getInstallationToken } from "@open-swe/shared/github/auth";
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants"; import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
const GITHUB_API_URL = "https://api.github.com"; const GITHUB_API_URL = "https://api.github.com";

View file

@ -1,9 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { import { getInstallationToken } from "@open-swe/shared/github/auth";
getInstallationToken, import { getInstallationRepositories, Repository } from "@/utils/github";
getInstallationRepositories,
Repository,
} from "@/utils/github";
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants"; import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
/** /**

View file

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getInstallationToken } from "@/utils/github"; import { getInstallationToken } from "@open-swe/shared/github/auth";
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants"; import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
/** /**

View file

@ -1,5 +1,3 @@
import * as jwt from "jsonwebtoken";
function getBaseApiUrl(): string { function getBaseApiUrl(): string {
let baseApiUrl = new URL( let baseApiUrl = new URL(
process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api", process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api",
@ -8,54 +6,6 @@ function getBaseApiUrl(): string {
return baseApiUrl; return baseApiUrl;
} }
/**
* Generates a JWT for GitHub App authentication
*/
export function generateJWT(appId: string, privateKey: string): string {
const now = Math.floor(Date.now() / 1000);
const payload = {
iat: now,
exp: now + 10 * 60,
iss: appId,
};
return jwt.sign(payload, privateKey, { algorithm: "RS256" });
}
/**
* Gets an installation access token for a GitHub App installation
*/
export async function getInstallationToken(
installationId: string,
appId: string,
privateKey: string,
): Promise<string> {
const jwtToken = generateJWT(appId, privateKey);
const response = await fetch(
`https://api.github.com/app/installations/${installationId}/access_tokens`,
{
method: "POST",
headers: {
Authorization: `Bearer ${jwtToken}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "OpenSWE-Agent",
},
},
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Failed to get installation token: ${JSON.stringify(errorData)}`,
);
}
const data = await response.json();
return data.token;
}
/** /**
* Fetches repositories accessible to a GitHub App installation * Fetches repositories accessible to a GitHub App installation
*/ */

View file

@ -21,6 +21,7 @@
"@langchain/langgraph": "^0.3.8", "@langchain/langgraph": "^0.3.8",
"@langchain/langgraph-sdk": "^0.0.95", "@langchain/langgraph-sdk": "^0.0.95",
"@octokit/rest": "^22.0.0", "@octokit/rest": "^22.0.0",
"jsonwebtoken": "^9.0.2",
"zod": "^3.25.32" "zod": "^3.25.32"
}, },
"devDependencies": { "devDependencies": {

View file

@ -10,6 +10,7 @@ export const GITHUB_TOKEN_COOKIE = "x-github-access-token";
export const GITHUB_INSTALLATION_TOKEN_COOKIE = "x-github-installation-token"; export const GITHUB_INSTALLATION_TOKEN_COOKIE = "x-github-installation-token";
export const GITHUB_INSTALLATION_NAME = "x-github-installation-name"; export const GITHUB_INSTALLATION_NAME = "x-github-installation-name";
export const GITHUB_PAT = "x-github-pat"; export const GITHUB_PAT = "x-github-pat";
export const GITHUB_INSTALLATION_ID = "x-github-installation-id";
export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-"; export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-";
export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state"; export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state";

View file

@ -0,0 +1,37 @@
import { generateJWT } from "../jwt.js";
/**
* Gets an installation access token for a GitHub App installation
*/
export async function getInstallationToken(
installationId: string,
appId: string,
privateKey: string,
): Promise<string> {
const jwtToken = generateJWT(appId, privateKey);
const response = await fetch(
`https://api.github.com/app/installations/${installationId}/access_tokens`,
{
method: "POST",
headers: {
Authorization: `Bearer ${jwtToken}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "OpenSWE-Agent",
},
},
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Failed to get installation token: ${JSON.stringify(errorData)}`,
);
}
const data = await response.json();
if (typeof data !== "object" || !data || !("token" in data)) {
throw new Error("No token returned after fetching installation token");
}
return data.token as string;
}

View file

@ -0,0 +1,16 @@
import * as jwt from "jsonwebtoken";
/**
* Generates a JWT for GitHub App authentication
*/
export function generateJWT(appId: string, privateKey: string): string {
const now = Math.floor(Date.now() / 1000);
const payload = {
iat: now,
exp: now + 10 * 60,
iss: appId,
};
return jwt.sign(payload, privateKey, { algorithm: "RS256" });
}

View file

@ -21,6 +21,7 @@ import {
GITHUB_USER_LOGIN_HEADER, GITHUB_USER_LOGIN_HEADER,
GITHUB_PAT, GITHUB_PAT,
DEFAULT_MCP_SERVERS, DEFAULT_MCP_SERVERS,
GITHUB_INSTALLATION_ID,
} from "../constants.js"; } from "../constants.js";
import { withLangGraph } from "@langchain/langgraph/zod"; import { withLangGraph } from "@langchain/langgraph/zod";
import { BaseMessage } from "@langchain/core/messages"; import { BaseMessage } from "@langchain/core/messages";
@ -417,6 +418,11 @@ export const GraphConfigurationMetadata: {
type: "hidden", type: "hidden",
}, },
}, },
[GITHUB_INSTALLATION_ID]: {
x_open_swe_ui_config: {
type: "hidden",
},
},
[GITHUB_PAT]: { [GITHUB_PAT]: {
x_open_swe_ui_config: { x_open_swe_ui_config: {
type: "hidden", type: "hidden",
@ -553,6 +559,12 @@ export const GraphConfiguration = z.object({
[GITHUB_INSTALLATION_NAME]: withLangGraph(z.string().optional(), { [GITHUB_INSTALLATION_NAME]: withLangGraph(z.string().optional(), {
metadata: GraphConfigurationMetadata[GITHUB_INSTALLATION_NAME], metadata: GraphConfigurationMetadata[GITHUB_INSTALLATION_NAME],
}), }),
/**
* The installation ID of the GitHub app the user is using to create the run.
*/
[GITHUB_INSTALLATION_ID]: withLangGraph(z.string().optional(), {
metadata: GraphConfigurationMetadata[GITHUB_INSTALLATION_ID],
}),
/** /**
* GitHub Personal Access Token. Used for simpler authentication in environments like evals * GitHub Personal Access Token. Used for simpler authentication in environments like evals
* where GitHub App installation tokens are not available or needed. * where GitHub App installation tokens are not available or needed.

View file

@ -4194,6 +4194,7 @@ __metadata:
eslint-plugin-import: ^2.27.5 eslint-plugin-import: ^2.27.5
eslint-plugin-no-instanceof: ^1.0.1 eslint-plugin-no-instanceof: ^1.0.1
eslint-plugin-prettier: ^4.2.1 eslint-plugin-prettier: ^4.2.1
jsonwebtoken: ^9.0.2
prettier: ^3.5.2 prettier: ^3.5.2
typescript: ~5.7.2 typescript: ~5.7.2
typescript-eslint: ^8.22.0 typescript-eslint: ^8.22.0
@ -4249,7 +4250,6 @@ __metadata:
eslint-plugin-react-refresh: ^0.4.18 eslint-plugin-react-refresh: ^0.4.18
framer-motion: ^12.4.9 framer-motion: ^12.4.9
globals: ^15.14.0 globals: ^15.14.0
jsonwebtoken: ^9.0.2
katex: ^0.16.21 katex: ^0.16.21
langgraph-nextjs-api-passthrough: ^0.1.3 langgraph-nextjs-api-passthrough: ^0.1.3
lodash: ^4.17.21 lodash: ^4.17.21