mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 23:02:12 +00:00
parent
f6fa1fa4a4
commit
6ac4deacd6
18 changed files with 327 additions and 182 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
apps/open-swe/src/utils/update-config.ts
Normal file
14
apps/open-swe/src/utils/update-config.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
37
packages/shared/src/github/auth.ts
Normal file
37
packages/shared/src/github/auth.ts
Normal 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;
|
||||||
|
}
|
||||||
16
packages/shared/src/jwt.ts
Normal file
16
packages/shared/src/jwt.ts
Normal 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" });
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue