From 97bfa96a15c90be09bd93c99f0a1d2c87abab7dc Mon Sep 17 00:00:00 2001 From: chuckbutkus Date: Mon, 4 Aug 2025 17:50:59 -0400 Subject: [PATCH] Enterprise sso (#10008) Co-authored-by: openhands Co-authored-by: Rohit Malhotra --- .../components/user-actions.test.tsx | 149 ++++++++++++------ .../routes/secrets-settings.test.tsx | 3 +- .../account-settings-context-menu.tsx | 2 +- .../features/sidebar/user-actions.tsx | 7 +- .../features/sidebar/user-avatar.tsx | 1 + .../features/waitlist/auth-modal.tsx | 27 ++++ .../conversation-subscriptions-provider.tsx | 6 +- frontend/src/hooks/query/use-get-secrets.ts | 6 +- frontend/src/hooks/query/use-git-user.ts | 6 +- frontend/src/hooks/use-auto-login.ts | 8 + frontend/src/i18n/declaration.ts | 1 + frontend/src/i18n/translation.json | 16 ++ frontend/src/types/settings.ts | 1 + frontend/src/utils/local-storage.ts | 1 + openhands/integrations/service_types.py | 1 + openhands/server/app.py | 6 +- poetry.lock | 23 ++- pyproject.toml | 1 + 18 files changed, 203 insertions(+), 62 deletions(-) diff --git a/frontend/__tests__/components/user-actions.test.tsx b/frontend/__tests__/components/user-actions.test.tsx index 4009efd738..9e9f2bdc4e 100644 --- a/frontend/__tests__/components/user-actions.test.tsx +++ b/frontend/__tests__/components/user-actions.test.tsx @@ -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 }) => ( + + {children} + + ), + }); + }; + + 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(); + renderWithQueryClient(); 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( { }); it("should call onLogout and close the menu when the logout option is clicked", async () => { - render( + renderWithQueryClient( { ).not.toBeInTheDocument(); }); - it("should show context menu when user is undefined and avatar is clicked", async () => { - render(); + 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(); 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(); + renderWithQueryClient( + , + ); 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(); + 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(); 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(); + // 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( + , + ); - // 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( - , + + + , ); // 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( { screen.getByTestId("account-settings-context-menu"), ).toBeInTheDocument(); - // Remove user prop - menu should still be visible - rerender(); + // 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( + + + , + ); + + // 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( { 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(), ); diff --git a/frontend/src/components/features/context-menu/account-settings-context-menu.tsx b/frontend/src/components/features/context-menu/account-settings-context-menu.tsx index 71829ef903..1db3ef0f8c 100644 --- a/frontend/src/components/features/context-menu/account-settings-context-menu.tsx +++ b/frontend/src/components/features/context-menu/account-settings-context-menu.tsx @@ -22,7 +22,7 @@ export function AccountSettingsContextMenu({ ref={ref} className="absolute right-full md:left-full -top-1 z-10 w-fit" > - + {t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)} diff --git a/frontend/src/components/features/sidebar/user-actions.tsx b/frontend/src/components/features/sidebar/user-actions.tsx index 591337e003..89f87f9269 100644 --- a/frontend/src/components/features/sidebar/user-actions.tsx +++ b/frontend/src/components/features/sidebar/user-actions.tsx @@ -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 (
- {accountContextMenuIsVisible && ( + {showMenu && ( { 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)} )} + + {showEnterpriseSso && ( + + {t(I18nKey.ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO)} + + )} )}
diff --git a/frontend/src/context/conversation-subscriptions-provider.tsx b/frontend/src/context/conversation-subscriptions-provider.tsx index 93adad525b..36ebe3e190 100644 --- a/frontend/src/context/conversation-subscriptions-provider.tsx +++ b/frontend/src/context/conversation-subscriptions-provider.tsx @@ -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, diff --git a/frontend/src/hooks/query/use-get-secrets.ts b/frontend/src/hooks/query/use-get-secrets.ts index e031222d58..05bae09ef4 100644 --- a/frontend/src/hooks/query/use-get-secrets.ts +++ b/frontend/src/hooks/query/use-get-secrets.ts @@ -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 }); }; diff --git a/frontend/src/hooks/query/use-git-user.ts b/frontend/src/hooks/query/use-git-user.ts index b1b60a40f2..dcb5750a6b 100644 --- a/frontend/src/hooks/query/use-git-user.ts +++ b/frontend/src/hooks/query/use-git-user.ts @@ -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 diff --git a/frontend/src/hooks/use-auto-login.ts b/frontend/src/hooks/use-auto-login.ts index 2dfab7fd1f..83e4293b9a 100644 --- a/frontend/src/hooks/use-auto-login.ts +++ b/frontend/src/hooks/use-auto-login.ts @@ -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, ]); }; diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts index 7c4b67ef6e..fe8421432c 100644 --- a/frontend/src/i18n/declaration.ts +++ b/frontend/src/i18n/declaration.ts @@ -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", diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 99e0aa9dc3..6906782a63 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -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プロバイダーでサインイン", diff --git a/frontend/src/types/settings.ts b/frontend/src/types/settings.ts index a083429b38..03f72a4a85 100644 --- a/frontend/src/types/settings.ts +++ b/frontend/src/types/settings.ts @@ -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; diff --git a/frontend/src/utils/local-storage.ts b/frontend/src/utils/local-storage.ts index d6ab3e29d2..ffdf14a164 100644 --- a/frontend/src/utils/local-storage.ts +++ b/frontend/src/utils/local-storage.ts @@ -8,6 +8,7 @@ export enum LoginMethod { GITHUB = "github", GITLAB = "gitlab", BITBUCKET = "bitbucket", + ENTERPRISE_SSO = "enterprise_sso", } /** diff --git a/openhands/integrations/service_types.py b/openhands/integrations/service_types.py index 75a08538d5..8bde8822a9 100644 --- a/openhands/integrations/service_types.py +++ b/openhands/integrations/service_types.py @@ -18,6 +18,7 @@ class ProviderType(Enum): GITHUB = 'github' GITLAB = 'gitlab' BITBUCKET = 'bitbucket' + ENTERPRISE_SSO = 'enterprise_sso' class TaskType(str, Enum): diff --git a/openhands/server/app.py b/openhands/server/app.py index f1c0ad7073..bae0e70fde 100644 --- a/openhands/server/app.py +++ b/openhands/server/app.py @@ -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) diff --git a/poetry.lock b/poetry.lock index d1ca9adaa7..dfdc5b3a40 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 71f041b094..c18b3d20a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" ]