Compare commits

..

7 Commits

Author SHA1 Message Date
Tim O'Farrell
85b959d883 chore: remove unused items from openhands.server package (#14202)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 17:57:23 -06:00
Tim O'Farrell
1ee548b909 Remove unused items from openhands.core package (#14201)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 17:05:57 -06:00
Tim O'Farrell
21aa52ce3b Move openhands.server.user_auth to openhands.app_server.user_auth (#14199)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 15:00:50 -06:00
Tim O'Farrell
1e023ce56b Remove unused legacy V0 server modules (#14198)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 14:47:26 -06:00
Tim O'Farrell
5b500d640a refactor: move openhands.integrations to openhands.app_server.integrations (#14195)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 14:21:14 -06:00
Tim O'Farrell
c824b2dda5 refactor: move FileStore to openhands.app_server.file_store (#14178)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 13:10:31 -06:00
Rohit Malhotra
c9b6f54e76 fix: correct GLOBAL_SKILLS_DIR path for skills settings page (#14194)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 19:08:04 +00:00
269 changed files with 1204 additions and 2718 deletions

View File

@@ -27,7 +27,7 @@ Before pushing any changes, you MUST ensure that any lint errors or simple test
* If you've made changes to the backend, you should run `pre-commit run --config ./dev_config/python/.pre-commit-config.yaml` (this will run on staged files).
* If you've made changes to the frontend, you should run `cd frontend && npm run lint:fix && npm run build ; cd ..`
* If you've made changes to the VSCode extension, you should run `cd openhands/integrations/vscode && npm run lint:fix && npm run compile ; cd ../../..`
* If you've made changes to the VSCode extension, you should run `cd openhands/app_server/integrations/vscode && npm run lint:fix && npm run compile ; cd ../../..`
The pre-commit hooks MUST pass successfully before pushing any changes to the repository. This is a mandatory requirement to maintain code quality and consistency.
@@ -150,7 +150,7 @@ Frontend:
VSCode Extension:
- Located in the `openhands/integrations/vscode` directory
- Located in the `openhands/app_server/integrations/vscode` directory
- Setup: Run `npm install` in the extension directory
- Linting:
- Run linting with fixes: `npm run lint:fix`

View File

@@ -1,9 +1,11 @@
from pydantic import SecretStr
from server.auth.token_manager import TokenManager
from openhands.app_server.integrations.bitbucket.bitbucket_service import (
BitBucketService,
)
from openhands.app_server.integrations.service_types import ProviderType
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.bitbucket.bitbucket_service import BitBucketService
from openhands.integrations.service_types import ProviderType
class SaaSBitBucketService(BitBucketService):

View File

@@ -1,11 +1,11 @@
from pydantic import SecretStr
from server.auth.token_manager import TokenManager
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.bitbucket_data_center.bitbucket_dc_service import (
from openhands.app_server.integrations.bitbucket_data_center.bitbucket_dc_service import (
BitbucketDCService,
)
from openhands.integrations.service_types import ProviderType
from openhands.app_server.integrations.service_types import ProviderType
from openhands.core.logger import openhands_logger as logger
class SaaSBitbucketDCService(BitbucketDCService):

View File

@@ -19,12 +19,12 @@ from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
from storage.openhands_pr import OpenhandsPR
from storage.openhands_pr_store import OpenhandsPRStore
from openhands.app_server.conversation_paths import get_conversation_dir
from openhands.app_server.file_store import get_file_store
from openhands.app_server.integrations.github.github_service import GithubServiceImpl
from openhands.app_server.integrations.service_types import ProviderType
from openhands.core.config import load_openhands_config
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.github.github_service import GithubServiceImpl
from openhands.integrations.service_types import ProviderType
from openhands.storage import get_file_store
from openhands.storage.locations import get_conversation_dir
config = load_openhands_config()
file_store = get_file_store(config.file_store, config.file_store_path)
@@ -112,7 +112,7 @@ class GitHubDataCollector:
suffix = path.format(repo_id, number)
if conversation_id:
return f'{get_conversation_dir(conversation_id)}{suffix}'
return f'{get_conversation_dir(conversation_id)}/{suffix}'
return suffix

View File

@@ -31,10 +31,10 @@ from server.auth.auth_error import ExpiredError
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
from server.auth.token_manager import TokenManager
from openhands.app_server.integrations.provider import ProviderToken, ProviderType
from openhands.app_server.integrations.service_types import AuthenticationError
from openhands.app_server.secrets.secrets_models import Secrets
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.integrations.service_types import AuthenticationError
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,

View File

@@ -4,9 +4,9 @@ from integrations.store_repo_utils import store_repositories_in_db
from pydantic import SecretStr
from server.auth.token_manager import TokenManager
from openhands.app_server.integrations.github.github_service import GitHubService
from openhands.app_server.integrations.service_types import ProviderType, Repository
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.github.github_service import GitHubService
from openhands.integrations.service_types import ProviderType, Repository
from openhands.server.types import AppMode

View File

@@ -34,14 +34,14 @@ from openhands.app_server.app_conversation.app_conversation_models import (
ConversationTrigger,
)
from openhands.app_server.config import get_app_conversation_service
from openhands.app_server.integrations.github.github_service import GithubServiceImpl
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.app_server.integrations.service_types import Comment
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.github.github_service import GithubServiceImpl
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.integrations.service_types import Comment
from openhands.sdk import TextContent
from openhands.server.user_auth.user_auth import UserAuth
from openhands.utils.async_utils import call_sync_from_async
OH_LABEL, INLINE_OH_LABEL = get_oh_labels(HOST)

View File

@@ -25,10 +25,10 @@ from jinja2 import Environment, FileSystemLoader
from pydantic import SecretStr
from server.auth.token_manager import TokenManager
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.app_server.integrations.provider import ProviderToken, ProviderType
from openhands.app_server.secrets.secrets_models import Secrets
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,

View File

@@ -7,14 +7,14 @@ from server.auth.token_manager import TokenManager
from storage.gitlab_webhook import GitlabWebhook, WebhookStatus
from storage.gitlab_webhook_store import GitlabWebhookStore
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.gitlab.gitlab_service import GitLabService
from openhands.integrations.service_types import (
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabService
from openhands.app_server.integrations.service_types import (
ProviderType,
RateLimitError,
Repository,
RequestMethod,
)
from openhands.core.logger import openhands_logger as logger
from openhands.server.types import AppMode

View File

@@ -22,14 +22,14 @@ from openhands.app_server.app_conversation.app_conversation_models import (
ConversationTrigger,
)
from openhands.app_server.config import get_app_conversation_service
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.app_server.integrations.service_types import Comment
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.integrations.service_types import Comment
from openhands.sdk import TextContent
from openhands.server.user_auth.user_auth import UserAuth
OH_LABEL, INLINE_OH_LABEL = get_oh_labels(HOST)
CONFIDENTIAL_NOTE = 'confidential_note'

View File

@@ -42,13 +42,13 @@ from storage.jira_integration_store import JiraIntegrationStore
from storage.jira_user import JiraUser
from storage.jira_workspace import JiraWorkspace
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.core.logger import openhands_logger as logger
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,
SessionExpiredError,
)
from openhands.server.user_auth.user_auth import UserAuth
from openhands.utils.http_session import httpx_verify_option
JIRA_CLOUD_API_URL = 'https://api.atlassian.com/ex/jira'

View File

@@ -7,7 +7,7 @@ from jinja2 import Environment
from storage.jira_user import JiraUser
from storage.jira_workspace import JiraWorkspace
from openhands.server.user_auth.user_auth import UserAuth
from openhands.app_server.user_auth.user_auth import UserAuth
if TYPE_CHECKING:
from integrations.jira.jira_payload import JiraWebhookPayload

View File

@@ -38,12 +38,12 @@ from openhands.app_server.app_conversation.app_conversation_models import (
ConversationTrigger,
)
from openhands.app_server.config import get_app_conversation_service
from openhands.app_server.integrations.provider import ProviderHandler, ProviderType
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler, ProviderType
from openhands.sdk import TextContent
from openhands.server.user_auth.user_auth import UserAuth
from openhands.utils.http_session import httpx_verify_option
JIRA_CLOUD_API_URL = 'https://api.atlassian.com/ex/jira'

View File

@@ -29,16 +29,16 @@ from storage.jira_dc_integration_store import JiraDcIntegrationStore
from storage.jira_dc_user import JiraDcUser
from storage.jira_dc_workspace import JiraDcWorkspace
from openhands.app_server.integrations.provider import ProviderHandler
from openhands.app_server.integrations.service_types import Repository
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler
from openhands.integrations.service_types import Repository
from openhands.server.shared import server_config
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,
SessionExpiredError,
)
from openhands.server.user_auth.user_auth import UserAuth
from openhands.utils.http_session import httpx_verify_option

View File

@@ -5,7 +5,7 @@ from jinja2 import Environment
from storage.jira_dc_user import JiraDcUser
from storage.jira_dc_workspace import JiraDcWorkspace
from openhands.server.user_auth.user_auth import UserAuth
from openhands.app_server.user_auth.user_auth import UserAuth
class JiraDcViewInterface(ABC):

View File

@@ -30,12 +30,12 @@ from openhands.app_server.app_conversation.app_conversation_models import (
ConversationTrigger,
)
from openhands.app_server.config import get_app_conversation_service
from openhands.app_server.integrations.provider import ProviderHandler, ProviderType
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler, ProviderType
from openhands.sdk import TextContent
from openhands.server.user_auth.user_auth import UserAuth
integration_store = JiraDcIntegrationStore.get_instance()

View File

@@ -1,11 +1,14 @@
from uuid import UUID
from openhands.app_server.integrations.provider import (
PROVIDER_TOKEN_TYPE,
ProviderHandler,
)
from openhands.app_server.integrations.service_types import ProviderType, UserGitInfo
from openhands.app_server.user.user_context import UserContext
from openhands.app_server.user.user_models import UserInfo
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler
from openhands.integrations.service_types import ProviderType, UserGitInfo
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.sdk.secret import SecretSource, StaticSecret
from openhands.server.user_auth.user_auth import UserAuth
class ResolverUserContext(UserContext):

View File

@@ -30,20 +30,20 @@ from sqlalchemy import select
from storage.database import a_session_maker
from storage.slack_user import SlackUser
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler
from openhands.integrations.service_types import (
from openhands.app_server.integrations.provider import ProviderHandler
from openhands.app_server.integrations.service_types import (
AuthenticationError,
ProviderTimeoutError,
Repository,
)
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.core.logger import openhands_logger as logger
from openhands.server.shared import config, server_config, sio
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,
SessionExpiredError,
)
from openhands.server.user_auth.user_auth import UserAuth
authorize_url_generator = AuthorizeUrlGenerator(
client_id=SLACK_CLIENT_ID,

View File

@@ -5,7 +5,7 @@ from integrations.types import SummaryExtractionTracker
from jinja2 import Environment
from storage.slack_user import SlackUser
from openhands.server.user_auth.user_auth import UserAuth
from openhands.app_server.user_auth.user_auth import UserAuth
@dataclass

View File

@@ -30,13 +30,13 @@ from openhands.app_server.app_conversation.app_conversation_models import (
SendMessageRequest,
)
from openhands.app_server.config import get_app_conversation_service
from openhands.app_server.integrations.provider import ProviderHandler
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler
from openhands.sdk import TextContent
from openhands.server.user_auth.user_auth import UserAuth
from openhands.utils.async_utils import GENERAL_TIMEOUT
# =================================================

View File

@@ -3,9 +3,9 @@ from storage.stored_repository import StoredRepository
from storage.user_repo_map import UserRepositoryMap
from storage.user_repo_map_store import UserRepositoryMapStore
from openhands.app_server.integrations.service_types import Repository
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import Repository
async def store_repositories_in_db(repos: list[Repository], user_id: str) -> None:

View File

@@ -9,8 +9,8 @@ from pydantic import BaseModel
if TYPE_CHECKING:
from integrations.models import Message
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.server.user_auth.user_auth import UserAuth
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.app_server.user_auth.user_auth import UserAuth
class GitLabResourceType(Enum):

View File

@@ -6,7 +6,7 @@ import re
from jinja2 import Environment, FileSystemLoader
from server.constants import WEB_HOST
from openhands.integrations.service_types import Repository
from openhands.app_server.integrations.service_types import Repository
# ---- DO NOT REMOVE ----
# WARNING: Langfuse depends on the WEB_HOST environment variable being set to track events.
@@ -65,7 +65,7 @@ def get_user_not_found_message(username: str | None = None) -> str:
OPENHANDS_RESOLVER_TEMPLATES_DIR = (
os.getenv('OPENHANDS_RESOLVER_TEMPLATES_DIR')
or 'openhands/integrations/templates/resolver/'
or 'openhands/app_server/integrations/templates/resolver/'
)
_jinja_env = Environment(loader=FileSystemLoader(OPENHANDS_RESOLVER_TEMPLATES_DIR))

View File

@@ -7,8 +7,8 @@ from pydantic import SecretStr
from server.auth.saas_user_auth import SaasUserAuth
from server.auth.token_manager import TokenManager
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth.user_auth import UserAuth
def is_budget_exceeded_error(error_message: str) -> bool:

View File

@@ -40,8 +40,8 @@ from storage.org_member_store import OrgMemberStore
from storage.role import Role
from storage.role_store import RoleStore
from openhands.app_server.user_auth import get_user_auth, get_user_id
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth import get_user_auth, get_user_id
class Permission(str, Enum):

View File

@@ -1,6 +1,6 @@
import os
from openhands.integrations.gitlab.constants import GITLAB_HOST
from openhands.app_server.integrations.gitlab.constants import GITLAB_HOST
GITHUB_APP_CLIENT_ID = os.getenv('GITHUB_APP_CLIENT_ID', '').strip()
GITHUB_APP_CLIENT_SECRET = os.getenv('GITHUB_APP_CLIENT_SECRET', '').strip()

View File

@@ -2,8 +2,8 @@ from integrations.github.github_service import SaaSGitHubService
from pydantic import SecretStr
from server.auth.auth_utils import user_verifier
from openhands.app_server.integrations.github.github_types import GitHubUser
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.github.github_types import GitHubUser
def is_user_allowed(user_login: str):

View File

@@ -3,8 +3,8 @@ import asyncio
from pydantic import SecretStr
from sqlalchemy import select
from openhands.app_server.integrations.service_types import ProviderType
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import ProviderType
from openhands.server.types import AppMode
@@ -55,7 +55,7 @@ def schedule_gitlab_repo_sync(
# Lazy import to avoid circular dependency:
# middleware -> gitlab_sync -> integrations.gitlab.gitlab_service
# -> openhands.integrations.gitlab.gitlab_service -> get_impl
# -> openhands.app_server.integrations.gitlab.gitlab_service -> get_impl
# -> integrations.gitlab.gitlab_service (circular)
from integrations.gitlab.gitlab_service import SaaSGitLabService

View File

@@ -35,15 +35,15 @@ from storage.user_authorization_store import UserAuthorizationStore
from storage.user_store import UserStore
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
from openhands.app_server.secrets.secrets_models import Secrets
from openhands.app_server.settings.settings_models import Settings
from openhands.app_server.settings.settings_store import SettingsStore
from openhands.integrations.provider import (
from openhands.app_server.integrations.provider import (
PROVIDER_TOKEN_TYPE,
ProviderToken,
ProviderType,
)
from openhands.server.user_auth.user_auth import AuthType, UserAuth
from openhands.app_server.secrets.secrets_models import Secrets
from openhands.app_server.settings.settings_models import Settings
from openhands.app_server.settings.settings_store import SettingsStore
from openhands.app_server.user_auth.user_auth import AuthType, UserAuth
token_manager = TokenManager()

View File

@@ -51,7 +51,7 @@ from storage.github_app_installation import GithubAppInstallation
from storage.offline_token_store import OfflineTokenStore
from tenacity import RetryCallState, retry, retry_if_exception_type, stop_after_attempt
from openhands.integrations.service_types import ProviderType
from openhands.app_server.integrations.service_types import ProviderType
from openhands.server.types import SessionExpiredError
from openhands.utils.http_session import httpx_verify_option

View File

@@ -22,8 +22,8 @@ from server.auth.constants import (
)
from server.constants import DEPLOYMENT_MODE
from openhands.app_server.integrations.service_types import ProviderType
from openhands.core.config.utils import load_openhands_config
from openhands.integrations.service_types import ProviderType
from openhands.server.config.server_config import ServerConfig
from openhands.server.types import AppMode

View File

@@ -4,8 +4,8 @@ Email domain validation utilities for enterprise endpoints.
from fastapi import Depends, HTTPException, Request, status
from openhands.app_server.user_auth import get_user_auth, get_user_id
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth import get_user_auth, get_user_id
async def get_admin_user_id(

View File

@@ -15,9 +15,9 @@ from server.auth.saas_user_auth import SaasUserAuth, token_manager
from server.routes.auth import set_response_cookie
from server.utils.url_utils import get_cookie_domain, get_cookie_samesite
from openhands.app_server.user_auth.user_auth import AuthType, UserAuth, get_user_auth
from openhands.core.logger import openhands_logger as logger
from openhands.server.shared import config
from openhands.server.user_auth.user_auth import AuthType, UserAuth, get_user_auth
class SetAuthCookieMiddleware:

View File

@@ -2,8 +2,8 @@
from pydantic import BaseModel
from openhands.app_server.integrations.service_types import ProviderType
from openhands.app_server.user.user_models import UserInfo
from openhands.integrations.service_types import ProviderType
class SaasUserInfo(UserInfo):

View File

@@ -12,9 +12,9 @@ from storage.org_member_store import OrgMemberStore
from storage.org_service import OrgService
from storage.user_store import UserStore
from openhands.app_server.user_auth import get_user_auth, get_user_id
from openhands.app_server.user_auth.user_auth import AuthType
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth import get_user_auth, get_user_id
from openhands.server.user_auth.user_auth import AuthType
# Helper functions for BYOR API key management

View File

@@ -3,6 +3,7 @@ import json
import uuid
import warnings
from datetime import datetime, timezone
from types import MappingProxyType
from typing import Annotated, Optional, cast
from urllib.parse import quote, urlencode
from uuid import UUID as parse_uuid
@@ -46,13 +47,16 @@ from storage.database import a_session_maker
from storage.user import User
from storage.user_store import UserStore
from openhands.app_server.integrations.provider import (
PROVIDER_TOKEN_TYPE,
ProviderHandler,
ProviderToken,
)
from openhands.app_server.integrations.service_types import ProviderType, TokenResponse
from openhands.app_server.user_auth import get_access_token
from openhands.app_server.user_auth.user_auth import get_user_auth
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler
from openhands.integrations.service_types import ProviderType, TokenResponse
from openhands.server.services.conversation_service import create_provider_tokens_object
from openhands.server.shared import config
from openhands.server.user_auth import get_access_token
from openhands.server.user_auth.user_auth import get_user_auth
with warnings.catch_warnings():
warnings.simplefilter('ignore')
@@ -63,6 +67,18 @@ oauth_router = APIRouter(prefix='/oauth')
token_manager = TokenManager()
def create_provider_tokens_object(
providers_set: list[ProviderType],
) -> PROVIDER_TOKEN_TYPE:
"""Create provider tokens object for the given providers."""
provider_information: dict[ProviderType, ProviderToken] = {}
for provider in providers_set:
provider_information[provider] = ProviderToken(token=None, user_id=None)
return MappingProxyType(provider_information)
def set_response_cookie(
request: Request,
response: Response,

View File

@@ -21,7 +21,7 @@ from storage.subscription_access import SubscriptionAccess
from storage.user_store import UserStore
from openhands.app_server.config import get_global_config
from openhands.server.user_auth import get_user_id
from openhands.app_server.user_auth import get_user_id
stripe.api_key = STRIPE_API_KEY
billing_router = APIRouter(prefix='/api/billing', tags=['Billing'])

View File

@@ -13,9 +13,9 @@ from server.utils.rate_limit_utils import check_rate_limit_by_user_id
from server.utils.url_utils import get_web_url
from storage.user_store import UserStore
from openhands.app_server.user_auth import get_user_id
from openhands.app_server.user_auth.user_auth import get_user_auth
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth import get_user_id
from openhands.server.user_auth.user_auth import get_user_auth
# Email validation regex pattern
EMAIL_REGEX = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')

View File

@@ -2,25 +2,21 @@ import asyncio
import hashlib
import hmac
import os
from typing import cast
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, status
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request
from fastapi.responses import JSONResponse
from integrations.github.data_collector import GitHubDataCollector
from integrations.github.github_manager import GithubManager
from integrations.models import Message, SourceType
from pydantic import BaseModel
from server.auth.constants import (
AUTOMATION_EVENT_FORWARDING_ENABLED,
GITHUB_APP_WEBHOOK_SECRET,
)
from server.auth.saas_user_auth import SaasUserAuth
from server.auth.token_manager import TokenManager
from server.services.automation_event_service import AutomationEventService
from openhands.app_server.integrations.provider import ProviderType
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderType
from openhands.server.user_auth.user_auth import get_user_auth
# Environment variable to disable GitHub webhooks
GITHUB_WEBHOOKS_ENABLED = os.environ.get('GITHUB_WEBHOOKS_ENABLED', '1') in (
@@ -109,42 +105,3 @@ async def github_events(
except Exception as e:
logger.exception(f'Error processing GitHub event: {e}')
return JSONResponse(status_code=400, content={'error': 'Invalid payload.'})
class GitHubTokenResponse(BaseModel):
"""Response model for the GitHub token endpoint."""
access_token: str
@github_integration_router.get('/github/token')
async def get_github_token(request: Request) -> GitHubTokenResponse:
"""Get the GitHub access token for the authenticated user.
This endpoint retrieves the user's GitHub OAuth token, refreshing it
if necessary. The token can be used for GitHub API operations.
Returns:
GitHubTokenResponse containing the access token.
Raises:
HTTPException 401: If the user is not authenticated.
HTTPException 404: If no GitHub token is available for the user.
"""
user_auth = cast(SaasUserAuth, await get_user_auth(request))
provider_tokens = await user_auth.get_provider_tokens()
if not provider_tokens:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='No provider tokens available for this user.',
)
github_token = provider_tokens.get(ProviderType.GITHUB)
if not github_token or not github_token.token:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='No GitHub token available for this user.',
)
return GitHubTokenResponse(access_token=github_token.token.get_secret_value())

View File

@@ -19,10 +19,10 @@ from server.auth.token_manager import TokenManager
from storage.gitlab_webhook import GitlabWebhook
from storage.gitlab_webhook_store import GitlabWebhookStore
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.app_server.user_auth import get_user_id
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.server.shared import sio
from openhands.server.user_auth import get_user_id
gitlab_integration_router = APIRouter(prefix='/integration')
webhook_store = GitlabWebhookStore()

View File

@@ -20,8 +20,8 @@ from server.auth.token_manager import TokenManager
from storage.jira_workspace import JiraWorkspace
from storage.redis import create_redis_client
from openhands.app_server.user_auth.user_auth import get_user_auth
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth.user_auth import get_user_auth
# Environment variable to disable Jira webhooks
JIRA_WEBHOOKS_ENABLED = os.environ.get('JIRA_WEBHOOKS_ENABLED', '0') in (

View File

@@ -28,8 +28,8 @@ from server.auth.token_manager import TokenManager
from server.constants import WEB_HOST
from storage.redis import create_redis_client
from openhands.app_server.user_auth.user_auth import get_user_auth
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth.user_auth import get_user_auth
# Environment variable to disable Jira DC webhooks
JIRA_DC_WEBHOOKS_ENABLED = os.environ.get('JIRA_DC_WEBHOOKS_ENABLED', '0') in (

View File

@@ -39,7 +39,10 @@ from storage.slack_team_store import SlackTeamStore
from storage.slack_user import SlackUser
from storage.user_store import UserStore
from openhands.integrations.service_types import ProviderTimeoutError, ProviderType
from openhands.app_server.integrations.service_types import (
ProviderTimeoutError,
ProviderType,
)
from openhands.server.shared import config, sio
signature_verifier = SignatureVerifier(signing_secret=SLACK_SIGNING_SECRET)

View File

@@ -10,8 +10,8 @@ from server.utils.url_utils import get_web_url
from storage.api_key_store import ApiKeyStore
from storage.device_code_store import DeviceCodeStore
from openhands.app_server.user_auth import get_user_id
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth import get_user_id
# ---------------------------------------------------------------------------
# Constants

View File

@@ -22,8 +22,8 @@ from server.utils.rate_limit_utils import check_rate_limit_by_user_id
from storage.org_store import OrgStore
from storage.role_store import RoleStore
from openhands.app_server.user_auth import get_user_id
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth import get_user_id
# Router for invitation operations on an organization (requires org_id)
invitation_router = APIRouter(prefix='/api/organizations/{org_id}/members')

View File

@@ -50,8 +50,8 @@ from storage.org_service import OrgService
from storage.org_store import OrgStore
from storage.user_store import UserStore
from openhands.app_server.user_auth import get_user_id
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth import get_user_id
# Initialize API router
org_router = APIRouter(prefix='/api/organizations', tags=['Orgs'])

View File

@@ -17,12 +17,12 @@ from openhands.app_server.config import (
depends_user_context,
resolve_provider_llm_base_url,
)
from openhands.app_server.integrations.provider import ProviderHandler
from openhands.app_server.integrations.service_types import ProviderType
from openhands.app_server.sandbox.session_auth import validate_session_key_ownership
from openhands.app_server.user.auth_user_context import AuthUserContext
from openhands.app_server.user.user_context import UserContext
from openhands.app_server.utils.dependencies import get_dependencies
from openhands.integrations.provider import ProviderHandler
from openhands.integrations.service_types import ProviderType
_logger = logging.getLogger(__name__)

View File

@@ -35,8 +35,8 @@ from server.auth.constants import (
)
from server.auth.token_manager import TokenManager
from openhands.app_server.integrations.provider import ProviderType
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderType
from openhands.server.shared import sio
# Cache TTL constants

View File

@@ -1,7 +1,7 @@
from datetime import datetime
# Simplified imports to avoid dependency chain issues
# from openhands.integrations.service_types import ProviderType
# from openhands.app_server.integrations.service_types import ProviderType
# from openhands.sdk.llm import MetricsSnapshot
# from openhands.app_server.app_conversation.app_conversation_models import ConversationTrigger
# For now, use Any to avoid import issues

View File

@@ -30,8 +30,8 @@ from openhands.agent_server.utils import utc_now
from openhands.app_server.app_conversation.sql_app_conversation_info_service import (
StoredConversationMetadata,
)
from openhands.app_server.integrations.provider import ProviderType
from openhands.app_server.services.injector import InjectorState
from openhands.integrations.provider import ProviderType
from openhands.sdk.llm import MetricsSnapshot, TokenUsage
logger = logging.getLogger(__name__)

View File

@@ -10,8 +10,8 @@ from sqlalchemy.exc import OperationalError
from storage.auth_tokens import AuthTokens
from storage.database import a_session_maker
from openhands.app_server.integrations.service_types import ProviderType
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import ProviderType
# Time buffer (in seconds) before actual expiration to consider token expired
# This ensures tokens are refreshed before they actually expire. The

View File

@@ -6,8 +6,8 @@ from sqlalchemy import and_, desc, select
from storage.database import a_session_maker
from storage.openhands_pr import OpenhandsPR
from openhands.app_server.integrations.service_types import ProviderType
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import ProviderType
class OpenhandsPRStore:

View File

@@ -13,8 +13,8 @@ from sqlalchemy import and_, delete, select, update
from storage.database import a_session_maker
from storage.proactive_convos import ProactiveConversation
from openhands.app_server.integrations.service_types import ProviderType
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import ProviderType
@dataclass

View File

@@ -15,8 +15,8 @@ from storage.database import a_session_maker
from storage.gitlab_webhook import GitlabWebhook, WebhookStatus
from storage.gitlab_webhook_store import GitlabWebhookStore
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
if TYPE_CHECKING:
from integrations.gitlab.gitlab_service import SaaSGitLabService

View File

@@ -34,7 +34,7 @@ class TestSaaSBitbucketDCServiceInit:
def test_refresh_flag_is_true(self):
# self.refresh = True is required so the base class BitbucketDCService
# retries the request with a refreshed token on 401 responses.
# See openhands/integrations/bitbucket_data_center/service/base.py,
# See openhands/app_server/integrations/bitbucket_data_center/service/base.py,
# which checks `if self.refresh` before attempting the retry.
service = SaaSBitbucketDCService()
assert service.refresh is True

View File

@@ -24,7 +24,10 @@ def jinja_env() -> Environment:
repo_root = Path(__file__).resolve().parents[5]
return Environment(
loader=FileSystemLoader(
str(repo_root / 'openhands/integrations/templates/resolver/github')
str(
repo_root
/ 'openhands/app_server/integrations/templates/resolver/github'
)
)
)

View File

@@ -18,8 +18,8 @@ from storage.jira_conversation import JiraConversation
from storage.jira_user import JiraUser
from storage.jira_workspace import JiraWorkspace
from openhands.integrations.service_types import ProviderType, Repository
from openhands.server.user_auth.user_auth import UserAuth
from openhands.app_server.integrations.service_types import ProviderType, Repository
from openhands.app_server.user_auth.user_auth import UserAuth
@pytest.fixture

View File

@@ -16,8 +16,8 @@ from storage.jira_dc_conversation import JiraDcConversation
from storage.jira_dc_user import JiraDcUser
from storage.jira_dc_workspace import JiraDcWorkspace
from openhands.integrations.service_types import ProviderType, Repository
from openhands.server.user_auth.user_auth import UserAuth
from openhands.app_server.integrations.service_types import ProviderType, Repository
from openhands.app_server.user_auth.user_auth import UserAuth
@pytest.fixture

View File

@@ -17,7 +17,7 @@ from integrations.jira_dc.jira_dc_view import (
)
from integrations.models import Message, SourceType
from openhands.integrations.service_types import ProviderType, Repository
from openhands.app_server.integrations.service_types import ProviderType, Repository
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,

View File

@@ -17,7 +17,7 @@ from storage.slack_conversation import SlackConversation
from storage.slack_user import SlackUser
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
from openhands.server.user_auth.user_auth import UserAuth
from openhands.app_server.user_auth.user_auth import UserAuth
# ---------------------------------------------------------------------------
# Fixtures

View File

@@ -10,11 +10,11 @@ import pytest
from pydantic import SecretStr
from enterprise.integrations.resolver_context import ResolverUserContext
from openhands.app_server.secrets.secrets_models import Secrets
# Import the real classes we want to test
from openhands.integrations.provider import CustomSecret, ProviderToken
from openhands.integrations.service_types import ProviderType
from openhands.app_server.integrations.provider import CustomSecret, ProviderToken
from openhands.app_server.integrations.service_types import ProviderType
from openhands.app_server.secrets.secrets_models import Secrets
# Import the SDK types we need for testing
from openhands.sdk.secret import SecretSource, StaticSecret
@@ -344,7 +344,7 @@ async def test_get_provider_handler_creates_handler_with_correct_params(
handler = await resolver_context._get_provider_handler()
# Assert
from openhands.integrations.provider import ProviderHandler
from openhands.app_server.integrations.provider import ProviderHandler
assert isinstance(handler, ProviderHandler)
assert handler.provider_tokens == provider_tokens

View File

@@ -19,7 +19,7 @@ from server.routes.api_keys import (
)
from storage.lite_llm_manager import LiteLlmManager
from openhands.server.user_auth.user_auth import AuthType
from openhands.app_server.user_auth.user_auth import AuthType
class TestVerifyByorKeyInLitellm:

View File

@@ -1,193 +0,0 @@
"""Unit tests for GitHub integration routes.
Tests for:
- get_github_token endpoint
"""
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pydantic import SecretStr
@pytest.fixture(autouse=True)
def mock_github_dependencies():
"""Mock module-level dependencies before importing the github module.
The github.py module instantiates GitHubDataCollector at module level,
which requires GitHub App credentials. We mock these dependencies to
allow importing the module in test environments without credentials.
"""
# Store original modules if they exist
original_modules = {}
modules_to_mock = [
'integrations.github.data_collector',
'integrations.github.github_manager',
'server.routes.integration.github',
]
for mod in modules_to_mock:
if mod in sys.modules:
original_modules[mod] = sys.modules[mod]
del sys.modules[mod]
# Create mock GitHubDataCollector that doesn't require credentials
mock_data_collector_module = MagicMock()
mock_data_collector_instance = MagicMock()
mock_data_collector_module.GitHubDataCollector.return_value = (
mock_data_collector_instance
)
sys.modules['integrations.github.data_collector'] = mock_data_collector_module
# Create mock GithubManager
mock_github_manager_module = MagicMock()
mock_github_manager_instance = MagicMock()
mock_github_manager_module.GithubManager.return_value = mock_github_manager_instance
sys.modules['integrations.github.github_manager'] = mock_github_manager_module
yield
# Clean up the mocked modules
for mod in modules_to_mock:
if mod in sys.modules:
del sys.modules[mod]
# Restore original modules
for mod, original in original_modules.items():
sys.modules[mod] = original
class TestGitHubTokenResponse:
"""Test suite for GitHubTokenResponse model."""
def test_github_token_response_with_valid_token(self):
"""GitHubTokenResponse should accept a valid access_token."""
from server.routes.integration.github import GitHubTokenResponse
response = GitHubTokenResponse(access_token='ghp_test_token_12345')
assert response.access_token == 'ghp_test_token_12345'
def test_github_token_response_model_dump(self):
"""GitHubTokenResponse model_dump should include access_token."""
from server.routes.integration.github import GitHubTokenResponse
response = GitHubTokenResponse(access_token='ghp_test_token_12345')
data = response.model_dump()
assert data['access_token'] == 'ghp_test_token_12345'
class TestGetGitHubToken:
"""Test suite for get_github_token endpoint."""
@pytest.fixture
def mock_request(self):
"""Create a mock request object."""
request = MagicMock()
request.state = MagicMock()
return request
@pytest.fixture
def mock_saas_user_auth(self):
"""Create a mock SaasUserAuth object."""
from openhands.integrations.provider import ProviderToken, ProviderType
mock_auth = AsyncMock()
mock_auth.get_provider_tokens = AsyncMock(
return_value={
ProviderType.GITHUB: ProviderToken(
token=SecretStr('ghp_test_token_12345')
)
}
)
return mock_auth
@pytest.mark.asyncio
async def test_get_github_token_success(self, mock_request, mock_saas_user_auth):
"""Should return GitHub token when user has a valid token."""
from server.routes.integration.github import (
GitHubTokenResponse,
get_github_token,
)
with patch(
'server.routes.integration.github.get_user_auth',
return_value=mock_saas_user_auth,
):
result = await get_github_token(mock_request)
assert isinstance(result, GitHubTokenResponse)
assert result.access_token == 'ghp_test_token_12345'
mock_saas_user_auth.get_provider_tokens.assert_called_once()
@pytest.mark.asyncio
async def test_get_github_token_no_provider_tokens(self, mock_request):
"""Should raise 404 when user has no provider tokens."""
from fastapi import HTTPException
from server.routes.integration.github import get_github_token
mock_auth = AsyncMock()
mock_auth.get_provider_tokens = AsyncMock(return_value=None)
with (
patch(
'server.routes.integration.github.get_user_auth',
return_value=mock_auth,
),
pytest.raises(HTTPException) as exc_info,
):
await get_github_token(mock_request)
assert exc_info.value.status_code == 404
assert 'No provider tokens' in exc_info.value.detail
@pytest.mark.asyncio
async def test_get_github_token_no_github_token(self, mock_request):
"""Should raise 404 when user has provider tokens but no GitHub token."""
from fastapi import HTTPException
from server.routes.integration.github import get_github_token
from openhands.integrations.provider import ProviderToken, ProviderType
mock_auth = AsyncMock()
# Return GitLab token but no GitHub token
mock_auth.get_provider_tokens = AsyncMock(
return_value={
ProviderType.GITLAB: ProviderToken(
token=SecretStr('glpat_test_token_12345')
)
}
)
with (
patch(
'server.routes.integration.github.get_user_auth',
return_value=mock_auth,
),
pytest.raises(HTTPException) as exc_info,
):
await get_github_token(mock_request)
assert exc_info.value.status_code == 404
assert 'No GitHub token' in exc_info.value.detail
@pytest.mark.asyncio
async def test_get_github_token_empty_provider_tokens(self, mock_request):
"""Should raise 404 when user has empty provider tokens dict."""
from fastapi import HTTPException
from server.routes.integration.github import get_github_token
mock_auth = AsyncMock()
mock_auth.get_provider_tokens = AsyncMock(return_value={})
with (
patch(
'server.routes.integration.github.get_user_auth',
return_value=mock_auth,
),
pytest.raises(HTTPException) as exc_info,
):
await get_github_token(mock_request)
assert exc_info.value.status_code == 404
# Empty dict is falsy, so it triggers the "no provider tokens" error
assert 'No provider tokens' in exc_info.value.detail

View File

@@ -22,7 +22,7 @@ from server.routes.orgs import (
from sqlalchemy.exc import IntegrityError
from storage.org_git_claim import OrgGitClaim
from openhands.server.user_auth import get_user_id
from openhands.app_server.user_auth import get_user_id
TEST_USER_ID = str(uuid.uuid4())

View File

@@ -44,8 +44,8 @@ from server.routes.orgs import (
)
from storage.org import Org
from openhands.app_server.user_auth import get_user_id
from openhands.sdk.settings import AgentSettings, ConversationSettings
from openhands.server.user_auth import get_user_id
# Test user ID constant (must be a valid UUID string)
TEST_USER_ID = str(uuid.uuid4())

View File

@@ -16,7 +16,7 @@ from server.routes.user_app_settings_models import (
UserNotFoundError,
)
from openhands.server.user_auth import get_user_id
from openhands.app_server.user_auth import get_user_id
TEST_USER_ID = str(uuid.uuid4())

View File

@@ -144,7 +144,7 @@ class TestGetOrgInfoFromContext:
from server.routes.users_v1 import _get_org_info_from_context
from openhands.app_server.user.auth_user_context import AuthUserContext
from openhands.server.user_auth.user_auth import UserAuth
from openhands.app_server.user_auth.user_auth import UserAuth
# Create AuthUserContext with a non-SaasUserAuth
mock_user_auth = MagicMock(spec=UserAuth)

View File

@@ -15,7 +15,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openhands.integrations.service_types import ProviderType
from openhands.app_server.integrations.service_types import ProviderType
# Default patches for constants
CONSTANT_PATCHES = {

View File

@@ -15,7 +15,7 @@ from storage.auth_token_store import (
from storage.auth_tokens import AuthTokens
from storage.base import Base
from openhands.integrations.service_types import ProviderType
from openhands.app_server.integrations.service_types import ProviderType
@pytest.fixture

View File

@@ -24,8 +24,8 @@ from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationInfo,
ConversationTrigger,
)
from openhands.app_server.integrations.service_types import ProviderType
from openhands.app_server.user.specifiy_user_context import SpecifyUserContext
from openhands.integrations.service_types import ProviderType
# Test UUIDs
USER1_ID = UUID('a1111111-1111-1111-1111-111111111111')

View File

@@ -13,7 +13,7 @@ from server.auth.auth_error import (
from server.auth.saas_user_auth import SaasUserAuth
from server.middleware import SetAuthCookieMiddleware
from openhands.server.user_auth.user_auth import AuthType
from openhands.app_server.user_auth.user_auth import AuthType
@pytest.fixture

View File

@@ -19,7 +19,7 @@ from server.routes.auth import (
set_response_cookie,
)
from openhands.integrations.service_types import ProviderType
from openhands.app_server.integrations.service_types import ProviderType
def create_mock_user_authorizer(success: bool = True, error_detail: str | None = None):
@@ -799,7 +799,7 @@ async def test_logout_without_refresh_token():
with patch('server.routes.auth.token_manager') as mock_token_manager:
with patch(
'openhands.server.user_auth.default_user_auth.DefaultUserAuth.get_instance'
'openhands.app_server.user_auth.default_user_auth.DefaultUserAuth.get_instance'
) as mock_get_instance:
mock_get_instance.side_effect = AuthError()
result = await logout(mock_request)

View File

@@ -68,7 +68,7 @@ class TestAcceptInvitationPostEndpoint:
def auth_app(self):
"""Create a FastAPI app with dependency overrides for authenticated tests."""
from openhands.server.user_auth import get_user_id
from openhands.app_server.user_auth import get_user_id
app = FastAPI()
app.include_router(accept_router)
@@ -200,7 +200,7 @@ class TestCreateInvitationBatchEndpoint:
@pytest.fixture
def batch_app(self):
"""Create a FastAPI app with dependency overrides for batch tests."""
from openhands.server.user_auth import get_user_id
from openhands.app_server.user_auth import get_user_id
app = FastAPI()
app.include_router(invitation_router)

View File

@@ -8,9 +8,9 @@ from pydantic import SecretStr
from storage.saas_secrets_store import SaasSecretsStore
from storage.stored_custom_secrets import StoredCustomSecrets
from openhands.app_server.integrations.provider import CustomSecret
from openhands.app_server.secrets.secrets_models import Secrets
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.integrations.provider import CustomSecret
@pytest.fixture

View File

@@ -22,8 +22,8 @@ from server.auth.saas_user_auth import (
from storage.api_key_store import ApiKeyValidationResult
from storage.user_authorization import UserAuthorizationType
from openhands.app_server.integrations.provider import ProviderToken, ProviderType
from openhands.app_server.secrets.secrets_models import Secrets
from openhands.integrations.provider import ProviderToken, ProviderType
@pytest.fixture

View File

@@ -21,9 +21,9 @@ from openhands.app_server.app_conversation.app_conversation_models import (
from openhands.app_server.app_conversation.sql_app_conversation_info_service import (
SQLAppConversationInfoService,
)
from openhands.app_server.integrations.provider import ProviderType
from openhands.app_server.user.specifiy_user_context import SpecifyUserContext
from openhands.app_server.utils.sql_utils import Base
from openhands.integrations.provider import ProviderType
from openhands.sdk.llm import MetricsSnapshot, TokenUsage

View File

@@ -11,12 +11,12 @@ from integrations.slack.slack_manager import (
from integrations.slack.slack_view import SlackNewConversationView
from storage.slack_user import SlackUser
from openhands.integrations.service_types import (
from openhands.app_server.integrations.service_types import (
ProviderTimeoutError,
ProviderType,
Repository,
)
from openhands.server.user_auth.user_auth import UserAuth
from openhands.app_server.user_auth.user_auth import UserAuth
@pytest.fixture

View File

@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from server.auth.token_manager import TokenManager, create_encryption_utility
from openhands.integrations.service_types import ProviderType
from openhands.app_server.integrations.service_types import ProviderType
@pytest.fixture

View File

@@ -12,9 +12,9 @@ import pytest
from fastapi import HTTPException, status
from pydantic import SecretStr
from openhands.app_server.integrations.provider import ProviderToken
from openhands.app_server.integrations.service_types import ProviderType
from openhands.app_server.user.user_context import UserContext
from openhands.integrations.provider import ProviderToken
from openhands.integrations.service_types import ProviderType
def _make_user_context(provider_tokens, user_id: str = 'user-1') -> UserContext:
@@ -97,7 +97,7 @@ async def test_returns_organizations_for_supported_provider(
)
with patch(
'openhands.integrations.provider.ProviderHandler.get_service'
'openhands.app_server.integrations.provider.ProviderHandler.get_service'
) as mock_get_service:
mock_service = mock_get_service.return_value
setattr(mock_service, service_method, AsyncMock(return_value=service_return))

View File

@@ -15,11 +15,11 @@ from openhands.agent_server.utils import OpenHandsUUID, utc_now
from openhands.app_server.event_callback.event_callback_models import (
EventCallbackProcessor,
)
from openhands.app_server.integrations.service_types import ProviderType, SuggestedTask
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
# Import from new location and re-export for backward compatibility
from openhands.app_server.settings.settings_models import SandboxGroupingStrategy
from openhands.integrations.service_types import ProviderType, SuggestedTask
from openhands.sdk.conversation import ConversationExecutionStatus
from openhands.sdk.llm import MetricsSnapshot
from openhands.sdk.plugin import PluginSource

View File

@@ -70,6 +70,8 @@ from openhands.app_server.event_callback.event_callback_service import (
from openhands.app_server.event_callback.set_title_callback_processor import (
SetTitleCallbackProcessor,
)
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.app_server.integrations.service_types import SuggestedTask
from openhands.app_server.pending_messages.pending_message_service import (
PendingMessageService,
)
@@ -92,8 +94,6 @@ from openhands.app_server.utils.llm_metadata import (
get_llm_metadata,
should_set_litellm_extra_body,
)
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.integrations.service_types import SuggestedTask
from openhands.sdk import Agent, AgentContext, LocalWorkspace
from openhands.sdk.agent.acp_agent import ACPAgent
from openhands.sdk.hooks import HookConfig

View File

@@ -15,10 +15,10 @@ import logging
import httpx
from pydantic import BaseModel
from openhands.app_server.integrations.provider import ProviderType
from openhands.app_server.integrations.service_types import AuthenticationError
from openhands.app_server.sandbox.sandbox_models import SandboxInfo
from openhands.app_server.user.user_context import UserContext
from openhands.integrations.provider import ProviderType
from openhands.integrations.service_types import AuthenticationError
from openhands.sdk.context.skills import KeywordTrigger, Skill, TaskTrigger
_logger = logging.getLogger(__name__)

View File

@@ -48,13 +48,13 @@ from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationSortOrder,
ConversationTrigger,
)
from openhands.app_server.integrations.provider import ProviderType
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.user_context import UserContext
from openhands.app_server.utils.sql_utils import (
Base,
create_json_type_decorator,
)
from openhands.integrations.provider import ProviderType
from openhands.sdk import ConversationStats
from openhands.sdk.event import ConversationStateUpdateEvent
from openhands.sdk.llm import MetricsSnapshot, TokenUsage

View File

@@ -0,0 +1,73 @@
"""Conversation path helpers for consistent path construction.
This module provides helper functions for constructing conversation-related
storage paths. Use these helpers instead of hardcoding path patterns to ensure
consistency across the codebase.
"""
from pathlib import Path
from uuid import UUID
# The base directory name for v1 conversation storage
V1_CONVERSATIONS_DIR = 'v1_conversations'
def get_conversation_dir(conversation_id: UUID | str) -> str:
"""Get the conversation directory path segment.
Args:
conversation_id: The conversation ID (UUID or hex string)
Returns:
Path segment like 'v1_conversations/{conversation_id_hex}'
Example:
>>> get_conversation_dir(UUID('12345678-1234-5678-1234-567812345678'))
'v1_conversations/12345678123456781234567812345678'
>>> get_conversation_dir('12345678123456781234567812345678')
'v1_conversations/12345678123456781234567812345678'
"""
if isinstance(conversation_id, UUID):
conversation_id_hex = conversation_id.hex
else:
# Already a hex string
conversation_id_hex = conversation_id
return f'{V1_CONVERSATIONS_DIR}/{conversation_id_hex}'
def get_conversation_path(
conversation_id: UUID | str,
user_id: str | None = None,
prefix: Path | str | None = None,
) -> Path:
"""Get the full conversation path.
Args:
conversation_id: The conversation ID (UUID or hex string)
user_id: Optional user ID to include in path
prefix: Optional prefix path
Returns:
Full path like '{prefix}/{user_id}/v1_conversations/{conversation_id_hex}'
Example:
>>> get_conversation_path(UUID('...'), user_id='user123', prefix=Path('/data'))
Path('/data/user123/v1_conversations/...')
"""
if isinstance(conversation_id, UUID):
conversation_id_hex = conversation_id.hex
else:
conversation_id_hex = conversation_id
parts: list[str] = []
if prefix:
parts.append(str(prefix))
if user_id:
parts.append(user_id)
parts.append(V1_CONVERSATIONS_DIR)
parts.append(conversation_id_hex)
return Path(*parts) if parts else Path(V1_CONVERSATIONS_DIR) / conversation_id_hex

View File

@@ -12,6 +12,7 @@ from openhands.app_server.app_conversation.app_conversation_info_service import
from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationInfo,
)
from openhands.app_server.conversation_paths import V1_CONVERSATIONS_DIR
from openhands.app_server.event.event_service import EventService
from openhands.app_server.event_callback.event_callback_models import EventKind
from openhands.sdk import Event
@@ -60,7 +61,7 @@ class EventServiceBase(EventService, ABC):
conversation_info = await task
if conversation_info and conversation_info.created_by_user_id:
path /= conversation_info.created_by_user_id
path = path / 'v1_conversations' / conversation_id.hex
path = path / V1_CONVERSATIONS_DIR / conversation_id.hex
return path
async def get_event(self, conversation_id: UUID, event_id: UUID) -> Event | None:

View File

@@ -34,6 +34,7 @@ from openhands.app_server.event_callback.event_callback_models import EventCallb
from openhands.app_server.event_callback.set_title_callback_processor import (
SetTitleCallbackProcessor,
)
from openhands.app_server.integrations.provider import ProviderType
from openhands.app_server.sandbox.sandbox_models import SandboxInfo
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.services.jwt_service import JwtService
@@ -43,14 +44,13 @@ from openhands.app_server.user.specifiy_user_context import (
USER_CONTEXT_ATTR,
SpecifyUserContext,
)
from openhands.integrations.provider import ProviderType
from openhands.app_server.user_auth.default_user_auth import DefaultUserAuth
from openhands.app_server.user_auth.user_auth import (
get_for_user as get_user_auth_for_user,
)
from openhands.sdk import ConversationExecutionStatus, Event
from openhands.sdk.event import ConversationStateUpdateEvent
from openhands.server.types import AppMode
from openhands.server.user_auth.default_user_auth import DefaultUserAuth
from openhands.server.user_auth.user_auth import (
get_for_user as get_user_auth_for_user,
)
router = APIRouter(prefix='/webhooks', tags=['Webhooks'])
event_service_dependency = depends_event_service()

View File

@@ -0,0 +1,77 @@
# OpenHands FileStore Module
The file store module provides different storage backends for file operations in OpenHands. This module implements a common interface (`FileStore`) that allows for interchangeable storage backends.
All FileStore implementations use `DiscriminatedUnionMixin` for automatic serialization/deserialization with a `kind` discriminator field based on the class name.
## Usage
```python
from openhands.app_server.file_store import get_file_store, LocalFileStore
# Using the factory function
store = get_file_store("local", "/tmp/file_store")
# Or instantiate directly
store = LocalFileStore(root="/tmp/file_store")
# Write, read, list, and delete operations
store.write("example.txt", "Hello, world!")
content = store.read("example.txt")
files = store.list("/")
store.delete("example.txt")
```
## Available Storage Backends
### 1. Local File Storage (`LocalFileStore`)
Local file storage saves files to the local filesystem.
**Parameters:**
- `root`: The root directory for file storage (supports `~` expansion)
### 2. In-Memory Storage (`InMemoryFileStore`)
In-memory storage keeps files in memory, useful for testing or temporary storage.
**Parameters:**
- `files`: Optional dictionary of initial files (default: empty)
### 3. Amazon S3 Storage (`S3FileStore`)
S3 storage uses Amazon S3 or compatible services for file storage.
**Parameters:**
- `bucket`: The S3 bucket name (falls back to `AWS_S3_BUCKET` environment variable if empty)
**Environment Variables:**
- `AWS_ACCESS_KEY_ID`: Your AWS access key
- `AWS_SECRET_ACCESS_KEY`: Your AWS secret key
- `AWS_S3_BUCKET`: Default bucket name (used if `bucket` parameter is empty)
- `AWS_S3_ENDPOINT`: Optional custom endpoint for S3-compatible services
- `AWS_S3_SECURE`: Whether to use HTTPS (default: "true")
### 4. Google Cloud Storage (`GoogleCloudFileStore`)
Google Cloud Storage uses GCS buckets for file storage.
**Parameters:**
- `bucket_name`: The GCS bucket name (falls back to `GOOGLE_CLOUD_BUCKET_NAME` environment variable if empty)
**Environment Variables:**
- `GOOGLE_CLOUD_BUCKET_NAME`: Default bucket name (used if `bucket_name` parameter is empty)
- `GOOGLE_APPLICATION_CREDENTIALS`: Path to Google Cloud credentials JSON file
## Configuration
To configure the file store in OpenHands, use the following configuration options:
```toml
[core]
# File store type: "local", "memory", "s3", "google_cloud"
file_store = "local"
# Path/bucket for file store (interpretation depends on file_store type)
file_store_path = "/tmp/file_store"
```

View File

@@ -0,0 +1,21 @@
from openhands.app_server.file_store.files import FileStore
from openhands.app_server.file_store.google_cloud import GoogleCloudFileStore
from openhands.app_server.file_store.local import LocalFileStore
from openhands.app_server.file_store.memory import InMemoryFileStore
from openhands.app_server.file_store.s3 import S3FileStore
def get_file_store(
file_store_type: str,
file_store_path: str | None = None,
) -> FileStore:
if file_store_type == 'local':
if file_store_path is None:
raise ValueError('file_store_path is required for local file store')
return LocalFileStore(root=file_store_path)
elif file_store_type == 's3':
return S3FileStore(bucket_name=file_store_path or '')
elif file_store_type == 'google_cloud':
return GoogleCloudFileStore(bucket_name=file_store_path or '')
else:
return InMemoryFileStore()

View File

@@ -0,0 +1,30 @@
from abc import ABC, abstractmethod
from pydantic import ConfigDict
from openhands.sdk.utils.models import DiscriminatedUnionMixin
class FileStore(DiscriminatedUnionMixin, ABC):
"""Base class for file storage implementations.
Uses DiscriminatedUnionMixin for automatic `kind` field based on class name.
"""
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
@abstractmethod
def write(self, path: str, contents: str | bytes) -> None:
pass
@abstractmethod
def read(self, path: str) -> str:
pass
@abstractmethod
def list(self, path: str) -> list[str]:
pass
@abstractmethod
def delete(self, path: str) -> None:
pass

View File

@@ -5,21 +5,44 @@ from google.cloud import storage
from google.cloud.storage.blob import Blob
from google.cloud.storage.bucket import Bucket
from google.cloud.storage.client import Client
from pydantic import Field, PrivateAttr
from openhands.storage.files import FileStore
from openhands.app_server.file_store.files import FileStore
class GoogleCloudFileStore(FileStore):
def __init__(self, bucket_name: str | None = None) -> None:
"""Create a new FileStore.
"""Google Cloud Storage file store.
If GOOGLE_APPLICATION_CREDENTIALS is defined in the environment it will be used
for authentication. Otherwise access will be anonymous.
"""
if bucket_name is None:
bucket_name = os.environ['GOOGLE_CLOUD_BUCKET_NAME']
self.storage_client: Client = storage.Client()
self.bucket: Bucket = self.storage_client.bucket(bucket_name)
If GOOGLE_APPLICATION_CREDENTIALS is defined in the environment it will be used
for authentication. Otherwise access will be anonymous.
The storage client and bucket are initialized lazily on first access.
"""
bucket_name: str = Field(default='')
_storage_client: Client | None = PrivateAttr(default=None)
_bucket: Bucket | None = PrivateAttr(default=None)
def _get_bucket_name(self) -> str:
"""Get bucket name, falling back to environment variable if not set."""
if self.bucket_name:
return self.bucket_name
return os.environ['GOOGLE_CLOUD_BUCKET_NAME']
@property
def storage_client(self) -> Client:
"""Get the storage client, initializing lazily on first access."""
if self._storage_client is None:
self._storage_client = storage.Client()
return self._storage_client
@property
def bucket(self) -> Bucket:
"""Get the bucket, initializing lazily on first access."""
if self._bucket is None:
self._bucket = self.storage_client.bucket(self._get_bucket_name())
return self._bucket
def write(self, path: str, contents: str | bytes) -> None:
blob: Blob = self.bucket.blob(path)

View File

@@ -2,18 +2,21 @@ import os
import shutil
import threading
from pydantic import model_validator
from openhands.app_server.file_store.files import FileStore
from openhands.core.logger import openhands_logger as logger
from openhands.storage.files import FileStore
class LocalFileStore(FileStore):
root: str
def __init__(self, root: str):
if root.startswith('~'):
root = os.path.expanduser(root)
self.root = root
@model_validator(mode='after')
def _setup_root(self) -> 'LocalFileStore':
if self.root.startswith('~'):
self.root = os.path.expanduser(self.root)
os.makedirs(self.root, exist_ok=True)
return self
def get_full_path(self, path: str) -> str:
if path.startswith('/'):

View File

@@ -1,16 +1,13 @@
import os
from pydantic import Field
from openhands.app_server.file_store.files import FileStore
from openhands.core.logger import openhands_logger as logger
from openhands.storage.files import FileStore
class InMemoryFileStore(FileStore):
files: dict[str, str]
def __init__(self, files: dict[str, str] | None = None) -> None:
self.files = {}
if files is not None:
self.files = files
files: dict[str, str] = Field(default_factory=dict)
def write(self, path: str, contents: str | bytes) -> None:
if isinstance(contents, bytes):

View File

@@ -3,8 +3,9 @@ from typing import Any, TypedDict
import boto3
import botocore
from pydantic import Field, PrivateAttr
from openhands.storage.files import FileStore
from openhands.app_server.file_store.files import FileStore
class S3ObjectDict(TypedDict):
@@ -20,45 +21,64 @@ class ListObjectsV2OutputDict(TypedDict):
class S3FileStore(FileStore):
def __init__(self, bucket_name: str | None) -> None:
access_key = os.getenv('AWS_ACCESS_KEY_ID')
secret_key = os.getenv('AWS_SECRET_ACCESS_KEY')
secure = os.getenv('AWS_S3_SECURE', 'true').lower() == 'true'
endpoint = self._ensure_url_scheme(secure, os.getenv('AWS_S3_ENDPOINT'))
if bucket_name is None:
bucket_name = os.environ['AWS_S3_BUCKET']
self.bucket: str = bucket_name
self.client: Any = boto3.client(
's3',
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
endpoint_url=endpoint,
use_ssl=secure,
)
"""S3-compatible file store.
The S3 client is initialized lazily on first access.
"""
bucket_name: str = Field(default='')
_client: Any = PrivateAttr(default=None)
_resolved_bucket: str | None = PrivateAttr(default=None)
def _get_bucket_name(self) -> str:
"""Get bucket name, falling back to environment variable if not set."""
if self._resolved_bucket is None:
self._resolved_bucket = self.bucket_name or os.environ['AWS_S3_BUCKET']
return self._resolved_bucket
@property
def client(self) -> Any:
"""Get the S3 client, initializing lazily on first access."""
if self._client is None:
access_key = os.getenv('AWS_ACCESS_KEY_ID')
secret_key = os.getenv('AWS_SECRET_ACCESS_KEY')
secure = os.getenv('AWS_S3_SECURE', 'true').lower() == 'true'
endpoint = self._ensure_url_scheme(secure, os.getenv('AWS_S3_ENDPOINT'))
self._client = boto3.client(
's3',
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
endpoint_url=endpoint,
use_ssl=secure,
)
return self._client
def write(self, path: str, contents: str | bytes) -> None:
try:
as_bytes = (
contents.encode('utf-8') if isinstance(contents, str) else contents
)
self.client.put_object(Bucket=self.bucket, Key=path, Body=as_bytes)
self.client.put_object(
Bucket=self._get_bucket_name(), Key=path, Body=as_bytes
)
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'AccessDenied':
raise FileNotFoundError(
f"Error: Access denied to bucket '{self.bucket}'."
f"Error: Access denied to bucket '{self._get_bucket_name()}'."
)
elif e.response['Error']['Code'] == 'NoSuchBucket':
raise FileNotFoundError(
f"Error: The bucket '{self.bucket}' does not exist."
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
)
raise FileNotFoundError(
f"Error: Failed to write to bucket '{self.bucket}' at path {path}: {e}"
f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}: {e}"
)
def read(self, path: str) -> str:
try:
response: GetObjectOutputDict = self.client.get_object(
Bucket=self.bucket, Key=path
Bucket=self._get_bucket_name(), Key=path
)
with response['Body'] as stream:
return str(stream.read().decode('utf-8'))
@@ -66,19 +86,19 @@ class S3FileStore(FileStore):
# Catch all S3-related errors
if e.response['Error']['Code'] == 'NoSuchBucket':
raise FileNotFoundError(
f"Error: The bucket '{self.bucket}' does not exist."
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
)
elif e.response['Error']['Code'] == 'NoSuchKey':
raise FileNotFoundError(
f"Error: The object key '{path}' does not exist in bucket '{self.bucket}'."
f"Error: The object key '{path}' does not exist in bucket '{self._get_bucket_name()}'."
)
else:
raise FileNotFoundError(
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}: {e}"
)
except Exception as e:
raise FileNotFoundError(
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}: {e}"
)
def list(self, path: str) -> list[str]:
@@ -96,7 +116,7 @@ class S3FileStore(FileStore):
results: set[str] = set()
prefix_len = len(path)
response: ListObjectsV2OutputDict = self.client.list_objects_v2(
Bucket=self.bucket, Prefix=path
Bucket=self._get_bucket_name(), Prefix=path
)
contents = response.get('Contents')
if not contents:
@@ -123,34 +143,36 @@ class S3FileStore(FileStore):
# Try to delete any child resources (Assume the path is a directory)
response = self.client.list_objects_v2(
Bucket=self.bucket, Prefix=f'{path}/'
Bucket=self._get_bucket_name(), Prefix=f'{path}/'
)
for content in response.get('Contents') or []:
self.client.delete_object(Bucket=self.bucket, Key=content['Key'])
self.client.delete_object(
Bucket=self._get_bucket_name(), Key=content['Key']
)
# Next try to delete item as a file
self.client.delete_object(Bucket=self.bucket, Key=path)
self.client.delete_object(Bucket=self._get_bucket_name(), Key=path)
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'NoSuchBucket':
raise FileNotFoundError(
f"Error: The bucket '{self.bucket}' does not exist."
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
)
elif e.response['Error']['Code'] == 'AccessDenied':
raise FileNotFoundError(
f"Error: Access denied to bucket '{self.bucket}'."
f"Error: Access denied to bucket '{self._get_bucket_name()}'."
)
elif e.response['Error']['Code'] == 'NoSuchKey':
raise FileNotFoundError(
f"Error: The object key '{path}' does not exist in bucket '{self.bucket}'."
f"Error: The object key '{path}' does not exist in bucket '{self._get_bucket_name()}'."
)
else:
raise FileNotFoundError(
f"Error: Failed to delete key '{path}' from bucket '{self.bucket}': {e}"
f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}': {e}"
)
except Exception as e:
raise FileNotFoundError(
f"Error: Failed to delete key '{path}' from bucket '{self.bucket}: {e}"
f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}: {e}"
)
def _ensure_url_scheme(self, secure: bool, url: str | None) -> str | None:

View File

@@ -4,7 +4,7 @@ from enum import StrEnum
from pydantic import BaseModel
from openhands.integrations.service_types import (
from openhands.app_server.integrations.service_types import (
Branch,
Repository,
SuggestedTask,

View File

@@ -18,6 +18,13 @@ from openhands.app_server.git.git_models import (
SortOrder,
SuggestedTaskPage,
)
from openhands.app_server.integrations.provider import ProviderHandler
from openhands.app_server.integrations.service_types import (
Branch,
ProviderType,
Repository,
SuggestedTask,
)
from openhands.app_server.user.user_context import UserContext
from openhands.app_server.utils.dependencies import get_dependencies
from openhands.app_server.utils.paging_utils import (
@@ -25,16 +32,9 @@ from openhands.app_server.utils.paging_utils import (
encode_page_id,
paginate_results,
)
from openhands.integrations.provider import ProviderHandler
from openhands.integrations.service_types import (
Branch,
ProviderType,
Repository,
SuggestedTask,
)
if TYPE_CHECKING:
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE
# We use the get_dependencies method here to signal to the OpenAPI docs that this endpoint
# is protected. The actual protection is provided by SetAuthCookieMiddleware

View File

@@ -4,22 +4,26 @@ from typing import Any
import httpx
from pydantic import SecretStr
from openhands.integrations.azure_devops.service.branches import (
from openhands.app_server.integrations.azure_devops.service.branches import (
AzureDevOpsBranchesMixin,
)
from openhands.integrations.azure_devops.service.features import (
from openhands.app_server.integrations.azure_devops.service.features import (
AzureDevOpsFeaturesMixin,
)
from openhands.integrations.azure_devops.service.prs import AzureDevOpsPRsMixin
from openhands.integrations.azure_devops.service.repos import AzureDevOpsReposMixin
from openhands.integrations.azure_devops.service.resolver import (
from openhands.app_server.integrations.azure_devops.service.prs import (
AzureDevOpsPRsMixin,
)
from openhands.app_server.integrations.azure_devops.service.repos import (
AzureDevOpsReposMixin,
)
from openhands.app_server.integrations.azure_devops.service.resolver import (
AzureDevOpsResolverMixin,
)
from openhands.integrations.azure_devops.service.work_items import (
from openhands.app_server.integrations.azure_devops.service.work_items import (
AzureDevOpsWorkItemsMixin,
)
from openhands.integrations.protocols.http_client import HTTPClient
from openhands.integrations.service_types import (
from openhands.app_server.integrations.protocols.http_client import HTTPClient
from openhands.app_server.integrations.service_types import (
BaseGitService,
GitService,
ProviderType,
@@ -242,7 +246,7 @@ class AzureDevOpsService(
# Dynamic class loading to support custom implementations (e.g., SaaS)
azure_devops_service_cls = os.environ.get(
'OPENHANDS_AZURE_DEVOPS_SERVICE_CLS',
'openhands.integrations.azure_devops.azure_devops_service.AzureDevOpsService',
'openhands.app_server.integrations.azure_devops.azure_devops_service.AzureDevOpsService',
)
# Lazy loading to avoid circular imports

View File

@@ -4,8 +4,8 @@ from urllib.parse import quote
from pydantic import SecretStr
from openhands.integrations.protocols.http_client import HTTPClient
from openhands.integrations.service_types import (
from openhands.app_server.integrations.protocols.http_client import HTTPClient
from openhands.app_server.integrations.service_types import (
BaseGitService,
RequestMethod,
)

View File

@@ -1,7 +1,12 @@
"""Branch operations for Azure DevOps integration."""
from openhands.integrations.azure_devops.service.base import AzureDevOpsMixinBase
from openhands.integrations.service_types import Branch, PaginatedBranchesResponse
from openhands.app_server.integrations.azure_devops.service.base import (
AzureDevOpsMixinBase,
)
from openhands.app_server.integrations.service_types import (
Branch,
PaginatedBranchesResponse,
)
class AzureDevOpsBranchesMixin(AzureDevOpsMixinBase):

View File

@@ -1,7 +1,9 @@
"""Feature operations for Azure DevOps integration (microagents, suggested tasks, user)."""
from openhands.integrations.azure_devops.service.base import AzureDevOpsMixinBase
from openhands.integrations.service_types import (
from openhands.app_server.integrations.azure_devops.service.base import (
AzureDevOpsMixinBase,
)
from openhands.app_server.integrations.service_types import (
ProviderType,
RequestMethod,
SuggestedTask,

Some files were not shown because too many files have changed in this diff Show More