mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Enterprise sso (#10008)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
This commit is contained in:
parent
0e2f2f4173
commit
97bfa96a15
@ -1,27 +1,64 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, test, vi, afterEach } from "vitest";
|
||||
import { describe, expect, it, test, vi, afterEach, beforeEach } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { UserActions } from "#/components/features/sidebar/user-actions";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
// Create a mock for useIsAuthed that we can control per test
|
||||
const useIsAuthedMock = vi
|
||||
.fn()
|
||||
.mockReturnValue({ data: true, isLoading: false });
|
||||
|
||||
// Mock the useIsAuthed hook
|
||||
vi.mock("#/hooks/query/use-is-authed", () => ({
|
||||
useIsAuthed: () => useIsAuthedMock(),
|
||||
}));
|
||||
|
||||
describe("UserActions", () => {
|
||||
const user = userEvent.setup();
|
||||
const onClickAccountSettingsMock = vi.fn();
|
||||
const onLogoutMock = vi.fn();
|
||||
|
||||
// Create a wrapper with QueryClientProvider
|
||||
const renderWithQueryClient = (ui: ReactElement) => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return render(ui, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the mock to default value before each test
|
||||
useIsAuthedMock.mockReturnValue({ data: true, isLoading: false });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
onClickAccountSettingsMock.mockClear();
|
||||
onLogoutMock.mockClear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
renderWithQueryClient(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
expect(screen.getByTestId("user-actions")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should toggle the user menu when the user avatar is clicked", async () => {
|
||||
render(
|
||||
renderWithQueryClient(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
@ -43,7 +80,7 @@ describe("UserActions", () => {
|
||||
});
|
||||
|
||||
it("should call onLogout and close the menu when the logout option is clicked", async () => {
|
||||
render(
|
||||
renderWithQueryClient(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
@ -62,20 +99,25 @@ describe("UserActions", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show context menu when user is undefined and avatar is clicked", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
it("should NOT show context menu when user is not authenticated and avatar is clicked", async () => {
|
||||
// Set isAuthed to false for this test
|
||||
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
|
||||
|
||||
renderWithQueryClient(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
// Context menu SHOULD appear even when user is undefined
|
||||
// Context menu should NOT appear because user is not authenticated
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show context menu even when user has no avatar_url", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} user={{ avatar_url: "" }} />);
|
||||
renderWithQueryClient(
|
||||
<UserActions onLogout={onLogoutMock} user={{ avatar_url: "" }} />,
|
||||
);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
@ -86,52 +128,66 @@ describe("UserActions", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should be able to access logout even when no user is provided", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
it("should NOT be able to access logout when user is not authenticated", async () => {
|
||||
// Set isAuthed to false for this test
|
||||
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
|
||||
|
||||
renderWithQueryClient(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
// Logout option should be accessible even when no user is provided
|
||||
// Context menu should NOT appear because user is not authenticated
|
||||
expect(
|
||||
screen.getByText("ACCOUNT_SETTINGS$LOGOUT"),
|
||||
).toBeInTheDocument();
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Verify logout works
|
||||
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
|
||||
await user.click(logoutOption);
|
||||
expect(onLogoutMock).toHaveBeenCalledOnce();
|
||||
// Logout option should NOT be accessible when user is not authenticated
|
||||
expect(screen.queryByText("ACCOUNT_SETTINGS$LOGOUT")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle user prop changing from undefined to defined", async () => {
|
||||
const { rerender } = render(<UserActions onLogout={onLogoutMock} />);
|
||||
// Start with no authentication
|
||||
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
|
||||
|
||||
// Initially no user - but we can still click to show the menu
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
const { rerender } = renderWithQueryClient(
|
||||
<UserActions onLogout={onLogoutMock} />,
|
||||
);
|
||||
|
||||
// Close the menu
|
||||
// Initially no user and not authenticated - menu should not appear
|
||||
let userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
expect(
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Add user prop
|
||||
// Set authentication to true for the rerender
|
||||
useIsAuthedMock.mockReturnValue({ data: true, isLoading: false });
|
||||
|
||||
// Add user prop and create a new QueryClient to ensure fresh state
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
rerender(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
/>,
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Component should still render correctly
|
||||
expect(screen.getByTestId("user-actions")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
|
||||
|
||||
// Menu should still work with user defined
|
||||
// Menu should now work with user defined and authenticated
|
||||
userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
@ -139,7 +195,7 @@ describe("UserActions", () => {
|
||||
});
|
||||
|
||||
it("should handle user prop changing from defined to undefined", async () => {
|
||||
const { rerender } = render(
|
||||
const { rerender } = renderWithQueryClient(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
@ -153,22 +209,27 @@ describe("UserActions", () => {
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Remove user prop - menu should still be visible
|
||||
rerender(<UserActions onLogout={onLogoutMock} />);
|
||||
// Set authentication to false for the rerender
|
||||
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
|
||||
|
||||
// Context menu should remain visible even when user becomes undefined
|
||||
// Remove user prop - menu should disappear because user is no longer authenticated
|
||||
rerender(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<UserActions onLogout={onLogoutMock} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Context menu should NOT be visible when user becomes unauthenticated
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Verify logout still works
|
||||
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
|
||||
await user.click(logoutOption);
|
||||
expect(onLogoutMock).toHaveBeenCalledOnce();
|
||||
// Logout option should not be accessible
|
||||
expect(screen.queryByText("ACCOUNT_SETTINGS$LOGOUT")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should work with loading state and user provided", async () => {
|
||||
render(
|
||||
renderWithQueryClient(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
|
||||
@ -101,7 +101,8 @@ describe("Content", () => {
|
||||
|
||||
renderSecretsSettings();
|
||||
|
||||
expect(getSecretsSpy).not.toHaveBeenCalled();
|
||||
// In SAAS mode, getSecrets is still called because the user is authenticated
|
||||
await waitFor(() => expect(getSecretsSpy).toHaveBeenCalled());
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument(),
|
||||
);
|
||||
|
||||
@ -22,7 +22,7 @@ export function AccountSettingsContextMenu({
|
||||
ref={ref}
|
||||
className="absolute right-full md:left-full -top-1 z-10 w-fit"
|
||||
>
|
||||
<ContextMenuListItem onClick={onLogout}>
|
||||
<ContextMenuListItem onClick={onLogout} data-testid="logout-button">
|
||||
{t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)}
|
||||
</ContextMenuListItem>
|
||||
</ContextMenu>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
import { AccountSettingsContextMenu } from "../context-menu/account-settings-context-menu";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
|
||||
interface UserActionsProps {
|
||||
onLogout: () => void;
|
||||
@ -9,6 +10,7 @@ interface UserActionsProps {
|
||||
}
|
||||
|
||||
export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
const [accountContextMenuIsVisible, setAccountContextMenuIsVisible] =
|
||||
React.useState(false);
|
||||
|
||||
@ -26,6 +28,9 @@ export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
|
||||
closeAccountMenu();
|
||||
};
|
||||
|
||||
// Always show the menu for authenticated users, even without user data
|
||||
const showMenu = accountContextMenuIsVisible && isAuthed;
|
||||
|
||||
return (
|
||||
<div data-testid="user-actions" className="w-8 h-8 relative cursor-pointer">
|
||||
<UserAvatar
|
||||
@ -34,7 +39,7 @@ export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{accountContextMenuIsVisible && (
|
||||
{showMenu && (
|
||||
<AccountSettingsContextMenu
|
||||
onLogout={handleLogout}
|
||||
onClose={closeAccountMenu}
|
||||
|
||||
@ -14,6 +14,7 @@ interface UserAvatarProps {
|
||||
|
||||
export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipButton
|
||||
testId="user-avatar"
|
||||
|
||||
@ -35,6 +35,11 @@ export function AuthModal({
|
||||
identityProvider: "bitbucket",
|
||||
});
|
||||
|
||||
const enterpriseSsoUrl = useAuthUrl({
|
||||
appMode: appMode || null,
|
||||
identityProvider: "enterprise_sso",
|
||||
});
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
if (githubAuthUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
@ -56,6 +61,13 @@ export function AuthModal({
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnterpriseSsoAuth = () => {
|
||||
if (enterpriseSsoUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = enterpriseSsoUrl;
|
||||
}
|
||||
};
|
||||
|
||||
// Only show buttons if providers are configured and include the specific provider
|
||||
const showGithub =
|
||||
providersConfigured &&
|
||||
@ -69,6 +81,10 @@ export function AuthModal({
|
||||
providersConfigured &&
|
||||
providersConfigured.length > 0 &&
|
||||
providersConfigured.includes("bitbucket");
|
||||
const showEnterpriseSso =
|
||||
providersConfigured &&
|
||||
providersConfigured.length > 0 &&
|
||||
providersConfigured.includes("enterprise_sso");
|
||||
|
||||
// Check if no providers are configured
|
||||
const noProvidersConfigured =
|
||||
@ -126,6 +142,17 @@ export function AuthModal({
|
||||
{t(I18nKey.BITBUCKET$CONNECT_TO_BITBUCKET)}
|
||||
</BrandButton>
|
||||
)}
|
||||
|
||||
{showEnterpriseSso && (
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleEnterpriseSsoAuth}
|
||||
className="w-full"
|
||||
>
|
||||
{t(I18nKey.ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO)}
|
||||
</BrandButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -31,7 +31,7 @@ interface ConversationSubscriptionsContextType {
|
||||
subscribeToConversation: (options: {
|
||||
conversationId: string;
|
||||
sessionApiKey: string | null;
|
||||
providersSet: ("github" | "gitlab" | "bitbucket")[];
|
||||
providersSet: ("github" | "gitlab" | "bitbucket" | "enterprise_sso")[];
|
||||
baseUrl: string;
|
||||
onEvent?: (event: unknown, conversationId: string) => void;
|
||||
}) => void;
|
||||
@ -135,7 +135,7 @@ export function ConversationSubscriptionsProvider({
|
||||
(options: {
|
||||
conversationId: string;
|
||||
sessionApiKey: string | null;
|
||||
providersSet: ("github" | "gitlab" | "bitbucket")[];
|
||||
providersSet: ("github" | "gitlab" | "bitbucket" | "enterprise_sso")[];
|
||||
baseUrl: string;
|
||||
onEvent?: (event: unknown, conversationId: string) => void;
|
||||
}) => {
|
||||
@ -226,6 +226,7 @@ export function ConversationSubscriptionsProvider({
|
||||
});
|
||||
|
||||
socket.on("connect_error", (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Socket for conversation ${conversationId} CONNECTION ERROR:`,
|
||||
error,
|
||||
@ -233,6 +234,7 @@ export function ConversationSubscriptionsProvider({
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Socket for conversation ${conversationId} DISCONNECTED! Reason:`,
|
||||
reason,
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { SecretsService } from "#/api/secrets-service";
|
||||
import { useUserProviders } from "../use-user-providers";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
|
||||
export const useGetSecrets = () => {
|
||||
const { data: config } = useConfig();
|
||||
const { providers } = useUserProviders();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
|
||||
const isOss = config?.APP_MODE === "oss";
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["secrets"],
|
||||
queryFn: SecretsService.getSecrets,
|
||||
enabled: isOss || providers.length > 0,
|
||||
enabled: isOss || isAuthed, // Enable regardless of providers
|
||||
});
|
||||
};
|
||||
|
||||
@ -3,16 +3,16 @@ import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useConfig } from "./use-config";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useUserProviders } from "../use-user-providers";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
|
||||
export const useGitUser = () => {
|
||||
const { providers } = useUserProviders();
|
||||
const { data: config } = useConfig();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
|
||||
const user = useQuery({
|
||||
queryKey: ["user"],
|
||||
queryFn: OpenHands.getGitUser,
|
||||
enabled: !!config?.APP_MODE && providers.length > 0,
|
||||
enabled: !!config?.APP_MODE && isAuthed, // Enable regardless of providers
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
|
||||
@ -31,6 +31,11 @@ export const useAutoLogin = () => {
|
||||
identityProvider: "bitbucket",
|
||||
});
|
||||
|
||||
const enterpriseSsoUrl = useAuthUrl({
|
||||
appMode: config?.APP_MODE || null,
|
||||
identityProvider: "enterprise_sso",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Only auto-login in SAAS mode
|
||||
if (config?.APP_MODE !== "saas") {
|
||||
@ -60,6 +65,8 @@ export const useAutoLogin = () => {
|
||||
authUrl = gitlabAuthUrl;
|
||||
} else if (loginMethod === LoginMethod.BITBUCKET) {
|
||||
authUrl = bitbucketAuthUrl;
|
||||
} else if (loginMethod === LoginMethod.ENTERPRISE_SSO) {
|
||||
authUrl = enterpriseSsoUrl;
|
||||
}
|
||||
|
||||
// If we have an auth URL, redirect to it
|
||||
@ -80,5 +87,6 @@ export const useAutoLogin = () => {
|
||||
githubAuthUrl,
|
||||
gitlabAuthUrl,
|
||||
bitbucketAuthUrl,
|
||||
enterpriseSsoUrl,
|
||||
]);
|
||||
};
|
||||
|
||||
@ -559,6 +559,7 @@ export enum I18nKey {
|
||||
GITHUB$CONNECT_TO_GITHUB = "GITHUB$CONNECT_TO_GITHUB",
|
||||
GITLAB$CONNECT_TO_GITLAB = "GITLAB$CONNECT_TO_GITLAB",
|
||||
BITBUCKET$CONNECT_TO_BITBUCKET = "BITBUCKET$CONNECT_TO_BITBUCKET",
|
||||
ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO = "ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO",
|
||||
AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER = "AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER",
|
||||
WAITLIST$JOIN_WAITLIST = "WAITLIST$JOIN_WAITLIST",
|
||||
ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS = "ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS",
|
||||
|
||||
@ -8943,6 +8943,22 @@
|
||||
"tr": "Bitbucket'a bağlan",
|
||||
"uk": "Увійти за допомогою Bitbucket"
|
||||
},
|
||||
"ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO": {
|
||||
"en": "Login with Enterprise SSO",
|
||||
"ja": "エンタープライズSSOでログイン",
|
||||
"zh-CN": "使用企业SSO登录",
|
||||
"zh-TW": "使用企業SSO登入",
|
||||
"ko-KR": "엔터프라이즈 SSO로 로그인",
|
||||
"de": "Mit Enterprise SSO anmelden",
|
||||
"no": "Logg inn med Enterprise SSO",
|
||||
"it": "Accedi con Enterprise SSO",
|
||||
"pt": "Entrar com Enterprise SSO",
|
||||
"es": "Iniciar sesión con Enterprise SSO",
|
||||
"ar": "تسجيل الدخول باستخدام Enterprise SSO",
|
||||
"fr": "Se connecter avec Enterprise SSO",
|
||||
"tr": "Enterprise SSO ile giriş yap",
|
||||
"uk": "Увійти за допомогою Enterprise SSO"
|
||||
},
|
||||
"AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER": {
|
||||
"en": "Log in to OpenHands",
|
||||
"ja": "IDプロバイダーでサインイン",
|
||||
|
||||
@ -2,6 +2,7 @@ export const ProviderOptions = {
|
||||
github: "github",
|
||||
gitlab: "gitlab",
|
||||
bitbucket: "bitbucket",
|
||||
enterprise_sso: "enterprise_sso",
|
||||
} as const;
|
||||
|
||||
export type Provider = keyof typeof ProviderOptions;
|
||||
|
||||
@ -8,6 +8,7 @@ export enum LoginMethod {
|
||||
GITHUB = "github",
|
||||
GITLAB = "gitlab",
|
||||
BITBUCKET = "bitbucket",
|
||||
ENTERPRISE_SSO = "enterprise_sso",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -18,6 +18,7 @@ class ProviderType(Enum):
|
||||
GITHUB = 'github'
|
||||
GITLAB = 'gitlab'
|
||||
BITBUCKET = 'bitbucket'
|
||||
ENTERPRISE_SSO = 'enterprise_sso'
|
||||
|
||||
|
||||
class TaskType(str, Enum):
|
||||
|
||||
@ -28,7 +28,8 @@ from openhands.server.routes.secrets import app as secrets_router
|
||||
from openhands.server.routes.security import app as security_api_router
|
||||
from openhands.server.routes.settings import app as settings_router
|
||||
from openhands.server.routes.trajectory import app as trajectory_router
|
||||
from openhands.server.shared import conversation_manager
|
||||
from openhands.server.shared import conversation_manager, server_config
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
mcp_app = mcp_server.http_app(path='/mcp')
|
||||
|
||||
@ -68,6 +69,7 @@ app.include_router(conversation_api_router)
|
||||
app.include_router(manage_conversation_api_router)
|
||||
app.include_router(settings_router)
|
||||
app.include_router(secrets_router)
|
||||
app.include_router(git_api_router)
|
||||
if server_config.app_mode == AppMode.OSS:
|
||||
app.include_router(git_api_router)
|
||||
app.include_router(trajectory_router)
|
||||
add_health_endpoints(app)
|
||||
|
||||
23
poetry.lock
generated
23
poetry.lock
generated
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
@ -3770,6 +3770,22 @@ http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-aiohttp"
|
||||
version = "0.1.8"
|
||||
description = "Aiohttp transport for HTTPX"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpx_aiohttp-0.1.8-py3-none-any.whl", hash = "sha256:b7bd958d1331f3759a38a0ba22ad29832cb63ca69498c17735228055bf78fa7e"},
|
||||
{file = "httpx_aiohttp-0.1.8.tar.gz", hash = "sha256:756c5e74cdb568c3248ba63fe82bfe8bbe64b928728720f7eaac64b3cf46f308"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.10.0,<4"
|
||||
httpx = ">=0.27.0"
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.0"
|
||||
@ -5136,11 +5152,8 @@ files = [
|
||||
{file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"},
|
||||
{file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"},
|
||||
@ -11753,4 +11766,4 @@ third-party-runtimes = ["daytona", "e2b", "modal", "runloop-api-client"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.12,<3.14"
|
||||
content-hash = "d957f92f0d194e78b1cbf4b5a31c28df83e34e508d2c9810de96204db8e8f158"
|
||||
content-hash = "8568c6ec2e11d4fcb23e206a24896b4d2d50e694c04011b668148f484e95b406"
|
||||
|
||||
@ -99,6 +99,7 @@ e2b = { version = ">=1.0.5,<1.8.0", optional = true }
|
||||
modal = { version = ">=0.66.26,<1.2.0", optional = true }
|
||||
runloop-api-client = { version = "0.50.0", optional = true }
|
||||
daytona = { version = "0.24.2", optional = true }
|
||||
httpx-aiohttp = "^0.1.8"
|
||||
|
||||
[tool.poetry.extras]
|
||||
third_party_runtimes = [ "e2b", "modal", "runloop-api-client", "daytona" ]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user