mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
feat: Require API keys from non langchain users (#448)
* feat: Require API keys from non langchain users * cr * cr
This commit is contained in:
parent
9e5f042c0e
commit
dda277456a
11 changed files with 361 additions and 83 deletions
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from "../../utils/github/label.js";
|
||||
import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types";
|
||||
import { RequestSource } from "../../constants.js";
|
||||
import { isAllowedUser } from "../../utils/github/allowed-users.js";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "GitHubIssueWebhook");
|
||||
|
||||
|
|
@ -129,6 +130,13 @@ webhooks.on("issues.labeled", async ({ payload }) => {
|
|||
userLogin: payload.sender.login,
|
||||
};
|
||||
|
||||
if (!isAllowedUser(issueData.userLogin)) {
|
||||
logger.error("User is not a member of allowed orgs", {
|
||||
username: issueData.userLogin,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const langGraphClient = createLangGraphClient({
|
||||
defaultHeaders: {
|
||||
[GITHUB_INSTALLATION_TOKEN_COOKIE]: encryptSecret(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
verifyGithubUserId,
|
||||
} from "@open-swe/shared/github/verify-user";
|
||||
import {
|
||||
API_KEY_REQUIRED_MESSAGE,
|
||||
GITHUB_INSTALLATION_NAME,
|
||||
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
||||
GITHUB_TOKEN_COOKIE,
|
||||
|
|
@ -16,6 +17,8 @@ import { verifyGitHubWebhookOrThrow } from "./github.js";
|
|||
import { createWithOwnerMetadata, createOwnerFilter } from "./utils.js";
|
||||
import { LANGGRAPH_USER_PERMISSIONS } from "../constants.js";
|
||||
import { getGitHubPatFromRequest } from "../utils/github-pat.js";
|
||||
import { isAllowedUser } from "../utils/github/allowed-users.js";
|
||||
import { validate } from "uuid";
|
||||
|
||||
// TODO: Export from LangGraph SDK
|
||||
export interface BaseAuthReturn {
|
||||
|
|
@ -31,6 +34,58 @@ interface AuthenticateReturn extends BaseAuthReturn {
|
|||
};
|
||||
}
|
||||
|
||||
function apiKeysInRequestBody(
|
||||
bodyStr: string | Record<string, unknown>,
|
||||
): boolean {
|
||||
try {
|
||||
const body = typeof bodyStr === "string" ? JSON.parse(bodyStr) : bodyStr;
|
||||
if (
|
||||
body.config?.configurable &&
|
||||
("anthropicApiKey" in body.config.configurable.apiKeys ||
|
||||
"openaiApiKey" in body.config.configurable.apiKeys ||
|
||||
"googleApiKey" in body.config.configurable.apiKeys)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
// no-op
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isRunReq(reqUrl: string): boolean {
|
||||
try {
|
||||
const url = new URL(reqUrl);
|
||||
const pathnameParts = url.pathname.split("/");
|
||||
const isCreateAndWait = !!(
|
||||
pathnameParts[1] === "threads" &&
|
||||
validate(pathnameParts[2]) &&
|
||||
pathnameParts[3] === "runs" &&
|
||||
pathnameParts[4] === "wait" &&
|
||||
pathnameParts.length === 5
|
||||
);
|
||||
const isCreateBackground = !!(
|
||||
pathnameParts[1] === "threads" &&
|
||||
validate(pathnameParts[2]) &&
|
||||
pathnameParts[3] === "runs" &&
|
||||
pathnameParts.length === 4
|
||||
);
|
||||
const isCreateStream = !!(
|
||||
pathnameParts[1] === "threads" &&
|
||||
validate(pathnameParts[2]) &&
|
||||
pathnameParts[3] === "runs" &&
|
||||
pathnameParts[4] === "stream" &&
|
||||
pathnameParts.length === 5
|
||||
);
|
||||
|
||||
return !!isCreateAndWait || !!isCreateBackground || !!isCreateStream;
|
||||
} catch {
|
||||
// no-op
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const auth = new Auth()
|
||||
.authenticate<AuthenticateReturn>(async (request: Request) => {
|
||||
if (request.method === "OPTIONS") {
|
||||
|
|
@ -100,10 +155,18 @@ export const auth = new Auth()
|
|||
});
|
||||
}
|
||||
|
||||
const encryptedAccessToken = request.headers.get(GITHUB_TOKEN_COOKIE);
|
||||
const decryptedAccessToken = encryptedAccessToken
|
||||
? decryptSecret(encryptedAccessToken, encryptionKey)
|
||||
: undefined;
|
||||
const decryptedInstallationToken = decryptSecret(
|
||||
encryptedInstallationToken,
|
||||
encryptionKey,
|
||||
);
|
||||
|
||||
let user: GithubUser | undefined;
|
||||
|
||||
const encryptedAccessToken = request.headers.get(GITHUB_TOKEN_COOKIE);
|
||||
if (!encryptedAccessToken) {
|
||||
if (!decryptedAccessToken) {
|
||||
// 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);
|
||||
|
|
@ -114,15 +177,13 @@ export const auth = new Auth()
|
|||
});
|
||||
}
|
||||
user = await verifyGithubUserId(
|
||||
decryptSecret(encryptedInstallationToken, encryptionKey),
|
||||
decryptedInstallationToken,
|
||||
Number(userIdHeader),
|
||||
userLoginHeader,
|
||||
);
|
||||
} else {
|
||||
// Ensure we decrypt the token before passing to the verification function.
|
||||
user = await verifyGithubUser(
|
||||
decryptSecret(encryptedAccessToken, encryptionKey),
|
||||
);
|
||||
user = await verifyGithubUser(decryptedAccessToken);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
|
|
@ -131,6 +192,18 @@ export const auth = new Auth()
|
|||
});
|
||||
}
|
||||
|
||||
const reqCopy = request.clone();
|
||||
const reqBody = await reqCopy.text();
|
||||
if (!isAllowedUser(user.login)) {
|
||||
if (isRunReq(request.url)) {
|
||||
if (!apiKeysInRequestBody(reqBody)) {
|
||||
throw new HTTPException(401, {
|
||||
message: API_KEY_REQUIRED_MESSAGE,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
identity: user.id.toString(),
|
||||
is_authenticated: true,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ export function getCustomConfigurableFields(
|
|||
GraphConfigurationMetadata,
|
||||
)) {
|
||||
if (key in config.configurable) {
|
||||
if (metadataValue.x_open_swe_ui_config.type !== "hidden") {
|
||||
if (
|
||||
metadataValue.x_open_swe_ui_config.type !== "hidden" ||
|
||||
key === "apiKeys"
|
||||
) {
|
||||
result[key as keyof GraphConfig["configurable"]] =
|
||||
config.configurable[key as keyof GraphConfig["configurable"]];
|
||||
}
|
||||
|
|
|
|||
66
apps/open-swe/src/utils/github/allowed-users.ts
Normal file
66
apps/open-swe/src/utils/github/allowed-users.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
export const ALLOWED_USERS = [
|
||||
"agola11",
|
||||
"akira",
|
||||
"aliyanishfaq",
|
||||
"andrewnguonly",
|
||||
"angus-langchain",
|
||||
"bracesproul",
|
||||
"ArthurLangChain",
|
||||
"baskaryan",
|
||||
"bvs-langchain",
|
||||
"catherine-langchain",
|
||||
"ccurme",
|
||||
"crystalro0",
|
||||
"dqbd",
|
||||
"emily-langchain",
|
||||
"eric-langchain",
|
||||
"EugeneJinXin",
|
||||
"eyurtsev",
|
||||
"gladwig2",
|
||||
"hari-dhanushkodi",
|
||||
"hinthornw",
|
||||
"hntrl",
|
||||
"hwchase17",
|
||||
"iakshay",
|
||||
"isahers1",
|
||||
"j-broekhuizen",
|
||||
"jacoblee93",
|
||||
"jdrogers940",
|
||||
"joaquin-borggio-lc",
|
||||
"katmayb",
|
||||
"keshivtandon",
|
||||
"langchain-infra",
|
||||
"lc-arjun",
|
||||
"lc-chad",
|
||||
"lnhsingh",
|
||||
"madams0013",
|
||||
"mdrxy",
|
||||
"mhk197",
|
||||
"nfcampos",
|
||||
"nhuang-lc",
|
||||
"nitboss",
|
||||
"PeriniM",
|
||||
"phvash",
|
||||
"QuentinBrosse",
|
||||
"rlancemartin",
|
||||
"romain-priour-lc",
|
||||
"samecrowder",
|
||||
"samnoyes",
|
||||
"starmorph",
|
||||
"suraj-langchain",
|
||||
"sydney-runkle",
|
||||
"tanushree-sharma",
|
||||
"victorm-lc",
|
||||
"xornivore",
|
||||
"xuro-langchain",
|
||||
];
|
||||
|
||||
// HACK: Until we setup proper support for API credits, we will only allow users to self host Open SWE
|
||||
export function isAllowedUser(username: string): boolean {
|
||||
const restrictToLangChainAuth =
|
||||
process.env.RESTRICT_TO_LANGCHAIN_AUTH === "true";
|
||||
if (!restrictToLangChainAuth) {
|
||||
return true;
|
||||
}
|
||||
return ALLOWED_USERS.some((u) => u === username);
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||
import { isAllowedUser } from "./github/allowed-users.js";
|
||||
import { decryptSecret } from "@open-swe/shared/crypto";
|
||||
|
||||
export enum Task {
|
||||
/**
|
||||
|
|
@ -35,6 +37,22 @@ const TASK_TO_CONFIG_DEFAULTS_MAP = {
|
|||
},
|
||||
};
|
||||
|
||||
const providerToApiKey = (
|
||||
providerName: string,
|
||||
apiKeys: Record<string, string>,
|
||||
): string => {
|
||||
switch (providerName) {
|
||||
case "openai":
|
||||
return apiKeys.openaiApiKey;
|
||||
case "anthropic":
|
||||
return apiKeys.anthropicApiKey;
|
||||
case "google-genai":
|
||||
return apiKeys.googleApiKey;
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${providerName}`);
|
||||
}
|
||||
};
|
||||
|
||||
export async function loadModel(config: GraphConfig, task: Task) {
|
||||
const modelStr =
|
||||
config.configurable?.[`${task}ModelName`] ??
|
||||
|
|
@ -66,9 +84,35 @@ export async function loadModel(config: GraphConfig, task: Task) {
|
|||
maxTokens = maxTokens > 8_192 ? 8_192 : maxTokens;
|
||||
}
|
||||
|
||||
// TODO: Fix types
|
||||
const userLogin = (config.configurable as any)?.langgraph_auth_user
|
||||
?.display_name;
|
||||
const secretsEncryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||
if (!secretsEncryptionKey) {
|
||||
throw new Error("SECRETS_ENCRYPTION_KEY environment variable is required");
|
||||
}
|
||||
if (!userLogin) {
|
||||
throw new Error("User login not found in config");
|
||||
}
|
||||
const apiKeys = config.configurable?.apiKeys;
|
||||
let apiKey: string | null = null;
|
||||
if (!isAllowedUser(userLogin)) {
|
||||
if (!apiKeys) {
|
||||
throw new Error("API keys not found in config");
|
||||
}
|
||||
apiKey = decryptSecret(
|
||||
providerToApiKey(modelProvider, apiKeys),
|
||||
secretsEncryptionKey,
|
||||
);
|
||||
if (!apiKey) {
|
||||
throw new Error("No API key found for provider: " + modelProvider);
|
||||
}
|
||||
}
|
||||
|
||||
const model = await initChatModel(modelName, {
|
||||
modelProvider,
|
||||
temperature: thinkingModel ? undefined : temperature,
|
||||
...(apiKey ? { apiKey } : {}),
|
||||
...(thinkingModel && modelProvider === "anthropic"
|
||||
? {
|
||||
thinking: { budget_tokens: thinkingBudgetTokens, type: "enabled" },
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
getInstallationNameFromReq,
|
||||
getGitHubAccessTokenOrThrow,
|
||||
} from "./utils";
|
||||
import { encryptSecret } from "@open-swe/shared/crypto";
|
||||
|
||||
// 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.
|
||||
|
|
@ -19,6 +20,33 @@ export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, runtime } =
|
|||
apiUrl: process.env.LANGGRAPH_API_URL ?? "http://localhost:2024",
|
||||
runtime: "edge", // default
|
||||
disableWarningLog: true,
|
||||
bodyParameters: (req, body) => {
|
||||
if (body.config?.configurable && "apiKeys" in body.config.configurable) {
|
||||
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||
if (!encryptionKey) {
|
||||
throw new Error(
|
||||
"SECRETS_ENCRYPTION_KEY environment variable is required",
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeys = body.config.configurable.apiKeys;
|
||||
const encryptedApiKeys: Record<string, unknown> = {};
|
||||
|
||||
// Encrypt each field in the apiKeys object
|
||||
for (const [key, value] of Object.entries(apiKeys)) {
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
encryptedApiKeys[key] = encryptSecret(value, encryptionKey);
|
||||
} else {
|
||||
encryptedApiKeys[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the body with encrypted apiKeys
|
||||
body.config.configurable.apiKeys = encryptedApiKeys;
|
||||
return body;
|
||||
}
|
||||
return body;
|
||||
},
|
||||
headers: async (req) => {
|
||||
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||
if (!encryptionKey) {
|
||||
|
|
|
|||
|
|
@ -11,12 +11,20 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg dark:group-[.toaster]:bg-slate-950 dark:group-[.toaster]:border-slate-800 dark:group-[.toaster]:shadow-2xl dark:group-[.toaster]:shadow-black/20",
|
||||
description:
|
||||
"group-[.toast]:text-muted-foreground dark:group-[.toast]:text-slate-400",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground font-medium",
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground font-medium hover:group-[.toast]:bg-primary/90 dark:group-[.toast]:bg-slate-200 dark:group-[.toast]:text-slate-900 dark:hover:group-[.toast]:bg-slate-100",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground font-medium",
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground font-medium hover:group-[.toast]:bg-muted/80 dark:group-[.toast]:bg-slate-800 dark:group-[.toast]:text-slate-300 dark:hover:group-[.toast]:bg-slate-700",
|
||||
success:
|
||||
"group-[.toast]:bg-green-50 group-[.toast]:text-green-900 group-[.toast]:border-green-200 dark:group-[.toast]:bg-green-950/50 dark:group-[.toast]:text-green-100 dark:group-[.toast]:border-green-800/50",
|
||||
error:
|
||||
"group-[.toast]:bg-red-50 group-[.toast]:text-red-900 group-[.toast]:border-red-200 dark:group-[.toast]:bg-red-950/50 dark:group-[.toast]:text-red-100 dark:group-[.toast]:border-red-800/50",
|
||||
warning:
|
||||
"group-[.toast]:bg-yellow-50 group-[.toast]:text-yellow-900 group-[.toast]:border-yellow-200 dark:group-[.toast]:bg-yellow-950/50 dark:group-[.toast]:text-yellow-100 dark:group-[.toast]:border-yellow-800/50",
|
||||
info: "group-[.toast]:bg-blue-50 group-[.toast]:text-blue-900 group-[.toast]:border-blue-200 dark:group-[.toast]:bg-blue-950/50 dark:group-[.toast]:text-blue-100 dark:group-[.toast]:border-blue-800/50",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ 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";
|
||||
import {
|
||||
API_KEY_REQUIRED_MESSAGE,
|
||||
MANAGER_GRAPH_ID,
|
||||
} from "@open-swe/shared/constants";
|
||||
import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types";
|
||||
import { useDraftStorage } from "@/hooks/useDraftStorage";
|
||||
|
||||
|
|
@ -113,7 +116,31 @@ export function TerminalInput({
|
|||
setContentBlocks([]);
|
||||
setAutoAcceptPlan(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (
|
||||
typeof e === "object" &&
|
||||
e !== null &&
|
||||
"message" in e &&
|
||||
e.message !== null &&
|
||||
typeof e.message === "string" &&
|
||||
e.message.includes(API_KEY_REQUIRED_MESSAGE)
|
||||
) {
|
||||
toast.error(
|
||||
<p>
|
||||
{API_KEY_REQUIRED_MESSAGE} Please add your API key(s) in{" "}
|
||||
<a
|
||||
className="text-blue-500 underline underline-offset-1 dark:text-blue-400"
|
||||
href="/settings?tab=api-keys"
|
||||
>
|
||||
settings
|
||||
</a>
|
||||
</p>,
|
||||
{
|
||||
richColors: true,
|
||||
duration: 30_000,
|
||||
closeButton: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ import { Eye, EyeOff, Key, Trash2 } from "lucide-react";
|
|||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useConfigStore, DEFAULT_CONFIG_KEY } from "@/hooks/useConfigStore";
|
||||
|
||||
interface ApiKey {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
value: string;
|
||||
isVisible: boolean;
|
||||
lastUsed?: string;
|
||||
|
|
@ -26,72 +28,82 @@ interface ApiKeySection {
|
|||
keys: ApiKey[];
|
||||
}
|
||||
|
||||
const API_KEY_SECTIONS: Record<string, Omit<ApiKeySection, "keys">> = {
|
||||
llms: {
|
||||
title: "LLMs",
|
||||
},
|
||||
infrastructure: {
|
||||
title: "Infrastructure",
|
||||
},
|
||||
};
|
||||
|
||||
const API_KEY_DEFINITIONS = {
|
||||
llms: [
|
||||
{ id: "anthropicApiKey", name: "Anthropic" },
|
||||
{ id: "openaiApiKey", name: "OpenAI" },
|
||||
{ id: "googleApiKey", name: "Google Gen AI" },
|
||||
],
|
||||
infrastructure: [
|
||||
{
|
||||
id: "daytonaApiKey",
|
||||
name: "Daytona",
|
||||
description: "Users not required to set this if using the demo",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function APIKeysTab() {
|
||||
const [apiKeySections, setApiKeySections] = useState<
|
||||
Record<string, ApiKeySection>
|
||||
>({
|
||||
llms: {
|
||||
title: "LLMs",
|
||||
keys: [
|
||||
{
|
||||
id: "anthropicApiKey",
|
||||
name: "Anthropic",
|
||||
value: "",
|
||||
isVisible: false,
|
||||
},
|
||||
{ id: "openaiApiKey", name: "OpenAI", value: "", isVisible: false },
|
||||
{
|
||||
id: "googleApiKey",
|
||||
name: "Google Gen AI",
|
||||
value: "",
|
||||
isVisible: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
infrastructure: {
|
||||
title: "Infrastructure",
|
||||
keys: [
|
||||
{ id: "daytonaApiKey", name: "Daytona", value: "", isVisible: false },
|
||||
],
|
||||
},
|
||||
});
|
||||
const { getConfig, updateConfig } = useConfigStore();
|
||||
const config = getConfig(DEFAULT_CONFIG_KEY);
|
||||
|
||||
const toggleKeyVisibility = (sectionKey: string, keyId: string) => {
|
||||
setApiKeySections((prev) => ({
|
||||
const [visibilityState, setVisibilityState] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
const toggleKeyVisibility = (keyId: string) => {
|
||||
setVisibilityState((prev) => ({
|
||||
...prev,
|
||||
[sectionKey]: {
|
||||
...prev[sectionKey],
|
||||
keys: prev[sectionKey].keys.map((key) =>
|
||||
key.id === keyId ? { ...key, isVisible: !key.isVisible } : key,
|
||||
),
|
||||
},
|
||||
[keyId]: !prev[keyId],
|
||||
}));
|
||||
};
|
||||
|
||||
const updateApiKey = (sectionKey: string, keyId: string, value: string) => {
|
||||
setApiKeySections((prev) => ({
|
||||
...prev,
|
||||
[sectionKey]: {
|
||||
...prev[sectionKey],
|
||||
keys: prev[sectionKey].keys.map((key) =>
|
||||
key.id === keyId ? { ...key, value } : key,
|
||||
),
|
||||
},
|
||||
}));
|
||||
const updateApiKey = (keyId: string, value: string) => {
|
||||
const currentApiKeys = config.apiKeys || {};
|
||||
updateConfig(DEFAULT_CONFIG_KEY, "apiKeys", {
|
||||
...currentApiKeys,
|
||||
[keyId]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteApiKey = (sectionKey: string, keyId: string) => {
|
||||
setApiKeySections((prev) => ({
|
||||
...prev,
|
||||
[sectionKey]: {
|
||||
...prev[sectionKey],
|
||||
keys: prev[sectionKey].keys.map((key) =>
|
||||
key.id === keyId ? { ...key, value: "" } : key,
|
||||
),
|
||||
},
|
||||
}));
|
||||
const deleteApiKey = (keyId: string) => {
|
||||
const currentApiKeys = config.apiKeys || {};
|
||||
const updatedApiKeys = { ...currentApiKeys };
|
||||
delete updatedApiKeys[keyId];
|
||||
updateConfig(DEFAULT_CONFIG_KEY, "apiKeys", updatedApiKeys);
|
||||
};
|
||||
|
||||
const getApiKeySections = (): Record<string, ApiKeySection> => {
|
||||
const sections: Record<string, ApiKeySection> = {};
|
||||
const apiKeys = config.apiKeys || {};
|
||||
|
||||
Object.entries(API_KEY_SECTIONS).forEach(([sectionKey, sectionInfo]) => {
|
||||
sections[sectionKey] = {
|
||||
...sectionInfo,
|
||||
keys: API_KEY_DEFINITIONS[
|
||||
sectionKey as keyof typeof API_KEY_DEFINITIONS
|
||||
].map((keyDef) => ({
|
||||
...keyDef,
|
||||
value: apiKeys[keyDef.id] || "",
|
||||
isVisible: visibilityState[keyDef.id] || false,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
const apiKeySections = getApiKeySections();
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{Object.entries(apiKeySections).map(([sectionKey, section]) => (
|
||||
|
|
@ -148,13 +160,18 @@ export function APIKeysTab() {
|
|||
>
|
||||
API Key
|
||||
</Label>
|
||||
{apiKey.description && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{apiKey.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<Input
|
||||
id={`${apiKey.id}-key`}
|
||||
type={apiKey.isVisible ? "text" : "password"}
|
||||
value={apiKey.value}
|
||||
onChange={(e) =>
|
||||
updateApiKey(sectionKey, apiKey.id, e.target.value)
|
||||
updateApiKey(apiKey.id, e.target.value)
|
||||
}
|
||||
placeholder={`Enter your ${apiKey.name} API key`}
|
||||
className="font-mono text-sm"
|
||||
|
|
@ -162,9 +179,7 @@ export function APIKeysTab() {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
toggleKeyVisibility(sectionKey, apiKey.id)
|
||||
}
|
||||
onClick={() => toggleKeyVisibility(apiKey.id)}
|
||||
className="px-2"
|
||||
>
|
||||
{apiKey.isVisible ? (
|
||||
|
|
@ -177,7 +192,7 @@ export function APIKeysTab() {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteApiKey(sectionKey, apiKey.id)}
|
||||
onClick={() => deleteApiKey(apiKey.id)}
|
||||
className={cn(
|
||||
"px-2",
|
||||
"text-destructive hover:bg-destructive/10 hover:text-destructive",
|
||||
|
|
@ -192,16 +207,8 @@ export function APIKeysTab() {
|
|||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Your API key is stored securely and encrypted
|
||||
Your API key is stored
|
||||
</p>
|
||||
{apiKey.value && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -38,3 +38,6 @@ export const DEFAULT_MCP_SERVERS = {
|
|||
stderr: "inherit" as const,
|
||||
},
|
||||
};
|
||||
|
||||
export const API_KEY_REQUIRED_MESSAGE =
|
||||
"Unknown users must provide API keys to use the Open SWE demo application";
|
||||
|
|
|
|||
|
|
@ -372,6 +372,11 @@ export const GraphConfigurationMetadata: {
|
|||
"JSON configuration for custom MCP servers. LangGraph docs server is set by default. See the `mcpServers` field of the LangChain MCP Adapters `ClientConfig` type for information on this schema. [Documentation here](https://v03.api.js.langchain.com/types/_langchain_mcp_adapters.ClientConfig.html).",
|
||||
},
|
||||
},
|
||||
apiKeys: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "hidden",
|
||||
},
|
||||
},
|
||||
[GITHUB_TOKEN_COOKIE]: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "hidden",
|
||||
|
|
@ -496,6 +501,12 @@ export const GraphConfiguration = z.object({
|
|||
maxTokens: withLangGraph(z.number().optional(), {
|
||||
metadata: GraphConfigurationMetadata.maxTokens,
|
||||
}),
|
||||
/**
|
||||
* User defined API keys to use
|
||||
*/
|
||||
apiKeys: withLangGraph(z.record(z.string(), z.string()).optional(), {
|
||||
metadata: GraphConfigurationMetadata.apiKeys,
|
||||
}),
|
||||
/**
|
||||
* The user's GitHub access token. To be used in requests to get information about the user.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue