feat: GitHub e2e auth (#106)

* feat: GitHub e2e auth

* cr

* reimplement proxy route

* fix github auth

* cr

* cr

* cr

* cr

* cr
This commit is contained in:
Brace Sproul 2025-06-11 14:00:08 -07:00 • committed by GitHub
parent 3dbb2a576d
commit bf72e1faf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 315 additions and 253 deletions

View file

@ -50,7 +50,9 @@ DAYTONA_API_KEY=""
And the web `.env` file should contain the following variables:
```bash
NEXT_PUBLIC_API_URL=http://localhost:2024 # Change to production URL when deployed
# Change to production URLs when deployed
NEXT_PUBLIC_API_URL="http://localhost:3000/api"
LANGGRAPH_API_URL="http://localhost:2024"
NEXT_PUBLIC_ASSISTANT_ID="open-swe"
# For the GitHub OAuth flow
@ -137,14 +139,3 @@ Once you've accepted the plan, it will begin the execution flow. When the agent
## Accessing Changes
Open SWE will automatically create a branch whenever you create a new thread with a naming format of `open-swe/<threadId>`. Every time a file is created, modified, or deleted, the changes will be committed to this branch. You can access the changes in the repository by checking out this branch.
## Install Daytona CLI
```bash
# Mac os
brew install daytonaio/cli/daytona
# Windows
powershell -Command "irm https://get.daytona.io/windows | iex"
```

View file

@ -29,7 +29,7 @@ export async function initialize(
state: GraphState,
config: GraphConfig,
): Promise<GraphUpdate> {
const { githubToken, githubAccessToken } = getGitHubTokensFromConfig(config);
const { githubAccessToken } = getGitHubTokensFromConfig(config);
const { sandboxSessionId, targetRepository } = state;
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
@ -58,7 +58,7 @@ export async function initialize(
});
const res = await cloneRepo(sandbox, targetRepository, {
githubToken,
githubAccessToken,
stateBranchName: state.branchName,
});
if (res.exitCode !== 0) {
@ -70,7 +70,6 @@ export async function initialize(
logger.info(`Configuring git user for repository at "${absoluteRepoDir}"...`);
await configureGitUserInRepo(absoluteRepoDir, sandbox, {
githubToken,
githubAccessToken,
owner: targetRepository.owner,
repo: targetRepository.repo,

View file

@ -72,7 +72,7 @@ export async function openPullRequest(
"Failed to open pull request: No sandbox session ID found in state.",
);
}
const { githubToken } = getGitHubTokensFromConfig(config);
const { githubAccessToken } = getGitHubTokensFromConfig(config);
const sandbox = await daytonaClient().get(sandboxSessionId);
@ -132,7 +132,7 @@ export async function openPullRequest(
headBranch: branchName ?? getBranchName(config),
title,
body,
githubToken,
githubAccessToken,
});
let sandboxDeleted = false;

View file

@ -0,0 +1,115 @@
import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth";
import { verifyGithubUser, GithubUser } from "./github-auth.js";
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
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 };
};
export const auth = new Auth()
.authenticate(async (request: Request) => {
if (request.method === "OPTIONS") {
return {
identity: "anonymous",
permissions: [],
is_authenticated: false,
display_name: "CORS Preflight",
};
}
// Parse Authorization header
const accessToken = request.headers.get(GITHUB_TOKEN_COOKIE);
if (!accessToken) {
throw new HTTPException(401, {
message: "GitHub access token header missing",
});
}
// Validate GitHub access token
let user: GithubUser | undefined;
try {
user = await verifyGithubUser(accessToken);
if (!user) {
throw new HTTPException(401, {
message:
"Invalid GitHub token or user is not a member of the required organization.",
});
}
} catch (e: any) {
throw new HTTPException(401, {
message: `Authentication error: ${e.message}`,
});
}
return {
identity: user.id.toString(),
is_authenticated: true,
display_name: user.login,
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",
],
};
})
// THREADS: create operations with metadata
.on("threads:create", ({ value, user }) =>
createWithOwnerMetadata(value, user),
)
.on("threads:create_run", ({ value, user }) =>
createWithOwnerMetadata(value, user),
)
// THREADS: read, update, delete, search operations
.on("threads:read", ({ user }) => createOwnerFilter(user))
.on("threads:update", ({ user }) => createOwnerFilter(user))
.on("threads:delete", ({ user }) => createOwnerFilter(user))
.on("threads:search", ({ user }) => createOwnerFilter(user))
// ASSISTANTS: create operation with metadata
.on("assistants:create", ({ value, user }) =>
createWithOwnerMetadata(value, user),
)
// ASSISTANTS: read, update, delete, search operations
.on("assistants:read", ({ user }) => createOwnerFilter(user))
.on("assistants:update", ({ user }) => createOwnerFilter(user))
.on("assistants:delete", ({ user }) => createOwnerFilter(user))
.on("assistants:search", ({ user }) => createOwnerFilter(user))
// STORE: permission-based access
.on("store", ({ user }) => {
return { owner: user.identity };
});

View file

@ -0,0 +1,57 @@
import { Octokit } from "@octokit/rest";
import { Endpoints } from "@octokit/types";
import { createLogger, LogLevel } from "../utils/logger.js";
const logger = createLogger(LogLevel.INFO, "GithubAuth");
export type GithubUser = Endpoints["GET /user"]["response"]["data"];
/**
* Verifies a GitHub user access token and checks for membership in the 'langchain-ai' organization.
*
* @param accessToken The GitHub user access token.
* @returns A promise that resolves with the user object if valid and a member, otherwise undefined.
*/
export async function verifyGithubUser(
accessToken: string,
): Promise<GithubUser | undefined> {
if (!accessToken) {
return undefined;
}
try {
const octokit = new Octokit({ auth: accessToken });
// 1. Fetch user information to validate the token
const { data: user } = await octokit.users.getAuthenticated();
if (!user || !user.login) {
logger.error(
"GitHub token is invalid or user information could not be retrieved.",
);
return undefined;
}
const username = user.login;
// 2. List organizations for the user
const { data: orgs } = await octokit.orgs.listForUser({
username,
});
// 3. Check for 'langchain-ai' organization membership
const isMember = orgs.some((org) => org.login === "langchain-ai");
if (!isMember) {
logger.info(
`User ${username} is not a member of the 'langchain-ai' organization.`,
);
return undefined;
}
return user;
} catch (error) {
logger.error("An error occurred during GitHub user verification:", error);
return undefined;
}
}

View file

@ -214,13 +214,12 @@ export async function configureGitUserInRepo(
absoluteRepoDir: string,
sandbox: Sandbox,
args: {
githubToken: string;
githubAccessToken: string;
owner: string;
repo: string;
},
): Promise<void> {
const { githubToken, githubAccessToken, owner, repo } = args;
const { githubAccessToken, owner, repo } = args;
let needsGitConfig = false;
try {
const nameCheck = await sandbox.process.executeCommand(
@ -262,7 +261,7 @@ export async function configureGitUserInRepo(
try {
// Set the remote URL with the token using the provided owner and repo
const setRemoteOutput = await sandbox.process.executeCommand(
`git remote set-url origin https://x-access-token:${githubToken}@github.com/${owner}/${repo}.git`,
`git remote set-url origin https://x-access-token:${githubAccessToken}@github.com/${owner}/${repo}.git`,
absoluteRepoDir,
undefined,
TIMEOUT_SEC,
@ -493,17 +492,17 @@ export async function createPullRequest({
headBranch,
title,
body = "",
githubToken,
githubAccessToken,
}: {
owner: string;
repo: string;
headBranch: string;
title: string;
body?: string;
githubToken: string;
githubAccessToken: string;
}) {
const octokit = new Octokit({
auth: githubToken,
auth: githubAccessToken,
});
try {
@ -535,7 +534,7 @@ export async function createPullRequest({
logger.info(
"Pull request already exists. Getting existing pull request...",
);
return getExistingPullRequest(owner, repo, headBranch, githubToken);
return getExistingPullRequest(owner, repo, headBranch, githubAccessToken);
}
logger.error(`Failed to create pull request`, {
@ -575,7 +574,7 @@ export async function cloneRepo(
sandbox: Sandbox,
targetRepository: TargetRepository,
args: {
githubToken: string;
githubAccessToken: string;
stateBranchName?: string;
},
) {
@ -583,7 +582,7 @@ export async function cloneRepo(
const gitCloneCommand = ["git", "clone"];
// Use x-access-token format for better GitHub authentication
const repoUrlWithToken = `https://x-access-token:${args.githubToken}@github.com/${targetRepository.owner}/${targetRepository.repo}.git`;
const repoUrlWithToken = `https://x-access-token:${args.githubAccessToken}@github.com/${targetRepository.owner}/${targetRepository.repo}.git`;
const branchName = args.stateBranchName || targetRepository.branch;
if (branchName) {

View file

@ -1,21 +1,15 @@
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
import { GraphConfig } from "@open-swe/shared/open-swe/types";
export function getGitHubTokensFromConfig(config: GraphConfig): {
githubToken: string;
githubAccessToken: string;
} {
if (!config.configurable) {
throw new Error("No configurable object found in graph config.");
}
const githubToken = config.configurable["x-github-installation-token"];
const githubAccessToken = config.configurable["x-github-access-token"];
if (!githubToken) {
throw new Error(
"Missing required x-github-installation-token in configuration.",
);
}
const githubAccessToken = config.configurable[GITHUB_TOKEN_COOKIE];
if (!githubAccessToken) {
throw new Error("Missing required x-github-access-token in configuration.");
}
return { githubToken, githubAccessToken };
return { githubAccessToken };
}

View file

@ -1,4 +1,5 @@
NEXT_PUBLIC_API_URL="http://localhost:2024"
NEXT_PUBLIC_API_URL="http://localhost:3000/api"
LANGGRAPH_API_URL="http://localhost:2024"
NEXT_PUBLIC_ASSISTANT_ID="open-swe"
# For the GitHub OAuth flow

View file

@ -47,7 +47,7 @@
"framer-motion": "^12.4.9",
"jsonwebtoken": "^9.0.2",
"katex": "^0.16.21",
"langgraph-nextjs-api-passthrough": "^0.0.4",
"langgraph-nextjs-api-passthrough": "^0.1.2",
"lodash": "^4.17.21",
"lucide-react": "^0.476.0",
"next-themes": "^0.4.4",

View file

@ -1,11 +1,18 @@
import { initApiPassthrough } from "langgraph-nextjs-api-passthrough";
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
// This file acts as a proxy for requests to your LangGraph server.
// Read the [Going to Production](https://github.com/langchain-ai/agent-chat-ui?tab=readme-ov-file#going-to-production) section for more information.
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, runtime } =
initApiPassthrough({
apiUrl: process.env.LANGGRAPH_API_URL ?? "remove-me", // default, if not defined it will attempt to read process.env.LANGGRAPH_API_URL
apiKey: process.env.LANGSMITH_API_KEY ?? "remove-me", // default, if not defined it will attempt to read process.env.LANGSMITH_API_KEY
apiUrl: process.env.LANGGRAPH_API_URL ?? "http://localhost:2024",
runtime: "edge", // default
disableWarningLog: true,
headers: (req) => {
return {
[GITHUB_TOKEN_COOKIE]:
req.cookies.get(GITHUB_TOKEN_COOKIE)?.value ?? "",
};
},
});

View file

@ -1,10 +1,10 @@
import {
GITHUB_AUTH_STATE_COOKIE,
GITHUB_INSTALLATION_ID_COOKIE,
GITHUB_TOKEN_COOKIE,
GITHUB_TOKEN_TYPE_COOKIE,
} from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
export async function GET(request: NextRequest) {
try {
@ -93,7 +93,7 @@ export async function GET(request: NextRequest) {
// Set token cookies directly on the response
response.cookies.set(GITHUB_TOKEN_COOKIE, tokenData.access_token, {
// httpOnly: true,
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 30, // 30 days

View file

@ -1,10 +1,10 @@
import {
GITHUB_INSTALLATION_RETURN_TO_COOKIE,
GITHUB_INSTALLATION_STATE_COOKIE,
GITHUB_TOKEN_COOKIE,
} from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";
import { randomBytes } from "crypto";
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
/**
* Initiates the GitHub App installation flow

View file

@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from "next/server";
import { getInstallationToken } from "../../../../../utils/github"; // Adjusted path
import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth";
const GITHUB_API_URL = "https://api.github.com";
async function handler(req: NextRequest) {
const path = req.nextUrl.pathname.replace(/^\/api\/github\/proxy\//, "");
const installationIdCookie = req.cookies.get(
GITHUB_INSTALLATION_ID_COOKIE,
)?.value;
if (!installationIdCookie) {
return NextResponse.json(
{ error: `"${GITHUB_INSTALLATION_ID_COOKIE}" cookie is required` },
{ status: 400 },
);
}
const appId = process.env.GITHUB_APP_ID;
const privateAppKey = process.env.GITHUB_APP_PRIVATE_KEY;
if (!appId || !privateAppKey) {
console.error("GitHub App ID or Private App Key is not configured.");
return NextResponse.json(
{ error: `Missing required environment variables.` },
{ status: 500 },
);
}
try {
const token = await getInstallationToken(
installationIdCookie,
appId,
privateAppKey,
);
const targetUrl = new URL(`${GITHUB_API_URL}/${path}`);
const headers = new Headers();
headers.set("Authorization", `Bearer ${token}`);
headers.set("Accept", "application/vnd.github.v3+json");
headers.set("User-Agent", "OpenSWE-Proxy");
if (req.headers.has("Content-Type")) {
headers.set("Content-Type", req.headers.get("Content-Type")!);
}
const response = await fetch(targetUrl.toString(), {
method: req.method,
headers: headers,
body:
req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
});
const responseHeaders = new Headers(response.headers);
responseHeaders.delete("Content-Encoding"); // Prevent ERR_CONTENT_DECODING_FAILED error.
return new NextResponse(response.body, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
} catch (error) {
console.error("Error in GitHub proxy:", error);
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return NextResponse.json(
{ error: "Failed to proxy request to GitHub", details: errorMessage },
{ status: 500 },
);
}
}
export const GET = handler;
export const POST = handler;
export const PUT = handler;
export const DELETE = handler;
export const PATCH = handler;

View file

@ -1,151 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { CopyIcon, CheckIcon, RefreshCwIcon } from "lucide-react";
import { InstallAppButton } from "./install-app-button";
interface AgentTokenProviderProps {
className?: string;
}
/**
* Component to fetch and display a GitHub installation token for use with the AI agent
* This token can be passed to your agent service to perform Git operations on behalf of the user
*/
export function AgentTokenProvider({
className = "",
}: AgentTokenProviderProps) {
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [installationId, setInstallationId] = useState<string | null>(null);
const fetchToken = async () => {
setIsLoading(true);
setError(null);
setCopied(false);
try {
const response = await fetch("/api/github/token");
if (!response.ok) {
const errorData = await response.json();
setError(errorData.error || "Failed to fetch token");
setIsLoading(false);
return;
}
const data = await response.json();
setToken(data.token);
setInstallationId(data.installation_id);
setIsLoading(false);
} catch {
setError("Network error when fetching token");
setIsLoading(false);
}
};
useEffect(() => {
fetchToken();
}, []);
const copyToken = () => {
if (token) {
navigator.clipboard.writeText(token);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
if (error && error.includes("installation")) {
return (
<div className={`rounded-md border p-4 ${className}`}>
<h3 className="mb-2 text-lg font-medium">GitHub App Not Installed</h3>
<p className="mb-4 text-sm text-gray-600">
You need to install our GitHub App to generate tokens for the AI
agent.
</p>
<InstallAppButton>Install GitHub App</InstallAppButton>
</div>
);
}
if (error) {
return (
<div className={`rounded-md border p-4 ${className}`}>
<div className="mb-4 rounded-md border border-red-200 bg-red-50 p-4">
<p className="text-sm text-red-800">{error}</p>
</div>
<Button
variant="outline"
onClick={fetchToken}
disabled={isLoading}
>
Try Again
</Button>
</div>
);
}
return (
<div className={`rounded-md border p-4 ${className}`}>
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-medium">GitHub Token for AI Agent</h3>
<Button
variant="outline"
size="sm"
onClick={fetchToken}
disabled={isLoading}
>
<RefreshCwIcon className="mr-2 h-4 w-4" />
Refresh Token
</Button>
</div>
{isLoading ? (
<div className="animate-pulse space-y-3">
<div className="h-4 w-3/4 rounded bg-gray-200"></div>
<div className="h-10 rounded bg-gray-200"></div>
</div>
) : token ? (
<>
<p className="mb-2 text-sm text-gray-600">
This token expires in 1 hour. Use it to authenticate your AI agent
with GitHub.
</p>
<div className="relative">
<div className="mb-2 overflow-x-auto rounded-md border bg-gray-50 p-3 font-mono text-sm whitespace-nowrap">
{token}
</div>
<Button
size="sm"
variant="ghost"
className="absolute top-2 right-2"
onClick={copyToken}
>
{copied ? (
<CheckIcon className="h-4 w-4" />
) : (
<CopyIcon className="h-4 w-4" />
)}
</Button>
</div>
<div className="mt-4 space-y-2">
<p className="text-sm font-medium">How to use this token:</p>
<div className="rounded-md border bg-gray-50 p-3 font-mono text-xs">
{`export GITHUB_TOKEN=${token}`}
</div>
<p className="text-xs text-gray-500">
Pass this token to your agent service to perform Git operations on
behalf of the user.
</p>
</div>
</>
) : (
<p className="text-gray-600">Loading token...</p>
)}
</div>
);
}

View file

@ -20,17 +20,6 @@ interface UseGitHubAppReturn {
defaultBranch: string | null;
}
// Helper function to get GitHub OAuth access token from cookies
function getGitHubAccessToken(): string | null {
if (typeof document === "undefined") return null;
const cookies = document.cookie.split("; ");
const tokenCookie = cookies.find((row) =>
row.startsWith("x-github_access_token="),
);
return tokenCookie ? tokenCookie.split("=")[1] : null;
}
export function useGitHubApp(): UseGitHubAppReturn {
const [isInstalled, setIsInstalled] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(true);
@ -116,12 +105,6 @@ export function useGitHubApp(): UseGitHubAppReturn {
return;
}
const accessToken = getGitHubAccessToken();
if (!accessToken) {
setBranchesError("GitHub access token not found");
return;
}
setBranchesLoading(true);
setBranchesError(null);
@ -129,7 +112,6 @@ export function useGitHubApp(): UseGitHubAppReturn {
const branchData = await getRepositoryBranches(
selectedRepository.owner,
selectedRepository.repo,
accessToken,
);
setBranches(branchData || []);
} catch (err) {

View file

@ -1,7 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
// Prefix the access token with `x-` so that it's included in requests to the LangGraph server.
export const GITHUB_TOKEN_COOKIE = "x-github_access_token";
export const GITHUB_TOKEN_TYPE_COOKIE = "github_token_type";
export const GITHUB_INSTALLATION_ID_COOKIE = "github_installation_id";
export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state";

View file

@ -7,7 +7,6 @@ import React, {
useRef,
} from "react";
import { useStream } from "@langchain/langgraph-sdk/react";
import { type Message } from "@langchain/langgraph-sdk";
import {
uiMessageReducer,
isUIMessage,
@ -53,22 +52,13 @@ const StreamSession = ({
githubToken: string;
}) => {
const [threadId, setThreadId] = useQueryState("threadId");
const { refreshThreads, setThreads, updateThreadFromStream } = useThreads();
const { refreshThreads, updateThreadFromStream } = useThreads();
const githubAccessToken =
document.cookie
.split("; ")
.find((row) => row.startsWith("x-github_access_token="))
?.split("=")[1] || "";
const streamValue = useTypedStream({
apiUrl,
assistantId,
reconnectOnMount: true,
threadId: threadId ?? null,
defaultHeaders: {
"x-github-installation-token": githubToken,
"x-github-access-token": githubAccessToken,
},
onCustomEvent: (event, options) => {
if (isUIMessage(event) || isRemoveUIMessage(event)) {
options.mutate((prev) => {
@ -179,7 +169,7 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
checkGitHubAppInstallation();
}
}
}, [isAuth, githubToken, isTokenLoading]);
}, [isAuth, githubToken]);
const checkAuthStatus = async () => {
try {

View file

@ -1,5 +1,14 @@
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
import * as jwt from "jsonwebtoken";
function getBaseApiUrl(): string {
let baseApiUrl = new URL(
process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api",
).href;
baseApiUrl = baseApiUrl.endsWith("/") ? baseApiUrl : `${baseApiUrl}/`;
return baseApiUrl;
}
/**
* Generates a JWT for GitHub App authentication
*/
@ -82,18 +91,17 @@ export async function getInstallationRepositories(
export async function getRepositoryBranches(
owner: string,
repo: string,
accessToken: string,
): Promise<Branch[]> {
const allBranches: Branch[] = [];
let page = 1;
const perPage = 100; // Maximum allowed by GitHub API
// First, get repository info to ensure we have the default branch
const repoResponse = await fetch(
`https://api.github.com/repos/${owner}/${repo}`,
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "OpenSWE-Agent",
},
@ -109,10 +117,9 @@ export async function getRepositoryBranches(
// Fetch all branches with pagination
while (true) {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`,
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "OpenSWE-Agent",
},

View file

@ -4,6 +4,9 @@
"open_swe": "./apps/open-swe/src/index.ts:graph"
},
"env": "./apps/open-swe/.env",
"dependencies": ["./apps/open-swe"]
"dependencies": ["./apps/open-swe"],
"auth": {
"path": "./apps/open-swe/src/security/auth.ts:auth"
}
}

View file

@ -3,3 +3,6 @@ export const SANDBOX_ROOT_DIR = "/home/daytona";
export const SNAPSHOT_NAME = "daytonaio/langchain-open-swe:0.1.0";
export const PLAN_INTERRUPT_DELIMITER = ":::";
export const PLAN_INTERRUPT_ACTION_TITLE = "Approve/Edit Plan";
// Prefix the access token with `x-` so that it's included in requests to the LangGraph server.
export const GITHUB_TOKEN_COOKIE = "x-github-access-token";

View file

@ -11,6 +11,7 @@ import {
type UIMessage,
type RemoveUIMessage,
} from "@langchain/langgraph-sdk/react-ui";
import { GITHUB_TOKEN_COOKIE } from "../constants.js";
export type PlanItem = {
/**
@ -285,12 +286,7 @@ export const GraphConfigurationMetadata: {
"The maximum number of tokens to generate in an individual generation",
},
},
"x-github-installation-token": {
x_open_swe_ui_config: {
type: "hidden",
},
},
"x-github-access-token": {
[GITHUB_TOKEN_COOKIE]: {
x_open_swe_ui_config: {
type: "hidden",
},
@ -414,22 +410,13 @@ export const GraphConfiguration = z.object({
.optional()
.default(() => 10_000)
.langgraph.metadata(GraphConfigurationMetadata.maxTokens),
/**
* The user's GitHub installation token. To be used to take actions on behalf of the user.
*/
"x-github-installation-token": z
.string()
.optional()
.langgraph.metadata(
GraphConfigurationMetadata["x-github-installation-token"],
),
/**
* The user's GitHub access token. To be used in requests to get information about the user.
*/
"x-github-access-token": z
[GITHUB_TOKEN_COOKIE]: z
.string()
.optional()
.langgraph.metadata(GraphConfigurationMetadata["x-github-access-token"]),
.langgraph.metadata(GraphConfigurationMetadata[GITHUB_TOKEN_COOKIE]),
});
export type GraphConfig = LangGraphRunnableConfig<

View file

@ -2346,7 +2346,7 @@ __metadata:
globals: ^15.14.0
jsonwebtoken: ^9.0.2
katex: ^0.16.21
langgraph-nextjs-api-passthrough: ^0.0.4
langgraph-nextjs-api-passthrough: ^0.1.2
lodash: ^4.17.21
lucide-react: ^0.476.0
next: ^15.2.3
@ -9446,12 +9446,12 @@ __metadata:
languageName: node
linkType: hard
"langgraph-nextjs-api-passthrough@npm:^0.0.4":
version: 0.0.4
resolution: "langgraph-nextjs-api-passthrough@npm:0.0.4"
"langgraph-nextjs-api-passthrough@npm:^0.1.2":
version: 0.1.2
resolution: "langgraph-nextjs-api-passthrough@npm:0.1.2"
peerDependencies:
next: "*"
checksum: e9e0f501cd1495c55166ac5840605c75f4e61bdad2ab102ebbffc8f5ef3055c87cf23842d12e889e77d81e00920084d59e8dd4c596fd54494077f2b2cd9e6fb1
checksum: 3ed989353b376e8e7b36cf37181838a439246e19cca1e1f8ae3cc27852ce8dcc7ed86ee62acb6c34055c60aea50a248d7f098012e2993f2b2c8e0f650ce71d23
languageName: node
linkType: hard