From ba0690caa01b0dcdcd45dd9876a9be589f892e48 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Mon, 30 Jun 2025 16:43:09 -0600 Subject: [PATCH] feat: trigger runs from GH issues (#324) * feat: trigger runs from GH issues * cr * working * cr * done * cr * cr * cr * cr * diff label for dev vs prod * cr * cr * cr --- apps/open-swe/package.json | 4 + apps/open-swe/scripts/check-dev-server.ts | 7 + .../graphs/manager/nodes/classify-message.ts | 6 + .../manager/nodes/create-new-session.ts | 6 + .../src/graphs/manager/nodes/start-planner.ts | 6 + .../src/graphs/planner/nodes/proposed-plan.ts | 6 + apps/open-swe/src/routes/app.ts | 6 + .../src/routes/github/issue-webhook.ts | 195 ++++++++++++++++++ apps/open-swe/src/security/auth.ts | 78 ++++--- apps/open-swe/src/security/github.ts | 58 ++++++ apps/open-swe/src/security/utils.ts | 28 +++ apps/open-swe/src/utils/github-app.ts | 46 +++++ apps/open-swe/src/utils/github-tokens.ts | 11 +- apps/open-swe/src/utils/github/api.ts | 3 +- apps/open-swe/src/utils/github/label.ts | 6 + .../src/app/(v2)/chat/[thread_id]/page.tsx | 7 +- apps/web/src/app/(v2)/chat/page.tsx | 5 +- apps/web/src/app/(v2)/chat/threads/page.tsx | 5 +- apps/web/src/components/v2/default-view.tsx | 8 +- apps/web/src/components/v2/terminal-input.tsx | 10 +- apps/web/src/components/v2/thread-view.tsx | 41 ++-- apps/web/src/providers/Stream.tsx | 10 +- langgraph.json | 3 + packages/shared/src/constants.ts | 7 + packages/shared/src/github/verify-user.ts | 28 +++ packages/shared/src/open-swe/types.ts | 26 +++ yarn.lock | 182 ++++++++++++++++ 27 files changed, 697 insertions(+), 101 deletions(-) create mode 100644 apps/open-swe/src/routes/app.ts create mode 100644 apps/open-swe/src/routes/github/issue-webhook.ts create mode 100644 apps/open-swe/src/security/github.ts create mode 100644 apps/open-swe/src/security/utils.ts create mode 100644 apps/open-swe/src/utils/github-app.ts create mode 100644 apps/open-swe/src/utils/github/label.ts diff --git a/apps/open-swe/package.json b/apps/open-swe/package.json index f5436a50..232c9770 100644 --- a/apps/open-swe/package.json +++ b/apps/open-swe/package.json @@ -29,9 +29,13 @@ "@langchain/langgraph": "^0.3.3", "@langchain/langgraph-sdk": "^0.0.85", "@langchain/openai": "^0.5.10", + "@octokit/app": "^16.0.1", + "@octokit/core": "^7.0.2", "@octokit/rest": "^22.0.0", + "@octokit/webhooks": "^14.0.2", "@open-swe/shared": "*", "diff": "^8.0.1", + "hono": "^4.8.3", "langchain": "^0.3.26", "langsmith": "^0.3.29", "uuid": "^11.0.5", diff --git a/apps/open-swe/scripts/check-dev-server.ts b/apps/open-swe/scripts/check-dev-server.ts index 2a0b2daa..10c1d021 100644 --- a/apps/open-swe/scripts/check-dev-server.ts +++ b/apps/open-swe/scripts/check-dev-server.ts @@ -2,6 +2,12 @@ import { spawn } from "child_process"; import * as path from "path"; +const REQUIRED_ENV = { + GITHUB_APP_ID: "test", + GITHUB_APP_PRIVATE_KEY: "test", + GITHUB_WEBHOOK_SECRET: "test", +}; + /** * Checks if the development server starts successfully. * This script starts the dev server and monitors the output for 30 seconds @@ -18,6 +24,7 @@ function checkDevServer(): Promise { cwd: targetCwd, shell: true, stdio: "pipe", + env: REQUIRED_ENV, }); let errorDetected = false; diff --git a/apps/open-swe/src/graphs/manager/nodes/classify-message.ts b/apps/open-swe/src/graphs/manager/nodes/classify-message.ts index 6be0c2c2..32dffe90 100644 --- a/apps/open-swe/src/graphs/manager/nodes/classify-message.ts +++ b/apps/open-swe/src/graphs/manager/nodes/classify-message.ts @@ -7,6 +7,8 @@ import { createLangGraphClient } from "../../../utils/langgraph-client.js"; import { GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_TOKEN_COOKIE, + GITHUB_USER_ID_HEADER, + GITHUB_USER_LOGIN_HEADER, } from "@open-swe/shared/constants"; import { BaseMessage, @@ -221,6 +223,10 @@ export async function classifyMessage( [GITHUB_TOKEN_COOKIE]: config.configurable?.[GITHUB_TOKEN_COOKIE] ?? "", [GITHUB_INSTALLATION_TOKEN_COOKIE]: config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE] ?? "", + [GITHUB_USER_ID_HEADER]: + config.configurable?.[GITHUB_USER_ID_HEADER] ?? "", + [GITHUB_USER_LOGIN_HEADER]: + config.configurable?.[GITHUB_USER_LOGIN_HEADER] ?? "", }, }); diff --git a/apps/open-swe/src/graphs/manager/nodes/create-new-session.ts b/apps/open-swe/src/graphs/manager/nodes/create-new-session.ts index 4e877eec..2e20efd1 100644 --- a/apps/open-swe/src/graphs/manager/nodes/create-new-session.ts +++ b/apps/open-swe/src/graphs/manager/nodes/create-new-session.ts @@ -8,6 +8,8 @@ import { createIssueTitleAndBodyFromMessages } from "../utils/generate-issue-fie import { GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_TOKEN_COOKIE, + GITHUB_USER_ID_HEADER, + GITHUB_USER_LOGIN_HEADER, } from "@open-swe/shared/constants"; import { createLangGraphClient } from "../../../utils/langgraph-client.js"; import { createIssue } from "../../../utils/github/api.js"; @@ -75,6 +77,10 @@ ${ISSUE_CONTENT_CLOSE_TAG}`, [GITHUB_TOKEN_COOKIE]: config.configurable?.[GITHUB_TOKEN_COOKIE] ?? "", [GITHUB_INSTALLATION_TOKEN_COOKIE]: config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE] ?? "", + [GITHUB_USER_ID_HEADER]: + config.configurable?.[GITHUB_USER_ID_HEADER] ?? "", + [GITHUB_USER_LOGIN_HEADER]: + config.configurable?.[GITHUB_USER_LOGIN_HEADER] ?? "", }, }); diff --git a/apps/open-swe/src/graphs/manager/nodes/start-planner.ts b/apps/open-swe/src/graphs/manager/nodes/start-planner.ts index 83980422..05d8f1e5 100644 --- a/apps/open-swe/src/graphs/manager/nodes/start-planner.ts +++ b/apps/open-swe/src/graphs/manager/nodes/start-planner.ts @@ -8,6 +8,8 @@ import { createLangGraphClient } from "../../../utils/langgraph-client.js"; import { GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_TOKEN_COOKIE, + GITHUB_USER_ID_HEADER, + GITHUB_USER_LOGIN_HEADER, } from "@open-swe/shared/constants"; import { createLogger, LogLevel } from "../../../utils/logger.js"; import { getBranchName } from "../../../utils/github/git.js"; @@ -27,6 +29,10 @@ export async function startPlanner( [GITHUB_TOKEN_COOKIE]: config.configurable?.[GITHUB_TOKEN_COOKIE] ?? "", [GITHUB_INSTALLATION_TOKEN_COOKIE]: config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE] ?? "", + [GITHUB_USER_ID_HEADER]: + config.configurable?.[GITHUB_USER_ID_HEADER] ?? "", + [GITHUB_USER_LOGIN_HEADER]: + config.configurable?.[GITHUB_USER_LOGIN_HEADER] ?? "", }, }); diff --git a/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts b/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts index 951e302a..668f04c8 100644 --- a/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts +++ b/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts @@ -12,6 +12,8 @@ import { getUserRequest } from "../../../utils/user-request.js"; import { GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_TOKEN_COOKIE, + GITHUB_USER_ID_HEADER, + GITHUB_USER_LOGIN_HEADER, PLAN_INTERRUPT_ACTION_TITLE, PLAN_INTERRUPT_DELIMITER, } from "@open-swe/shared/constants"; @@ -70,6 +72,10 @@ export async function interruptProposedPlan( [GITHUB_TOKEN_COOKIE]: config.configurable?.[GITHUB_TOKEN_COOKIE] ?? "", [GITHUB_INSTALLATION_TOKEN_COOKIE]: config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE] ?? "", + [GITHUB_USER_ID_HEADER]: + config.configurable?.[GITHUB_USER_ID_HEADER] ?? "", + [GITHUB_USER_LOGIN_HEADER]: + config.configurable?.[GITHUB_USER_LOGIN_HEADER] ?? "", }, }); diff --git a/apps/open-swe/src/routes/app.ts b/apps/open-swe/src/routes/app.ts new file mode 100644 index 00000000..a0ebd0b6 --- /dev/null +++ b/apps/open-swe/src/routes/app.ts @@ -0,0 +1,6 @@ +import { Hono } from "hono"; +import { issueWebhookHandler } from "./github/issue-webhook.js"; + +export const app = new Hono(); + +app.post("/webhooks/github", issueWebhookHandler); diff --git a/apps/open-swe/src/routes/github/issue-webhook.ts b/apps/open-swe/src/routes/github/issue-webhook.ts new file mode 100644 index 00000000..492cd466 --- /dev/null +++ b/apps/open-swe/src/routes/github/issue-webhook.ts @@ -0,0 +1,195 @@ +import { v4 as uuidv4 } from "uuid"; +import { Context } from "hono"; +import { BlankEnv, BlankInput } from "hono/types"; +import { createLogger, LogLevel } from "../../utils/logger.js"; +import { GitHubApp } from "../../utils/github-app.js"; +import { Webhooks } from "@octokit/webhooks"; +import { createLangGraphClient } from "../../utils/langgraph-client.js"; +import { + GITHUB_INSTALLATION_TOKEN_COOKIE, + GITHUB_USER_ID_HEADER, + GITHUB_USER_LOGIN_HEADER, + MANAGER_GRAPH_ID, +} from "@open-swe/shared/constants"; +import { encryptGitHubToken } from "@open-swe/shared/crypto"; +import { HumanMessage } from "@langchain/core/messages"; +import { getOpenSWELabel } from "../../utils/github/label.js"; + +const logger = createLogger(LogLevel.INFO, "GitHubIssueWebhook"); + +const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET!; + +const githubApp = new GitHubApp(); + +const webhooks = new Webhooks({ + secret: GITHUB_WEBHOOK_SECRET, +}); + +const getOpenSweAppUrl = (threadId: string) => { + if (!process.env.OPEN_SWE_APP_URL) { + return ""; + } + try { + const baseUrl = new URL(process.env.OPEN_SWE_APP_URL); + baseUrl.pathname = `/chat/${threadId}`; + return baseUrl.toString(); + } catch { + return ""; + } +}; + +const getPayload = (body: string): Record | null => { + try { + const payload = JSON.parse(body); + return payload; + } catch { + return null; + } +}; + +const getHeaders = ( + c: Context, +): { + id: string; + name: string; + installationId: string; + targetType: string; +} | null => { + const headers = c.req.header(); + const webhookId = headers["x-github-delivery"] || ""; + const webhookEvent = headers["x-github-event"] || ""; + const installationId = headers["x-github-hook-installation-target-id"] || ""; + const targetType = headers["x-github-hook-installation-target-type"] || ""; + if (!webhookId || !webhookEvent || !installationId || !targetType) { + return null; + } + return { id: webhookId, name: webhookEvent, installationId, targetType }; +}; + +webhooks.on("issues.labeled", async ({ payload }) => { + if (!process.env.GITHUB_TOKEN_ENCRYPTION_KEY) { + throw new Error( + "GITHUB_TOKEN_ENCRYPTION_KEY environment variable is required", + ); + } + if (payload.label?.name !== getOpenSWELabel()) { + return; + } + + logger.info(`'open-swe' label added to issue #${payload.issue.number}`); + + try { + // Get installation ID from the webhook payload + const installationId = payload.installation?.id; + + if (!installationId) { + logger.error("No installation ID found in webhook payload"); + return; + } + + const [octokit, { token }] = await Promise.all([ + githubApp.getInstallationOctokit(installationId), + githubApp.getInstallationAccessToken(installationId), + ]); + const issueData = { + owner: payload.repository.owner.login, + repo: payload.repository.name, + issueNumber: payload.issue.number, + issueTitle: payload.issue.title, + issueBody: payload.issue.body || "", + userId: payload.sender.id, + userLogin: payload.sender.login, + }; + + const langGraphClient = createLangGraphClient({ + defaultHeaders: { + [GITHUB_INSTALLATION_TOKEN_COOKIE]: encryptGitHubToken( + token, + process.env.GITHUB_TOKEN_ENCRYPTION_KEY, + ), + [GITHUB_USER_ID_HEADER]: issueData.userId.toString(), + [GITHUB_USER_LOGIN_HEADER]: issueData.userLogin, + }, + }); + + const threadId = uuidv4(); + const run = await langGraphClient.runs.create(threadId, MANAGER_GRAPH_ID, { + input: { + messages: [ + new HumanMessage({ + id: uuidv4(), + content: `**${issueData.issueTitle}**\n\n${issueData.issueBody}`, + additional_kwargs: { + isOriginalIssue: true, + githubIssueId: issueData.issueNumber, + }, + }), + ], + githubIssueId: issueData.issueNumber, + targetRepository: { + owner: issueData.owner, + repo: issueData.repo, + }, + }, + config: { + recursion_limit: 400, + }, + ifNotExists: "create", + streamResumable: true, + streamMode: ["values", "messages", "custom"], + }); + + logger.info("Created new run from GitHub issue.", { + thread_id: threadId, + run_id: run.run_id, + issue_number: issueData.issueNumber, + owner: issueData.owner, + repo: issueData.repo, + user_id: issueData.userId, + user_login: issueData.userLogin, + }); + + logger.info("Creating comment..."); + const appUrl = getOpenSweAppUrl(threadId); + await octokit.request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + { + owner: issueData.owner, + repo: issueData.repo, + issue_number: issueData.issueNumber, + body: `🤖 Open SWE has been triggered for this issue. Processing...\n\n${appUrl ? `View run in Open SWE [here](${appUrl})` : ""}`, + }, + ); + } catch (error) { + logger.error("Error processing webhook:", error); + } +}); + +export async function issueWebhookHandler( + c: Context, +) { + const payload = getPayload(await c.req.text()); + if (!payload) { + logger.error("Missing payload"); + return c.json({ error: "Missing payload" }, { status: 400 }); + } + + const eventHeaders = getHeaders(c); + if (!eventHeaders) { + logger.error("Missing webhook headers"); + return c.json({ error: "Missing webhook headers" }, { status: 400 }); + } + + try { + await webhooks.receive({ + id: eventHeaders.id, + name: eventHeaders.name as any, + payload, + }); + + return c.json({ received: true }); + } catch (error) { + logger.error("Webhook error:", error); + return c.json({ error: "Webhook processing failed" }, { status: 400 }); + } +} diff --git a/apps/open-swe/src/security/auth.ts b/apps/open-swe/src/security/auth.ts index 0f902a7a..5986dc67 100644 --- a/apps/open-swe/src/security/auth.ts +++ b/apps/open-swe/src/security/auth.ts @@ -2,38 +2,17 @@ import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth"; import { verifyGithubUser, GithubUser, + verifyGithubUserId, } from "@open-swe/shared/github/verify-user"; import { GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_TOKEN_COOKIE, + GITHUB_USER_ID_HEADER, + GITHUB_USER_LOGIN_HEADER, } from "@open-swe/shared/constants"; import { decryptGitHubToken } from "@open-swe/shared/crypto"; - -const STUDIO_USER_ID = "langgraph-studio-user"; - -// Helper function to check if user is studio user -const isStudioUser = (userIdentity: string): boolean => { - return userIdentity === STUDIO_USER_ID; -}; - -// Helper function for operations that only need owner filtering -const createOwnerFilter = (user: { identity: string }) => { - if (isStudioUser(user.identity)) { - return; - } - return { owner: user.identity }; -}; - -// Helper function for create operations that set metadata -const createWithOwnerMetadata = (value: any, user: { identity: string }) => { - if (isStudioUser(user.identity)) { - return; - } - - value.metadata ??= {}; - value.metadata.owner = user.identity; - return { owner: user.identity }; -}; +import { verifyGitHubWebhookOrThrow } from "./github.js"; +import { createWithOwnerMetadata, createOwnerFilter } from "./utils.js"; export const auth = new Auth() .authenticate(async (request: Request) => { @@ -45,6 +24,13 @@ export const auth = new Auth() display_name: "CORS Preflight", }; } + + const ghSecretHashHeader = request.headers.get("X-Hub-Signature-256"); + if (ghSecretHashHeader) { + // This will either return a valid user, or throw an error + return await verifyGitHubWebhookOrThrow(request); + } + const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY; if (!encryptionKey) { throw new Error( @@ -52,13 +38,6 @@ export const auth = new Auth() ); } - // Parse Authorization header - const encryptedAccessToken = request.headers.get(GITHUB_TOKEN_COOKIE); - if (!encryptedAccessToken) { - throw new HTTPException(401, { - message: "GitHub access token header missing", - }); - } // 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. const encryptedInstallationToken = request.headers.get( @@ -70,24 +49,37 @@ export const auth = new Auth() }); } - // Validate GitHub access token let user: GithubUser | undefined; - try { + + const encryptedAccessToken = request.headers.get(GITHUB_TOKEN_COOKIE); + if (!encryptedAccessToken) { + // If there isn't a user access token, check to see if the user info is in headers. + // This would indicate a bot created the request. + const userIdHeader = request.headers.get(GITHUB_USER_ID_HEADER); + const userLoginHeader = request.headers.get(GITHUB_USER_LOGIN_HEADER); + if (!userIdHeader || !userLoginHeader) { + throw new HTTPException(401, { + message: "Github-User-Id or Github-User-Login header missing", + }); + } + user = await verifyGithubUserId( + decryptGitHubToken(encryptedInstallationToken, encryptionKey), + Number(userIdHeader), + userLoginHeader, + ); + } else { // Ensure we decrypt the token before passing to the verification function. user = await verifyGithubUser( decryptGitHubToken(encryptedAccessToken, encryptionKey), ); - if (!user) { - throw new HTTPException(401, { - message: - "Invalid GitHub token or user is not a member of the required organization.", - }); - } - } catch (e: any) { + } + + if (!user) { throw new HTTPException(401, { - message: `Authentication error: ${e.message}`, + message: "User not found", }); } + return { identity: user.id.toString(), is_authenticated: true, diff --git a/apps/open-swe/src/security/github.ts b/apps/open-swe/src/security/github.ts new file mode 100644 index 00000000..2e5dd469 --- /dev/null +++ b/apps/open-swe/src/security/github.ts @@ -0,0 +1,58 @@ +import { HTTPException } from "@langchain/langgraph-sdk/auth"; +import { Webhooks } from "@octokit/webhooks"; +import { createLogger, LogLevel } from "../utils/logger.js"; + +const logger = createLogger(LogLevel.INFO, "GitHubWebhookAuth"); + +export async function verifyGitHubWebhookOrThrow(request: Request) { + const secret = process.env.GITHUB_WEBHOOK_SECRET; + if (!secret) { + throw new Error("Missing GITHUB_WEBHOOK_SECRET environment variable."); + } + const webhooks = new Webhooks({ + secret, + }); + + const requestClone = request.clone(); + + const githubDeliveryHeader = requestClone.headers.get("x-github-delivery"); + const githubEventHeader = requestClone.headers.get("x-github-event"); + const githubSignatureHeader = requestClone.headers.get("x-hub-signature-256"); + if (!githubDeliveryHeader || !githubEventHeader || !githubSignatureHeader) { + throw new HTTPException(401, { + message: "Missing GitHub webhook headers.", + }); + } + + const payload = await requestClone.text(); + const signature = await webhooks.sign(payload); + const isValid = await webhooks.verify(payload, signature); + if (!isValid) { + logger.error("Failed to verify GitHub webhook"); + throw new HTTPException(401, { + message: "Invalid GitHub webhook signature.", + }); + } + + return { + identity: "x-internal-github-bot", + is_authenticated: true, + display_name: "GitHub Bot", + permissions: [ + "threads:create", + "threads:create_run", + "threads:read", + "threads:delete", + "threads:update", + "threads:search", + "assistants:create", + "assistants:read", + "assistants:delete", + "assistants:update", + "assistants:search", + "deployments:read", + "deployments:search", + "store:access", + ], + }; +} diff --git a/apps/open-swe/src/security/utils.ts b/apps/open-swe/src/security/utils.ts new file mode 100644 index 00000000..7b7a78e6 --- /dev/null +++ b/apps/open-swe/src/security/utils.ts @@ -0,0 +1,28 @@ +const STUDIO_USER_ID = "langgraph-studio-user"; + +// Helper function to check if user is studio user +export function isStudioUser(userIdentity: string): boolean { + return userIdentity === STUDIO_USER_ID; +} + +// Helper function for operations that only need owner filtering +export function createOwnerFilter(user: { identity: string }) { + if (isStudioUser(user.identity)) { + return; + } + return { owner: user.identity }; +} + +// Helper function for create operations that set metadata +export function createWithOwnerMetadata( + value: any, + user: { identity: string }, +) { + if (isStudioUser(user.identity)) { + return; + } + + value.metadata ??= {}; + value.metadata.owner = user.identity; + return { owner: user.identity }; +} diff --git a/apps/open-swe/src/utils/github-app.ts b/apps/open-swe/src/utils/github-app.ts new file mode 100644 index 00000000..28c5e302 --- /dev/null +++ b/apps/open-swe/src/utils/github-app.ts @@ -0,0 +1,46 @@ +import { App } from "@octokit/app"; +import { Octokit } from "@octokit/core"; + +export class GitHubApp { + app: App; + + constructor() { + const appId = process.env.GITHUB_APP_ID; + const privateKey = process.env.GITHUB_APP_PRIVATE_KEY; + const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET; + if (!appId || !privateKey || !webhookSecret) { + throw new Error( + "GitHub App ID, Private Key, or Webhook Secret is not configured.", + ); + } + + this.app = new App({ + appId, + privateKey, + webhooks: { + secret: webhookSecret, + }, + }); + } + + async getInstallationOctokit(installationId: number): Promise { + return await this.app.getInstallationOctokit(installationId); + } + + async getInstallationAccessToken(installationId: number): Promise<{ + token: string; + expiresAt: string; + }> { + const octokit = await this.app.getInstallationOctokit(installationId); + + // The installation access token is available on the auth property + const auth = (await octokit.auth({ + type: "installation", + })) as any; + + return { + token: auth.token, + expiresAt: auth.expiresAt, + }; + } +} diff --git a/apps/open-swe/src/utils/github-tokens.ts b/apps/open-swe/src/utils/github-tokens.ts index a914d582..15a2d8f7 100644 --- a/apps/open-swe/src/utils/github-tokens.ts +++ b/apps/open-swe/src/utils/github-tokens.ts @@ -15,9 +15,9 @@ export function getGitHubTokensFromConfig(config: GraphConfig): { const encryptedGitHubToken = config.configurable[GITHUB_TOKEN_COOKIE]; const encryptedInstallationToken = config.configurable[GITHUB_INSTALLATION_TOKEN_COOKIE]; - if (!encryptedGitHubToken || !encryptedInstallationToken) { + if (!encryptedInstallationToken) { throw new Error( - "Missing required x-github-access-token or x-github-installation-token in configuration.", + `Missing required ${GITHUB_INSTALLATION_TOKEN_COOKIE} in configuration.`, ); } @@ -30,10 +30,9 @@ export function getGitHubTokensFromConfig(config: GraphConfig): { } // Decrypt the GitHub token - const githubAccessToken = decryptGitHubToken( - encryptedGitHubToken, - encryptionKey, - ); + const githubAccessToken = encryptedGitHubToken + ? decryptGitHubToken(encryptedGitHubToken, encryptionKey) + : ""; const githubInstallationToken = decryptGitHubToken( encryptedInstallationToken, encryptionKey, diff --git a/apps/open-swe/src/utils/github/api.ts b/apps/open-swe/src/utils/github/api.ts index 1d8bad7d..8a730a7c 100644 --- a/apps/open-swe/src/utils/github/api.ts +++ b/apps/open-swe/src/utils/github/api.ts @@ -1,6 +1,7 @@ import { Octokit } from "@octokit/rest"; import { createLogger, LogLevel } from "../logger.js"; import { GitHubIssue, GitHubIssueComment, GitHubPullRequest } from "./types.js"; +import { getOpenSWELabel } from "./label.js"; const logger = createLogger(LogLevel.INFO, "GitHub-API"); @@ -141,7 +142,7 @@ export async function createPullRequest({ owner, repo, issue_number: pullRequest.number, - labels: ["open-swe"], + labels: [getOpenSWELabel()], }); logger.info("Added 'open-swe' label to pull request", { pullRequestNumber: pullRequest.number, diff --git a/apps/open-swe/src/utils/github/label.ts b/apps/open-swe/src/utils/github/label.ts new file mode 100644 index 00000000..b39f90a6 --- /dev/null +++ b/apps/open-swe/src/utils/github/label.ts @@ -0,0 +1,6 @@ +/** + * @returns "open-swe" or "open-swe-dev" based on the NODE_ENV. + */ +export function getOpenSWELabel() { + return process.env.NODE_ENV === "production" ? "open-swe" : "open-swe-dev"; +} diff --git a/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx b/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx index 90b6fe3e..aea65755 100644 --- a/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx +++ b/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx @@ -5,6 +5,7 @@ import { ThreadViewLoading } from "@/components/v2/thread-view-loading"; import { ThreadDisplayInfo, threadToDisplayInfo } from "@/components/v2/types"; import { useThreads } from "@/hooks/useThreads"; import { useStream } from "@langchain/langgraph-sdk/react"; +import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types"; import { GraphState } from "@open-swe/shared/open-swe/types"; import { useRouter } from "next/navigation"; @@ -24,14 +25,12 @@ export default function ThreadPage({ const { thread_id } = use(params); const stream = useStream({ apiUrl: process.env.NEXT_PUBLIC_API_URL ?? "", - assistantId: process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID ?? "", + assistantId: MANAGER_GRAPH_ID, threadId: thread_id, reconnectOnMount: true, }); - const { threads, threadsLoading } = useThreads( - process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID, - ); + const { threads, threadsLoading } = useThreads(MANAGER_GRAPH_ID); // Find the thread by ID const thread = threads.find((t) => t.thread_id === thread_id); diff --git a/apps/web/src/app/(v2)/chat/page.tsx b/apps/web/src/app/(v2)/chat/page.tsx index c529d673..c7b6fe85 100644 --- a/apps/web/src/app/(v2)/chat/page.tsx +++ b/apps/web/src/app/(v2)/chat/page.tsx @@ -7,11 +7,10 @@ import { GitHubAppProvider } from "@/providers/GitHubApp"; import { GraphState } from "@open-swe/shared/open-swe/types"; import { Toaster } from "@/components/ui/sonner"; import { Suspense } from "react"; +import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; export default function ChatPage() { - const { threads, threadsLoading } = useThreads( - process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID, - ); + const { threads, threadsLoading } = useThreads(MANAGER_GRAPH_ID); // Convert Thread objects to ThreadDisplayInfo for UI const displayThreads: ThreadDisplayInfo[] = threads.map(threadToDisplayInfo); diff --git a/apps/web/src/app/(v2)/chat/threads/page.tsx b/apps/web/src/app/(v2)/chat/threads/page.tsx index 38bfd127..fef96ea0 100644 --- a/apps/web/src/app/(v2)/chat/threads/page.tsx +++ b/apps/web/src/app/(v2)/chat/threads/page.tsx @@ -12,14 +12,13 @@ import { useThreads } from "@/hooks/useThreads"; import { GraphState } from "@open-swe/shared/open-swe/types"; import { ThreadCard, ThreadCardLoading } from "@/components/v2/thread-card"; import { ThemeToggle } from "@/components/theme-toggle"; +import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; type FilterStatus = "all" | "running" | "completed" | "failed" | "pending"; export default function AllThreadsPage() { const router = useRouter(); - const { threads, threadsLoading } = useThreads( - process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID, - ); + const { threads, threadsLoading } = useThreads(MANAGER_GRAPH_ID); const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); diff --git a/apps/web/src/components/v2/default-view.tsx b/apps/web/src/components/v2/default-view.tsx index a9a55bb3..09eaeec2 100644 --- a/apps/web/src/components/v2/default-view.tsx +++ b/apps/web/src/components/v2/default-view.tsx @@ -27,6 +27,7 @@ import { GitHubInstallationBanner } from "../github/installation-banner"; import { QuickActions } from "./quick-actions"; import { useState } from "react"; import { GitHubLogoutButton } from "../github/github-oauth-button"; +import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; interface DefaultViewProps { threads: ThreadDisplayInfo[]; @@ -37,8 +38,7 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) { const router = useRouter(); const [quickActionPrompt, setQuickActionPrompt] = useState(""); const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? ""; - const assistantId: string | undefined = - process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID ?? ""; + const assistantId: string | undefined = MANAGER_GRAPH_ID; const { contentBlocks, setContentBlocks, @@ -49,8 +49,8 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) { handlePaste, } = useFileUpload(); - if (!apiUrl || !assistantId) { - return
Missing API URL or Assistant ID
; + if (!apiUrl) { + return
Missing API URL environment variable
; } return ( diff --git a/apps/web/src/components/v2/terminal-input.tsx b/apps/web/src/components/v2/terminal-input.tsx index 1d8e3fa7..14591dd4 100644 --- a/apps/web/src/components/v2/terminal-input.tsx +++ b/apps/web/src/components/v2/terminal-input.tsx @@ -14,6 +14,7 @@ import { GraphState } from "@open-swe/shared/open-swe/types"; import { Base64ContentBlock, HumanMessage } from "@langchain/core/messages"; import { toast } from "sonner"; import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/useConfigStore"; +import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; interface TerminalInputProps { placeholder?: string; @@ -51,14 +52,7 @@ export function TerminalInput({ }); const handleSend = async () => { - const assistantId = process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID; - if (!assistantId) { - toast.error("No assistant ID found", { - richColors: true, - closeButton: true, - }); - return; - } + const assistantId = MANAGER_GRAPH_ID; if (!selectedRepository) { toast.error("Please select a repository first", { richColors: true, diff --git a/apps/web/src/components/v2/thread-view.tsx b/apps/web/src/components/v2/thread-view.tsx index d81b56eb..daff9e5d 100644 --- a/apps/web/src/components/v2/thread-view.tsx +++ b/apps/web/src/components/v2/thread-view.tsx @@ -15,7 +15,11 @@ import { GraphState } from "@open-swe/shared/open-swe/types"; import { ActionsRenderer } from "./actions-renderer"; import { ThemeToggle } from "../theme-toggle"; import { HumanMessage } from "@langchain/core/messages"; -import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants"; +import { + DO_NOT_RENDER_ID_PREFIX, + PROGRAMMER_GRAPH_ID, + PLANNER_GRAPH_ID, +} from "@open-swe/shared/constants"; import { StickToBottom } from "use-stick-to-bottom"; import { StickyToBottomContent, @@ -23,9 +27,6 @@ import { } from "../../utils/scroll-utils"; import { ManagerChat } from "./manager-chat"; -const PROGRAMMER_ASSISTANT_ID = process.env.NEXT_PUBLIC_PROGRAMMER_ASSISTANT_ID; -const PLANNER_ASSISTANT_ID = process.env.NEXT_PUBLIC_PLANNER_ASSISTANT_ID; - interface ThreadViewProps { stream: ReturnType>; displayThread: ThreadDisplayInfo; @@ -153,23 +154,17 @@ export function ThreadView({ - {plannerThreadId && - plannerRunId && - PLANNER_ASSISTANT_ID && ( - - graphId={PLANNER_ASSISTANT_ID} - threadId={plannerThreadId} - runId={plannerRunId} - setProgrammerSession={setProgrammerSession} - programmerSession={programmerSession} - setSelectedTab={setSelectedTab} - /> - )} - {!( - plannerThreadId && - plannerRunId && - PLANNER_ASSISTANT_ID - ) && ( + {plannerThreadId && plannerRunId && ( + + graphId={PLANNER_GRAPH_ID} + threadId={plannerThreadId} + runId={plannerRunId} + setProgrammerSession={setProgrammerSession} + programmerSession={programmerSession} + setSelectedTab={setSelectedTab} + /> + )} + {!(plannerThreadId && plannerRunId) && (
@@ -183,9 +178,9 @@ export function ThreadView({ - {programmerSession && PROGRAMMER_ASSISTANT_ID && ( + {programmerSession && ( - graphId={PROGRAMMER_ASSISTANT_ID} + graphId={PROGRAMMER_GRAPH_ID} threadId={programmerSession.threadId} runId={programmerSession.runId} /> diff --git a/apps/web/src/providers/Stream.tsx b/apps/web/src/providers/Stream.tsx index 05b1dd4f..94533d1f 100644 --- a/apps/web/src/providers/Stream.tsx +++ b/apps/web/src/providers/Stream.tsx @@ -14,6 +14,7 @@ import { CustomNodeEvent, isCustomNodeEvent, } from "@open-swe/shared/open-swe/custom-node-events"; +import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; const useTypedStream = useStream< GraphState, @@ -37,18 +38,15 @@ const StreamSession = ({ children }: { children: ReactNode }) => { const [customEvents, setCustomEvents] = useState([]); const { refreshThreads } = useThreadsContext(); - if ( - !process.env.NEXT_PUBLIC_API_URL || - !process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID - ) { + if (!process.env.NEXT_PUBLIC_API_URL) { throw new Error( - "Both NEXT_PUBLIC_API_URL and NEXT_PUBLIC_MANAGER_ASSISTANT_ID environment variables must be defined.", + "NEXT_PUBLIC_API_URL environment variable must be defined.", ); } const streamValue = useTypedStream({ apiUrl: process.env.NEXT_PUBLIC_API_URL, - assistantId: process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID, + assistantId: MANAGER_GRAPH_ID, reconnectOnMount: true, threadId: threadId ?? null, onCustomEvent: (event, options) => { diff --git a/langgraph.json b/langgraph.json index 73a6b545..7c52390e 100644 --- a/langgraph.json +++ b/langgraph.json @@ -9,6 +9,9 @@ "dependencies": ["./apps/open-swe"], "auth": { "path": "./apps/open-swe/src/security/auth.ts:auth" + }, + "http": { + "app": "./apps/open-swe/src/routes/app.ts:app" } } \ No newline at end of file diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index b1cb7043..23638655 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -12,3 +12,10 @@ export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-"; export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state"; export const GITHUB_INSTALLATION_ID_COOKIE = "github_installation_id"; export const GITHUB_TOKEN_TYPE_COOKIE = "github_token_type"; + +export const MANAGER_GRAPH_ID = "manager"; +export const PLANNER_GRAPH_ID = "planner"; +export const PROGRAMMER_GRAPH_ID = "programmer"; + +export const GITHUB_USER_ID_HEADER = "x-github-user-id"; +export const GITHUB_USER_LOGIN_HEADER = "x-github-user-login"; diff --git a/packages/shared/src/github/verify-user.ts b/packages/shared/src/github/verify-user.ts index 2445f951..67ccc19c 100644 --- a/packages/shared/src/github/verify-user.ts +++ b/packages/shared/src/github/verify-user.ts @@ -26,3 +26,31 @@ export async function verifyGithubUser( return undefined; } } + +/** + * Verifies a GitHub user ID using the app installation token. Checks that the provided + * user ID is valid, and the provided login matches the user's login. + * @param installationToken The GitHub installation token. + * @param userId The GitHub user ID. + * @param userLogin The GitHub user login. + * @returns A promise that resolves with the user object if valid, otherwise undefined. + */ +export async function verifyGithubUserId( + installationToken: string, + userId: number, + userLogin: string, +): Promise { + try { + const octokit = new Octokit({ auth: installationToken }); + const { data: user } = await octokit.users.getById({ account_id: userId }); + if (!user || !user.login) { + return undefined; + } + if (user.login !== userLogin) { + return undefined; + } + return user; + } catch { + return undefined; + } +} diff --git a/packages/shared/src/open-swe/types.ts b/packages/shared/src/open-swe/types.ts index e227e77e..95cc4927 100644 --- a/packages/shared/src/open-swe/types.ts +++ b/packages/shared/src/open-swe/types.ts @@ -16,6 +16,8 @@ import { import { GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_TOKEN_COOKIE, + GITHUB_USER_ID_HEADER, + GITHUB_USER_LOGIN_HEADER, } from "../constants.js"; import { withLangGraph } from "@langchain/langgraph/zod"; import { BaseMessage } from "@langchain/core/messages"; @@ -395,6 +397,16 @@ export const GraphConfigurationMetadata: { type: "hidden", }, }, + [GITHUB_USER_ID_HEADER]: { + x_open_swe_ui_config: { + type: "hidden", + }, + }, + [GITHUB_USER_LOGIN_HEADER]: { + x_open_swe_ui_config: { + type: "hidden", + }, + }, }; export const GraphConfiguration = z.object({ @@ -550,6 +562,20 @@ export const GraphConfiguration = z.object({ .langgraph.metadata( GraphConfigurationMetadata[GITHUB_INSTALLATION_TOKEN_COOKIE], ), + /** + * The user's GitHub ID. Required when creating runs triggered by a bot (e.g. GitHub issue) + */ + [GITHUB_USER_ID_HEADER]: z + .string() + .optional() + .langgraph.metadata(GraphConfigurationMetadata[GITHUB_USER_ID_HEADER]), + /** + * The user's GitHub login. Required when creating runs triggered by a bot (e.g. GitHub issue) + */ + [GITHUB_USER_LOGIN_HEADER]: z + .string() + .optional() + .langgraph.metadata(GraphConfigurationMetadata[GITHUB_USER_LOGIN_HEADER]), }); export type GraphConfig = LangGraphRunnableConfig< diff --git a/yarn.lock b/yarn.lock index ccefd315..c48cdc0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2087,6 +2087,75 @@ __metadata: languageName: node linkType: hard +"@octokit/app@npm:^16.0.1": + version: 16.0.1 + resolution: "@octokit/app@npm:16.0.1" + dependencies: + "@octokit/auth-app": ^8.0.1 + "@octokit/auth-unauthenticated": ^7.0.1 + "@octokit/core": ^7.0.2 + "@octokit/oauth-app": ^8.0.1 + "@octokit/plugin-paginate-rest": ^13.0.0 + "@octokit/types": ^14.0.0 + "@octokit/webhooks": ^14.0.0 + checksum: a6c8fb0d1fda7da9022bb04b3e4baf3e30320eff03de63cdf34a0bf90a1afd471705063c909137f308b6bec359c6762e9a0f1303a53853770e06dabaa6f6460d + languageName: node + linkType: hard + +"@octokit/auth-app@npm:^8.0.1": + version: 8.0.1 + resolution: "@octokit/auth-app@npm:8.0.1" + dependencies: + "@octokit/auth-oauth-app": ^9.0.1 + "@octokit/auth-oauth-user": ^6.0.0 + "@octokit/request": ^10.0.2 + "@octokit/request-error": ^7.0.0 + "@octokit/types": ^14.0.0 + toad-cache: ^3.7.0 + universal-github-app-jwt: ^2.2.0 + universal-user-agent: ^7.0.0 + checksum: ca7ede162bfec0602f3578775f81a5c2f2b8c691ec51ae619fb0943b23952da4842495147333d8938faef01fa1dcc43885df9da320140f774a110ded0042ddec + languageName: node + linkType: hard + +"@octokit/auth-oauth-app@npm:^9.0.1": + version: 9.0.1 + resolution: "@octokit/auth-oauth-app@npm:9.0.1" + dependencies: + "@octokit/auth-oauth-device": ^8.0.1 + "@octokit/auth-oauth-user": ^6.0.0 + "@octokit/request": ^10.0.2 + "@octokit/types": ^14.0.0 + universal-user-agent: ^7.0.0 + checksum: cf6e6b87cc04638c88b1eda5d43a41fc524262e89ad73d5ad19ac1ae63e35727d867966a2e036bbf9699da9f7256d50b2d735a2e3d2fff84610d44a5107f5a86 + languageName: node + linkType: hard + +"@octokit/auth-oauth-device@npm:^8.0.1": + version: 8.0.1 + resolution: "@octokit/auth-oauth-device@npm:8.0.1" + dependencies: + "@octokit/oauth-methods": ^6.0.0 + "@octokit/request": ^10.0.2 + "@octokit/types": ^14.0.0 + universal-user-agent: ^7.0.0 + checksum: 074d1fb0b39efa0f440cb9e2b70bab920920d39d43f05210764a7e1f4c0afac71c7d9c5bfbce62e979d1adf886c6b9788a99a7617098970790e87963125d5264 + languageName: node + linkType: hard + +"@octokit/auth-oauth-user@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/auth-oauth-user@npm:6.0.0" + dependencies: + "@octokit/auth-oauth-device": ^8.0.1 + "@octokit/oauth-methods": ^6.0.0 + "@octokit/request": ^10.0.2 + "@octokit/types": ^14.0.0 + universal-user-agent: ^7.0.0 + checksum: 663b856fc3dd7047d08220bef5444d05ba748b1cb34bca48f485fc467283fdac64c3cdf5a21eae503058b4372ae0092d705b3ee59583715393229f9028370bb2 + languageName: node + linkType: hard + "@octokit/auth-token@npm:^6.0.0": version: 6.0.0 resolution: "@octokit/auth-token@npm:6.0.0" @@ -2094,6 +2163,16 @@ __metadata: languageName: node linkType: hard +"@octokit/auth-unauthenticated@npm:^7.0.1": + version: 7.0.1 + resolution: "@octokit/auth-unauthenticated@npm:7.0.1" + dependencies: + "@octokit/request-error": ^7.0.0 + "@octokit/types": ^14.0.0 + checksum: 825eaf2c2df66bd307984098440caac24d50f7e5c3e56bddaf96f6b46814be28b4d550de44786c1c8bcb744a962ade1fd92eecf3a59e422e776adc1344e6268f + languageName: node + linkType: hard + "@octokit/core@npm:^7.0.2": version: 7.0.2 resolution: "@octokit/core@npm:7.0.2" @@ -2130,6 +2209,41 @@ __metadata: languageName: node linkType: hard +"@octokit/oauth-app@npm:^8.0.1": + version: 8.0.1 + resolution: "@octokit/oauth-app@npm:8.0.1" + dependencies: + "@octokit/auth-oauth-app": ^9.0.1 + "@octokit/auth-oauth-user": ^6.0.0 + "@octokit/auth-unauthenticated": ^7.0.1 + "@octokit/core": ^7.0.2 + "@octokit/oauth-authorization-url": ^8.0.0 + "@octokit/oauth-methods": ^6.0.0 + "@types/aws-lambda": ^8.10.83 + universal-user-agent: ^7.0.0 + checksum: ef03e4fe7da34930aa14b9170a0f53d278f398b30e5f2c033bb8836f64d1226989f2bb25456cf6b234ae3ab84e30b6d329706ca0604e0efff1ad81ae5f6b4749 + languageName: node + linkType: hard + +"@octokit/oauth-authorization-url@npm:^8.0.0": + version: 8.0.0 + resolution: "@octokit/oauth-authorization-url@npm:8.0.0" + checksum: 8f7431f986ed87e3980b6647c2af7a4df383116e0b4f37a17d10708bb4b7d93dcb2de6f05228539a77f050c0786498f2a62ce71243299d1051e54746484b07f1 + languageName: node + linkType: hard + +"@octokit/oauth-methods@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/oauth-methods@npm:6.0.0" + dependencies: + "@octokit/oauth-authorization-url": ^8.0.0 + "@octokit/request": ^10.0.2 + "@octokit/request-error": ^7.0.0 + "@octokit/types": ^14.0.0 + checksum: f17ab37869b13bf832aea5471d96d8e5ebe1f14b079cde9d7400afe683e5925ec87f6555c92abf9e825b2013c43c667761472b945cc1405cef89c8c10ab19f52 + languageName: node + linkType: hard + "@octokit/openapi-types@npm:^20.0.0": version: 20.0.0 resolution: "@octokit/openapi-types@npm:20.0.0" @@ -2144,6 +2258,24 @@ __metadata: languageName: node linkType: hard +"@octokit/openapi-webhooks-types@npm:12.0.3": + version: 12.0.3 + resolution: "@octokit/openapi-webhooks-types@npm:12.0.3" + checksum: 63304f68de7484eef7aa7dfde0a9825b46f7cd99d792560c3a8e48ba740db247ae2bf8a0254ff7018f4b27ff54f29c2d5abfc47cde8aa08cbfcb5abe165a10cd + languageName: node + linkType: hard + +"@octokit/plugin-paginate-rest@npm:^13.0.0": + version: 13.1.0 + resolution: "@octokit/plugin-paginate-rest@npm:13.1.0" + dependencies: + "@octokit/types": ^14.1.0 + peerDependencies: + "@octokit/core": ">=6" + checksum: e697757714f7c66a9e66737d34619040dd332ded58c6f78ea44917665b3b87a7a8d741465c8bc012b728de517ccf7d3b9382b08be09d7ac6b700a1d2e55b5d83 + languageName: node + linkType: hard + "@octokit/plugin-paginate-rest@npm:^13.0.1": version: 13.0.1 resolution: "@octokit/plugin-paginate-rest@npm:13.0.1" @@ -2227,6 +2359,24 @@ __metadata: languageName: node linkType: hard +"@octokit/webhooks-methods@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/webhooks-methods@npm:6.0.0" + checksum: 6edf536eb8a695e1c26b7530f14bfaefdeec4e92f4a43e8f27b9a10175c0904f51804801f742d4ea60a4e9b6021dc74c9ef02079a0c979b0e8be5eccabea3c45 + languageName: node + linkType: hard + +"@octokit/webhooks@npm:^14.0.0, @octokit/webhooks@npm:^14.0.2": + version: 14.0.2 + resolution: "@octokit/webhooks@npm:14.0.2" + dependencies: + "@octokit/openapi-webhooks-types": 12.0.3 + "@octokit/request-error": ^7.0.0 + "@octokit/webhooks-methods": ^6.0.0 + checksum: 129bb615eed93fc1c8f20fe60bb5f9548821b27fd4a5b3fc2d6e81be3fe5163f3b427181c945cefa3196a3b02d18dc642127a4bf68de5ca50e52a79039c4dde7 + languageName: node + linkType: hard + "@open-draft/deferred-promise@npm:^2.2.0": version: 2.2.0 resolution: "@open-draft/deferred-promise@npm:2.2.0" @@ -2266,7 +2416,10 @@ __metadata: "@langchain/langgraph-cli": latest "@langchain/langgraph-sdk": ^0.0.85 "@langchain/openai": ^0.5.10 + "@octokit/app": ^16.0.1 + "@octokit/core": ^7.0.2 "@octokit/rest": ^22.0.0 + "@octokit/webhooks": ^14.0.2 "@open-swe/shared": "*" "@tsconfig/recommended": ^1.0.8 "@types/jest": ^29.5.0 @@ -2278,6 +2431,7 @@ __metadata: eslint-plugin-import: ^2.27.5 eslint-plugin-no-instanceof: ^1.0.1 eslint-plugin-prettier: ^4.2.1 + hono: ^4.8.3 jest: ^29.7.0 langchain: ^0.3.26 langsmith: ^0.3.29 @@ -3663,6 +3817,13 @@ __metadata: languageName: node linkType: hard +"@types/aws-lambda@npm:^8.10.83": + version: 8.10.150 + resolution: "@types/aws-lambda@npm:8.10.150" + checksum: 5bde42dfb6f566f00c55542585f81ecf6cda8063b9c27a3a49da6cab853c222987f078315000b0b9d2668a4398507c37378de66d6472727fcf3c1c6360641964 + languageName: node + linkType: hard + "@types/babel__core@npm:^7.1.14": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" @@ -7943,6 +8104,13 @@ __metadata: languageName: node linkType: hard +"hono@npm:^4.8.3": + version: 4.8.3 + resolution: "hono@npm:4.8.3" + checksum: cacd43002e5345547cd568c4d831f5d40b54ec8fe2f80dd03cf21d45d2112f31d08e6f57399d7ae08c0fc2329342113cdc4f0e46f3de52c971afe8ef9414c51f + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -13391,6 +13559,13 @@ __metadata: languageName: node linkType: hard +"toad-cache@npm:^3.7.0": + version: 3.7.0 + resolution: "toad-cache@npm:3.7.0" + checksum: d0f2092ab2c0f3355d3537c41b13888a12996f38080e6c39907e715eb382d997ccf61baab9e8eda3f202b6c07e304728106be3631c9fe3b6c001aaf15b7bdb8f + languageName: node + linkType: hard + "toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" @@ -13901,6 +14076,13 @@ __metadata: languageName: node linkType: hard +"universal-github-app-jwt@npm:^2.2.0": + version: 2.2.2 + resolution: "universal-github-app-jwt@npm:2.2.2" + checksum: 95c0801e69898aec15b42731b47e7a5a830dbe7e456ba84b66b9b78542d2e79a62d633424c98c58e6fe6b09fbad6951111cce7db85573955c14dd930ecda0ee4 + languageName: node + linkType: hard + "universal-user-agent@npm:^7.0.0, universal-user-agent@npm:^7.0.2": version: 7.0.3 resolution: "universal-user-agent@npm:7.0.3"