Compare commits

..

7 Commits

Author SHA1 Message Date
openhands
58b8c66215 Fix enterprise linting: use single quotes
Apply enterprise linting rules (single quotes) to test file.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 19:18:43 +00:00
openhands
48ed801b27 Fix failing tests: mock module-level GitHub dependencies
The test file was failing because importing GitHubTokenResponse from
server.routes.integration.github causes Python to execute the entire
module, including line 32 which instantiates GitHubDataCollector().
This requires GitHub App credentials (GITHUB_APP_CLIENT_ID and
GITHUB_APP_PRIVATE_KEY) that are not available in CI.

Changes:
- Add autouse fixture to mock integrations.github.data_collector and
  integrations.github.github_manager modules before importing
- Fix test assertion for empty provider tokens - empty dict {} is falsy,
  so it triggers 'No provider tokens' error, not 'No GitHub token'
- Apply ruff formatting to the test file

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 19:13:31 +00:00
chuckbutkus
756caebd27 Merge branch 'main' into add-github-token-endpoint 2026-04-28 14:58:55 -04:00
chuckbutkus
8d573d9cc6 Lint fix 2026-04-28 13:51:24 -04:00
chuckbutkus
fd81234ac4 Lint fixes 2026-04-28 13:46:02 -04:00
chuckbutkus
080ea0db5e Merge branch 'main' into add-github-token-endpoint 2026-04-28 13:09:03 -04:00
openhands
5156c580fe Add /integration/github/token endpoint
This endpoint returns a GitHub access token for the authenticated user,
refreshing the token if necessary.

- Add GitHubTokenResponse Pydantic model
- Add GET /integration/github/token endpoint
- Add unit tests for the endpoint

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 17:00:19 +00:00
279 changed files with 2963 additions and 1421 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/app_server/integrations/vscode && npm run lint:fix && npm run compile ; 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 ../../..`
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/app_server/integrations/vscode` directory
- Located in the `openhands/integrations/vscode` directory
- Setup: Run `npm install` in the extension directory
- Linting:
- Run linting with fixes: `npm run lint:fix`

View File

@@ -1,11 +1,9 @@
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.app_server.integrations.bitbucket_data_center.bitbucket_dc_service import (
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.bitbucket_data_center.bitbucket_dc_service import (
BitbucketDCService,
)
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 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.app_server.integrations.gitlab.gitlab_service import GitLabService
from openhands.app_server.integrations.service_types import (
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.gitlab.gitlab_service import GitLabService
from openhands.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.app_server.user_auth.user_auth import UserAuth
from openhands.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.app_server.user_auth.user_auth import UserAuth
from openhands.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,14 +1,11 @@
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.app_server.user_auth.user_auth import UserAuth
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler
from openhands.integrations.service_types import ProviderType, UserGitInfo
from openhands.sdk.secret import SecretSource, StaticSecret
from openhands.server.user_auth.user_auth import UserAuth
class ResolverUserContext(UserContext):

View File

@@ -28,23 +28,22 @@ from slack_sdk.oauth import AuthorizeUrlGenerator
from slack_sdk.web.async_client import AsyncWebClient
from sqlalchemy import select
from storage.database import a_session_maker
from storage.redis import get_redis_client_async
from storage.slack_user import SlackUser
from openhands.app_server.integrations.provider import ProviderHandler
from openhands.app_server.integrations.service_types import (
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler
from openhands.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
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,
@@ -115,7 +114,7 @@ class SlackManager(Manager[SlackViewInterface]):
"""
key = f'{SLACK_USER_MSG_KEY_PREFIX}:{message_ts}:{thread_ts}'
try:
redis = get_redis_client_async()
redis = sio.manager.redis
await redis.set(key, user_msg, ex=SLACK_USER_MSG_EXPIRATION)
logger.info(
'slack_stored_user_msg',
@@ -158,7 +157,7 @@ class SlackManager(Manager[SlackViewInterface]):
"""
key = f'{SLACK_USER_MSG_KEY_PREFIX}:{message_ts}:{thread_ts}'
try:
redis = get_redis_client_async()
redis = sio.manager.redis
user_msg = await redis.get(key)
if user_msg:
# Redis returns bytes, decode to string

View File

@@ -5,7 +5,7 @@ from integrations.types import SummaryExtractionTracker
from jinja2 import Environment
from storage.slack_user import SlackUser
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.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.app_server.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.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.app_server.integrations.service_types import Repository
from openhands.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/app_server/integrations/templates/resolver/'
or 'openhands/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

@@ -8,6 +8,7 @@ load_dotenv()
if not os.getenv('OPENHANDS_CONFIG_CLS'):
os.environ['OPENHANDS_CONFIG_CLS'] = 'server.config.SaaSServerConfig'
import socketio # noqa: E402
from fastapi import Request, status # noqa: E402
from fastapi.middleware.cors import CORSMiddleware # noqa: E402
from fastapi.responses import JSONResponse # noqa: E402
@@ -60,6 +61,7 @@ from server.verified_models.verified_model_router import ( # noqa: E402
)
from openhands.server.app import app as base_app # noqa: E402
from openhands.server.listen_socket import sio # noqa: E402
from openhands.server.middleware import ( # noqa: E402
CacheControlMiddleware,
)
@@ -176,5 +178,4 @@ async def expired_exception_handler(request: Request, exc: ExpiredError):
return JSONResponse({'error': ExpiredError.__name__}, status.HTTP_401_UNAUTHORIZED)
# Note: socketio is no longer used for communication. The base FastAPI app is used directly.
app = base_app
app = socketio.ASGIApp(sio, other_asgi_app=base_app)

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.app_server.integrations.gitlab.constants import GITLAB_HOST
from openhands.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.app_server.integrations.gitlab.gitlab_service -> get_impl
# -> openhands.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.integrations.provider import (
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 (
PROVIDER_TOKEN_TYPE,
ProviderToken,
ProviderType,
)
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
from openhands.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.app_server.integrations.service_types import ProviderType
from openhands.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,7 +3,6 @@ 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
@@ -47,16 +46,13 @@ 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')
@@ -67,18 +63,6 @@ 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.app_server.user_auth import get_user_id
from openhands.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,21 +2,25 @@ import asyncio
import hashlib
import hmac
import os
from typing import cast
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, status
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 (
@@ -105,3 +109,42 @@ 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

@@ -18,11 +18,11 @@ from pydantic import BaseModel
from server.auth.token_manager import TokenManager
from storage.gitlab_webhook import GitlabWebhook
from storage.gitlab_webhook_store import GitlabWebhookStore
from storage.redis import get_redis_client_async
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()
@@ -103,7 +103,7 @@ async def gitlab_events(
dedup_hash = hashlib.sha256(dedup_json.encode()).hexdigest()
dedup_key = f'gitlab_msg: {dedup_hash}'
redis = get_redis_client_async()
redis = sio.manager.redis
created = await redis.set(dedup_key, 1, nx=True, ex=60)
if not created:
logger.info('gitlab_is_duplicate')

View File

@@ -18,10 +18,10 @@ from server.auth.constants import JIRA_CLIENT_ID, JIRA_CLIENT_SECRET
from server.auth.saas_user_auth import SaasUserAuth
from server.auth.token_manager import TokenManager
from storage.jira_workspace import JiraWorkspace
from storage.redis import get_redis_client
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 (
@@ -123,7 +123,7 @@ class JiraValidateWorkspaceResponse(BaseModel):
jira_integration_router = APIRouter(prefix='/integration/jira')
token_manager = TokenManager()
jira_manager = JiraManager(token_manager)
redis_client = get_redis_client()
redis_client = create_redis_client()
async def verify_jira_signature(body: bytes, signature: str, payload: dict):

View File

@@ -26,10 +26,10 @@ from server.auth.constants import (
from server.auth.saas_user_auth import SaasUserAuth
from server.auth.token_manager import TokenManager
from server.constants import WEB_HOST
from storage.redis import get_redis_client
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 (
@@ -129,7 +129,7 @@ class JiraDcValidateWorkspaceResponse(BaseModel):
jira_dc_integration_router = APIRouter(prefix='/integration/jira-dc')
token_manager = TokenManager()
jira_dc_manager = JiraDcManager(token_manager)
redis_client = get_redis_client()
redis_client = create_redis_client()
async def _handle_workspace_link_creation(

View File

@@ -35,16 +35,12 @@ from slack_sdk.signature import SignatureVerifier
from slack_sdk.web.async_client import AsyncWebClient
from sqlalchemy import delete
from storage.database import a_session_maker
from storage.redis import get_redis_client_async
from storage.slack_team_store import SlackTeamStore
from storage.slack_user import SlackUser
from storage.user_store import UserStore
from openhands.app_server.integrations.service_types import (
ProviderTimeoutError,
ProviderType,
)
from openhands.server.shared import config
from openhands.integrations.service_types import ProviderTimeoutError, ProviderType
from openhands.server.shared import config, sio
signature_verifier = SignatureVerifier(signing_secret=SLACK_SIGNING_SECRET)
slack_router = APIRouter(prefix='/slack')
@@ -328,7 +324,7 @@ async def on_event(request: Request, background_tasks: BackgroundTasks):
team_id = payload['team_id']
# Sometimes slack sends duplicates, so we need to make sure this is not a duplicate.
redis = get_redis_client_async()
redis = sio.manager.redis
key = f'slack_msg:{client_msg_id}'
created = await redis.set(key, 1, nx=True, ex=60)
if not created:

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

@@ -1,7 +1,7 @@
from fastapi import APIRouter, HTTPException, status
from sqlalchemy.sql import text
from storage.database import a_session_maker
from storage.redis import get_redis_client
from storage.redis import create_redis_client
from openhands.core.logger import openhands_logger as logger
@@ -23,7 +23,7 @@ async def is_ready():
# Check Redis connection
try:
redis_client = get_redis_client()
redis_client = create_redis_client()
redis_client.ping()
except Exception as e:
logger.error(f'Redis check failed: {str(e)}')

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

@@ -34,10 +34,10 @@ from server.auth.constants import (
AUTOMATION_WEBHOOK_SECRET,
)
from server.auth.token_manager import TokenManager
from storage.redis import get_redis_client_async
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
ORG_CLAIM_CACHE_TTL_SECONDS = 3600 # 1 hour for org claims (rarely change)
@@ -382,7 +382,16 @@ class AutomationEventService:
Monitor logs for 'Redis unavailable' warnings to detect degradation.
"""
try:
redis = get_redis_client_async()
redis = getattr(sio.manager, 'redis', None)
if not redis:
# Log at warning level - this is a significant degradation that
# will cause DB load. Monitor these logs for alerting.
logger.warning(
'[AutomationEventService] Redis unavailable for cache read, '
'falling back to direct DB queries (this will increase DB load)'
)
return None
cached = await redis.get(cache_key)
if cached is None:
return None
@@ -406,7 +415,11 @@ class AutomationEventService:
Fails silently if Redis is unavailable (graceful degradation).
"""
try:
redis = get_redis_client_async()
redis = getattr(sio.manager, 'redis', None)
if not redis:
# Silent failure - read path already logs the warning
return
await redis.setex(cache_key, ttl_seconds, value)
except Exception as e:
# Log at warning level for visibility

View File

@@ -1,7 +1,7 @@
from datetime import datetime
# Simplified imports to avoid dependency chain issues
# from openhands.app_server.integrations.service_types import ProviderType
# from openhands.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

@@ -1,7 +1,7 @@
from fastapi import HTTPException, Request, status
from storage.redis import get_redis_client_async
from openhands.core.logger import openhands_logger as logger
from openhands.server.shared import sio
# Rate limiting constants
RATE_LIMIT_USER_SECONDS = 120 # 2 minutes per user_id
@@ -32,7 +32,7 @@ async def check_rate_limit_by_user_id(
HTTPException: If rate limit is exceeded (429 status code)
"""
try:
redis = get_redis_client_async()
redis = sio.manager.redis
if not redis:
# If Redis is unavailable, log warning and allow request (fail open)
logger.warning('Redis unavailable for rate limiting, allowing request')

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

@@ -1,76 +1,23 @@
import os
import threading
from redis import Redis
from redis import asyncio as aioredis
from redis import exceptions as redis_exceptions
import redis
# Redis configuration
REDIS_HOST = os.environ.get('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.environ.get('REDIS_PORT', '6379'))
REDIS_PASSWORD = os.environ.get('REDIS_PASSWORD', '')
REDIS_DB = int(os.environ.get('REDIS_DB', '0'))
REDIS_SOCKET_TIMEOUT = 2
_redis_client: Redis | None = None
_redis_client_async: aioredis.Redis | None = None
_redis_lock = threading.Lock()
def _get_redis_kwargs():
"""Return common kwargs for Redis client creation."""
return {
'host': REDIS_HOST,
'port': REDIS_PORT,
'password': REDIS_PASSWORD,
'db': REDIS_DB,
'socket_timeout': REDIS_SOCKET_TIMEOUT,
}
def get_redis_client() -> Redis:
"""Get a shared synchronous Redis client, lazily initialized.
Thread-safe with double-checked locking pattern.
Returns:
A Redis client for synchronous operations.
"""
global _redis_client
if _redis_client is None:
with _redis_lock:
if _redis_client is None:
_redis_client = Redis(**_get_redis_kwargs())
return _redis_client
def get_redis_client_async() -> aioredis.Redis:
"""Get a shared asynchronous Redis client, lazily initialized.
Note: This function is synchronous but returns an async client.
Thread-safe initialization is handled via a threading lock since
asyncio.Lock cannot be used in a sync context.
Returns:
An aioredis client for asynchronous operations.
"""
global _redis_client_async
if _redis_client_async is None:
with _redis_lock:
if _redis_client_async is None:
_redis_client_async = aioredis.Redis(**_get_redis_kwargs())
return _redis_client_async
def create_redis_client():
return redis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
password=REDIS_PASSWORD,
db=REDIS_DB,
socket_timeout=2,
)
def get_redis_authed_url():
return f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}'
__all__ = [
'Redis',
'aioredis',
'get_redis_client',
'get_redis_client_async',
'get_redis_authed_url',
'redis_exceptions',
]

View File

@@ -109,10 +109,10 @@ class UserStore:
@staticmethod
def _get_redis_client():
"""Get the shared async Redis client from enterprise storage."""
from storage.redis import get_redis_client_async
"""Get the Redis client from the Socket.IO manager."""
from openhands.server.shared import sio
return get_redis_client_async()
return getattr(sio.manager, 'redis', None)
@staticmethod
async def _acquire_user_creation_lock(user_id: str) -> bool:
@@ -121,21 +121,19 @@ class UserStore:
Returns True if the lock was acquired or if Redis is unavailable (fallback to no locking).
Returns False if another process holds the lock.
"""
from storage.redis import redis_exceptions
redis_client = UserStore._get_redis_client()
try:
user_key = f'{_REDIS_USER_CREATION_KEY_PREFIX}{user_id}'
lock_acquired = await redis_client.set(
user_key, 1, nx=True, ex=_REDIS_CREATE_TIMEOUT_SECONDS
)
return bool(lock_acquired)
except redis_exceptions.RedisError:
if redis_client is None:
logger.warning(
'user_store:_acquire_user_creation_lock:redis_error',
'user_store:_acquire_user_creation_lock:no_redis_client',
extra={'user_id': user_id},
)
return True # Proceed without locking on error
return True # Proceed without locking if Redis is unavailable
user_key = f'{_REDIS_USER_CREATION_KEY_PREFIX}{user_id}'
lock_acquired = await redis_client.set(
user_key, 1, nx=True, ex=_REDIS_CREATE_TIMEOUT_SECONDS
)
return bool(lock_acquired)
@staticmethod
async def _release_user_creation_lock(user_id: str) -> bool:
@@ -144,19 +142,17 @@ class UserStore:
Returns True if the lock was released or if Redis is unavailable.
Returns False if the lock could not be released.
"""
from storage.redis import redis_exceptions
redis_client = UserStore._get_redis_client()
try:
user_key = f'{_REDIS_USER_CREATION_KEY_PREFIX}{user_id}'
deleted = await redis_client.delete(user_key)
return bool(deleted)
except redis_exceptions.RedisError:
if redis_client is None:
logger.warning(
'user_store:_release_user_creation_lock:redis_error',
'user_store:_release_user_creation_lock:no_redis_client',
extra={'user_id': user_id},
)
return True # Proceed without locking on error
return True # Nothing to release if Redis is unavailable
user_key = f'{_REDIS_USER_CREATION_KEY_PREFIX}{user_id}'
deleted = await redis_client.delete(user_key)
return bool(deleted)
@staticmethod
async def migrate_user(

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/app_server/integrations/bitbucket_data_center/service/base.py,
# See openhands/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,10 +24,7 @@ def jinja_env() -> Environment:
repo_root = Path(__file__).resolve().parents[5]
return Environment(
loader=FileSystemLoader(
str(
repo_root
/ 'openhands/app_server/integrations/templates/resolver/github'
)
str(repo_root / 'openhands/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.app_server.integrations.service_types import ProviderType, Repository
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.integrations.service_types import ProviderType, Repository
from openhands.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.app_server.integrations.service_types import ProviderType, Repository
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.integrations.service_types import ProviderType, Repository
from openhands.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.app_server.integrations.service_types import ProviderType, Repository
from openhands.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.app_server.user_auth.user_auth import UserAuth
from openhands.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.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
from openhands.integrations.provider import CustomSecret, ProviderToken
from openhands.integrations.service_types import ProviderType
# 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.app_server.integrations.provider import ProviderHandler
from openhands.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.app_server.user_auth.user_auth import AuthType
from openhands.server.user_auth.user_auth import AuthType
class TestVerifyByorKeyInLitellm:

View File

@@ -0,0 +1,193 @@
"""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.app_server.user_auth import get_user_id
from openhands.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.app_server.user_auth import get_user_id
from openhands.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.app_server.user_auth.user_auth import UserAuth
from openhands.server.user_auth.user_auth import UserAuth
# Create AuthUserContext with a non-SaasUserAuth
mock_user_auth = MagicMock(spec=UserAuth)

View File

@@ -15,9 +15,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openhands.app_server.integrations.service_types import ProviderType
REDIS_PATCH = 'server.services.automation_event_service.get_redis_client_async'
from openhands.integrations.service_types import ProviderType
# Default patches for constants
CONSTANT_PATCHES = {
@@ -93,8 +91,10 @@ def github_user_payload():
def create_service(mock_token_manager):
"""Helper to create a service with mocked constants."""
with patch.dict('os.environ', {}, clear=False):
"""Helper to create a service with mocked sio and constants."""
with patch('server.services.automation_event_service.sio'), patch.dict(
'os.environ', {}, clear=False
):
for key, value in CONSTANT_PATCHES.items():
patch(key, value).start()
@@ -123,7 +123,9 @@ class TestResolveGitOrg:
'server.services.automation_event_service.resolve_org_for_repo',
new_callable=AsyncMock,
return_value=mock_org_git_claim.org_id,
), patch(REDIS_PATCH, return_value=mock_redis):
), patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
result = await service._resolve_git_org(ProviderType.GITHUB, 'test-org')
@@ -145,7 +147,11 @@ class TestResolveGitOrg:
with patch(
'server.services.automation_event_service.resolve_org_for_repo',
new_callable=AsyncMock,
) as mock_resolver, patch(REDIS_PATCH, return_value=mock_redis):
) as mock_resolver, patch(
'server.services.automation_event_service.sio'
) as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
result = await service._resolve_git_org(ProviderType.GITHUB, 'test-org')
@@ -168,7 +174,9 @@ class TestResolveGitOrg:
'server.services.automation_event_service.resolve_org_for_repo',
new_callable=AsyncMock,
return_value=None,
), patch(REDIS_PATCH, return_value=mock_redis):
), patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
result = await service._resolve_git_org(
ProviderType.GITHUB, 'unclaimed-org'
@@ -194,7 +202,11 @@ class TestResolveGitOrg:
with patch(
'server.services.automation_event_service.resolve_org_for_repo',
new_callable=AsyncMock,
) as mock_resolver, patch(REDIS_PATCH, return_value=mock_redis):
) as mock_resolver, patch(
'server.services.automation_event_service.sio'
) as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
result = await service._resolve_git_org(
ProviderType.GITHUB, 'unclaimed-org'
@@ -220,7 +232,9 @@ class TestResolveGitOrg:
'server.services.automation_event_service.resolve_org_for_repo',
new_callable=AsyncMock,
return_value=mock_org_git_claim.org_id,
), patch(REDIS_PATCH, return_value=mock_redis):
), patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
# Call for GitHub
@@ -250,7 +264,9 @@ class TestResolvePersonalOrg:
mock_redis.get = AsyncMock(return_value=None) # Cache miss
mock_redis.setex = AsyncMock()
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
result = await service._resolve_personal_org(ProviderType.GITHUB, 12345)
@@ -268,7 +284,9 @@ class TestResolvePersonalOrg:
mock_redis = AsyncMock()
mock_redis.get = AsyncMock(return_value=keycloak_id.encode())
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
result = await service._resolve_personal_org(ProviderType.GITHUB, 12345)
@@ -306,7 +324,9 @@ class TestResolvePersonalOrg:
mock_redis.get = AsyncMock(return_value=None)
mock_redis.setex = AsyncMock()
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
# Call for GitHub
@@ -339,11 +359,15 @@ class TestForwardEvent:
'server.services.automation_event_service.resolve_org_for_repo',
new_callable=AsyncMock,
return_value=mock_org_git_claim.org_id,
), patch(REDIS_PATCH, return_value=mock_redis), patch.object(
), patch(
'server.services.automation_event_service.sio'
) as mock_sio, patch.object(
AutomationEventService,
'_send_to_automation_service',
new_callable=AsyncMock,
) as mock_send:
mock_sio.manager.redis = mock_redis
service = AutomationEventService(mock_token_manager)
await service.forward_event(
provider=ProviderType.GITHUB,
@@ -387,11 +411,15 @@ class TestForwardEvent:
'server.services.automation_event_service.resolve_org_for_repo',
new_callable=AsyncMock,
return_value=None, # No org claim for personal repo
), patch(REDIS_PATCH, return_value=mock_redis), patch.object(
), patch(
'server.services.automation_event_service.sio'
) as mock_sio, patch.object(
AutomationEventService,
'_send_to_automation_service',
new_callable=AsyncMock,
) as mock_send:
mock_sio.manager.redis = mock_redis
service = AutomationEventService(mock_token_manager)
await service.forward_event(
provider=ProviderType.GITHUB,
@@ -422,7 +450,7 @@ class TestForwardEvent:
'sender': {'id': 12345, 'login': 'testuser'},
}
with patch(
with patch('server.services.automation_event_service.sio'), patch(
'server.services.automation_event_service.logger'
) as mock_logger, patch.object(
AutomationEventService,
@@ -459,13 +487,15 @@ class TestForwardEvent:
'server.services.automation_event_service.resolve_org_for_repo',
new_callable=AsyncMock,
return_value=None,
), patch(REDIS_PATCH, return_value=mock_redis), patch(
), patch('server.services.automation_event_service.sio') as mock_sio, patch(
'server.services.automation_event_service.logger'
) as mock_logger, patch.object(
AutomationEventService,
'_send_to_automation_service',
new_callable=AsyncMock,
) as mock_send:
mock_sio.manager.redis = mock_redis
service = AutomationEventService(mock_token_manager)
await service.forward_event(
provider=ProviderType.GITHUB,
@@ -578,7 +608,7 @@ class TestSendToAutomationService:
with patch(
'server.services.automation_event_service.AUTOMATION_SERVICE_URL',
'https://automation.example.com',
), patch(
), patch('server.services.automation_event_service.sio'), patch(
'server.services.automation_event_service.aiohttp.ClientSession',
return_value=mock_session_context,
):
@@ -623,7 +653,7 @@ class TestSendToAutomationService:
with patch(
'server.services.automation_event_service.AUTOMATION_SERVICE_URL',
'https://automation.example.com',
), patch(
), patch('server.services.automation_event_service.sio'), patch(
'server.services.automation_event_service.aiohttp.ClientSession',
return_value=mock_session_context,
):
@@ -648,7 +678,9 @@ class TestSendToAutomationService:
with patch(
'server.services.automation_event_service.AUTOMATION_SERVICE_URL', None
), patch('server.services.automation_event_service.logger') as mock_logger:
), patch('server.services.automation_event_service.sio'), patch(
'server.services.automation_event_service.logger'
) as mock_logger:
service = create_service(mock_token_manager)
await service._send_to_automation_service(
ProviderType.GITHUB, org_id, payload
@@ -670,7 +702,7 @@ class TestSignPayload:
with patch(
'server.services.automation_event_service.AUTOMATION_WEBHOOK_SECRET',
'test-shared-secret',
):
), patch('server.services.automation_event_service.sio'):
service = create_service(mock_token_manager)
payload_bytes = b'{"test": "data"}'
@@ -702,7 +734,7 @@ class TestSignPayload:
with patch(
'server.services.automation_event_service.AUTOMATION_WEBHOOK_SECRET',
shared_secret,
):
), patch('server.services.automation_event_service.sio'):
service = create_service(mock_token_manager)
signature = service._sign_payload(payload_bytes)
@@ -722,7 +754,9 @@ class TestCacheHelpers:
mock_redis = AsyncMock()
mock_redis.get = AsyncMock(return_value=b'cached-value')
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
result = await service._get_cached_value('test-key')
@@ -738,7 +772,9 @@ class TestCacheHelpers:
mock_redis = AsyncMock()
mock_redis.get = AsyncMock(return_value=None)
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
result = await service._get_cached_value('test-key')
@@ -751,7 +787,9 @@ class TestCacheHelpers:
WHEN: _get_cached_value is called
THEN: None is returned (graceful degradation)
"""
with patch(REDIS_PATCH, return_value=None):
with patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = None
service = create_service(mock_token_manager)
result = await service._get_cached_value('test-key')
@@ -767,7 +805,9 @@ class TestCacheHelpers:
mock_redis = AsyncMock()
mock_redis.setex = AsyncMock()
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
service = create_service(mock_token_manager)
await service._set_cached_value('test-key', 'test-value', 3600)
@@ -780,7 +820,9 @@ class TestCacheHelpers:
WHEN: _set_cached_value is called
THEN: No error is raised (silent failure)
"""
with patch(REDIS_PATCH, return_value=None):
with patch('server.services.automation_event_service.sio') as mock_sio:
mock_sio.manager.redis = None
service = create_service(mock_token_manager)
# Should not raise
await service._set_cached_value('test-key', 'test-value', 3600)

View File

@@ -8,8 +8,6 @@ from server.utils.rate_limit_utils import (
check_rate_limit_by_user_id,
)
REDIS_PATCH = 'server.utils.rate_limit_utils.get_redis_client_async'
@pytest.fixture
def mock_request():
@@ -36,9 +34,11 @@ async def test_rate_limit_by_user_id_first_request_succeeds(mock_request, mock_r
key_prefix = 'email_resend'
with (
patch(REDIS_PATCH, return_value=mock_redis),
patch('server.utils.rate_limit_utils.sio') as mock_sio,
patch('server.utils.rate_limit_utils.logger') as mock_logger,
):
mock_sio.manager.redis = mock_redis
# Act
await check_rate_limit_by_user_id(
request=mock_request, key_prefix=key_prefix, user_id=user_id
@@ -63,9 +63,11 @@ async def test_rate_limit_by_user_id_second_request_within_window_fails(
mock_redis.set = AsyncMock(return_value=False) # Key already exists
with (
patch(REDIS_PATCH, return_value=mock_redis),
patch('server.utils.rate_limit_utils.sio') as mock_sio,
patch('server.utils.rate_limit_utils.logger') as mock_logger,
):
mock_sio.manager.redis = mock_redis
# Act & Assert
with pytest.raises(HTTPException) as exc_info:
await check_rate_limit_by_user_id(
@@ -85,9 +87,11 @@ async def test_rate_limit_by_ip_when_user_id_is_none(mock_request, mock_redis):
key_prefix = 'email_resend'
with (
patch(REDIS_PATCH, return_value=mock_redis),
patch('server.utils.rate_limit_utils.sio') as mock_sio,
patch('server.utils.rate_limit_utils.logger') as mock_logger,
):
mock_sio.manager.redis = mock_redis
# Act
await check_rate_limit_by_user_id(
request=mock_request, key_prefix=key_prefix, user_id=None
@@ -112,7 +116,11 @@ async def test_rate_limit_by_ip_second_request_within_window_fails(
key_prefix = 'email_resend'
mock_redis.set = AsyncMock(return_value=False) # Key already exists
with patch(REDIS_PATCH, return_value=mock_redis):
with (
patch('server.utils.rate_limit_utils.sio') as mock_sio,
):
mock_sio.manager.redis = mock_redis
# Act & Assert
with pytest.raises(HTTPException) as exc_info:
await check_rate_limit_by_user_id(
@@ -131,9 +139,11 @@ async def test_rate_limit_redis_unavailable_fails_open(mock_request):
user_id = 'test_user_id'
with (
patch(REDIS_PATCH, return_value=None),
patch('server.utils.rate_limit_utils.sio') as mock_sio,
patch('server.utils.rate_limit_utils.logger') as mock_logger,
):
mock_sio.manager.redis = None # Redis unavailable
# Act
await check_rate_limit_by_user_id(
request=mock_request, key_prefix=key_prefix, user_id=user_id
@@ -154,9 +164,11 @@ async def test_rate_limit_redis_exception_fails_open(mock_request, mock_redis):
mock_redis.set = AsyncMock(side_effect=Exception('Redis connection error'))
with (
patch(REDIS_PATCH, return_value=mock_redis),
patch('server.utils.rate_limit_utils.sio') as mock_sio,
patch('server.utils.rate_limit_utils.logger') as mock_logger,
):
mock_sio.manager.redis = mock_redis
# Act
await check_rate_limit_by_user_id(
request=mock_request, key_prefix=key_prefix, user_id=user_id
@@ -174,7 +186,9 @@ async def test_rate_limit_custom_key_prefix(mock_request, mock_redis):
user_id = 'test_user_id'
key_prefix = 'password_reset'
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.utils.rate_limit_utils.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
# Act
await check_rate_limit_by_user_id(
request=mock_request, key_prefix=key_prefix, user_id=user_id
@@ -195,7 +209,9 @@ async def test_rate_limit_custom_rate_limit_seconds(mock_request, mock_redis):
custom_user_seconds = 60
custom_ip_seconds = 180
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.utils.rate_limit_utils.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
# Act
await check_rate_limit_by_user_id(
request=mock_request,
@@ -218,7 +234,9 @@ async def test_rate_limit_ip_with_unknown_client(mock_request, mock_redis):
key_prefix = 'email_resend'
mock_request.client = None # No client information
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.utils.rate_limit_utils.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
# Act
await check_rate_limit_by_user_id(
request=mock_request, key_prefix=key_prefix, user_id=None
@@ -240,7 +258,9 @@ async def test_rate_limit_different_users_have_separate_limits(
user_id_1 = 'user_1'
user_id_2 = 'user_2'
with patch(REDIS_PATCH, return_value=mock_redis):
with patch('server.utils.rate_limit_utils.sio') as mock_sio:
mock_sio.manager.redis = mock_redis
# Act
await check_rate_limit_by_user_id(
request=mock_request, key_prefix=key_prefix, user_id=user_id_1

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.app_server.integrations.service_types import ProviderType
from openhands.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.app_server.user_auth.user_auth import AuthType
from openhands.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.app_server.integrations.service_types import ProviderType
from openhands.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.app_server.user_auth.default_user_auth.DefaultUserAuth.get_instance'
'openhands.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

@@ -15,9 +15,9 @@ from server.routes.integration.gitlab import gitlab_events
@pytest.mark.asyncio
@patch('server.routes.integration.gitlab.verify_gitlab_signature')
@patch('server.routes.integration.gitlab.gitlab_manager')
@patch('server.routes.integration.gitlab.get_redis_client_async')
@patch('server.routes.integration.gitlab.sio')
async def test_gitlab_events_deduplication_with_object_id(
mock_get_redis_client_async, mock_gitlab_manager, mock_verify_signature
mock_sio, mock_gitlab_manager, mock_verify_signature
):
"""Test that duplicate GitLab events are deduplicated using object_attributes.id."""
# Setup mocks
@@ -26,7 +26,7 @@ async def test_gitlab_events_deduplication_with_object_id(
# Mock Redis
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
# First request - Redis returns True (key was set)
mock_redis.set.return_value = True
@@ -90,9 +90,9 @@ async def test_gitlab_events_deduplication_with_object_id(
@pytest.mark.asyncio
@patch('server.routes.integration.gitlab.verify_gitlab_signature')
@patch('server.routes.integration.gitlab.gitlab_manager')
@patch('server.routes.integration.gitlab.get_redis_client_async')
@patch('server.routes.integration.gitlab.sio')
async def test_gitlab_events_deduplication_without_object_id(
mock_get_redis_client_async, mock_gitlab_manager, mock_verify_signature
mock_sio, mock_gitlab_manager, mock_verify_signature
):
"""Test that GitLab events without object_attributes.id are deduplicated using hash of payload."""
# Setup mocks
@@ -101,7 +101,7 @@ async def test_gitlab_events_deduplication_without_object_id(
# Mock Redis
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
# First request - Redis returns True (key was set)
mock_redis.set.return_value = True
@@ -170,9 +170,9 @@ async def test_gitlab_events_deduplication_without_object_id(
@pytest.mark.asyncio
@patch('server.routes.integration.gitlab.verify_gitlab_signature')
@patch('server.routes.integration.gitlab.gitlab_manager')
@patch('server.routes.integration.gitlab.get_redis_client_async')
@patch('server.routes.integration.gitlab.sio')
async def test_gitlab_events_different_payloads_not_deduplicated(
mock_get_redis_client_async, mock_gitlab_manager, mock_verify_signature
mock_sio, mock_gitlab_manager, mock_verify_signature
):
"""Test that different GitLab events are not deduplicated."""
# Setup mocks
@@ -181,7 +181,7 @@ async def test_gitlab_events_different_payloads_not_deduplicated(
# Mock Redis
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
mock_redis.set.return_value = True # Always return True for this test
# First payload with ID 123
@@ -240,9 +240,9 @@ async def test_gitlab_events_different_payloads_not_deduplicated(
@pytest.mark.asyncio
@patch('server.routes.integration.gitlab.verify_gitlab_signature')
@patch('server.routes.integration.gitlab.gitlab_manager')
@patch('server.routes.integration.gitlab.get_redis_client_async')
@patch('server.routes.integration.gitlab.sio')
async def test_gitlab_events_multiple_identical_payloads_deduplicated(
mock_get_redis_client_async, mock_gitlab_manager, mock_verify_signature
mock_sio, mock_gitlab_manager, mock_verify_signature
):
"""Test that multiple identical GitLab events are properly deduplicated."""
# Setup mocks
@@ -251,7 +251,7 @@ async def test_gitlab_events_multiple_identical_payloads_deduplicated(
# Mock Redis
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
# Create a payload with object_attributes.id
payload = {

View File

@@ -68,7 +68,7 @@ class TestAcceptInvitationPostEndpoint:
def auth_app(self):
"""Create a FastAPI app with dependency overrides for authenticated tests."""
from openhands.app_server.user_auth import get_user_id
from openhands.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.app_server.user_auth import get_user_id
from openhands.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.app_server.integrations.service_types import (
from openhands.integrations.service_types import (
ProviderTimeoutError,
ProviderType,
Repository,
)
from openhands.app_server.user_auth.user_auth import UserAuth
from openhands.server.user_auth.user_auth import UserAuth
@pytest.fixture
@@ -89,21 +89,21 @@ def test_infer_repo_from_message(message, expected):
class TestRepoVerificationHandling:
"""Test repo verification handling for Slack integration."""
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
@patch('integrations.slack.slack_manager.ProviderHandler')
@patch.object(SlackManager, 'send_message', new_callable=AsyncMock)
async def test_timeout_during_verification_shows_selector(
self,
mock_send_message,
mock_provider_handler_class,
mock_get_redis_client_async,
mock_sio,
slack_manager,
slack_new_conversation_view,
):
"""Test that when repo verification times out, selector is shown."""
# Setup Redis mock
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
# Setup: Modify message to include exactly one repo reference to trigger verification
slack_new_conversation_view.user_msg = 'Help me with OpenHands/OpenHands repo'
@@ -132,12 +132,12 @@ class TestRepoVerificationHandling:
assert isinstance(selector_message, dict)
assert selector_message.get('text') == 'Choose a Repository:'
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
@patch.object(SlackManager, 'send_message', new_callable=AsyncMock)
async def test_no_repo_mentioned_shows_button_and_dropdown(
self,
mock_send_message,
mock_get_redis_client_async,
mock_sio,
slack_manager,
slack_new_conversation_view,
):
@@ -149,7 +149,7 @@ class TestRepoVerificationHandling:
"""
# Setup Redis mock
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
# Setup: user message without any repo mention
slack_new_conversation_view.user_msg = 'Hello, can you help me?'
@@ -189,10 +189,10 @@ class TestRepoVerificationHandling:
assert elements[1].get('action_id').startswith('repository_select:')
@pytest.mark.asyncio
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
async def test_no_repository_button_click_processes_correctly(
self,
mock_get_redis_client_async,
mock_sio,
slack_manager,
):
"""Test that clicking 'No Repository' button correctly processes the interaction.
@@ -202,7 +202,7 @@ class TestRepoVerificationHandling:
"""
# Setup: Mock Redis to return a stored user message
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
stored_msg = json.dumps({'text': 'Hello, help me with code', 'user': 'U123'})
mock_redis.get = AsyncMock(return_value=stored_msg)
@@ -236,14 +236,14 @@ class TestRepoVerificationHandling:
assert call_args.message['message_ts'] == '1234567890.123456'
assert call_args.message['thread_ts'] is None
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
@patch('integrations.slack.slack_manager.ProviderHandler')
@patch.object(SlackManager, 'send_message', new_callable=AsyncMock)
async def test_verified_repo_starts_job(
self,
mock_send_message,
mock_provider_handler_class,
mock_get_redis_client_async,
mock_sio,
slack_manager,
slack_new_conversation_view,
):
@@ -251,7 +251,7 @@ class TestRepoVerificationHandling:
# Setup Redis mock
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
# Setup: Modify message to include exactly one repo reference
slack_new_conversation_view.user_msg = 'Help me with OpenHands/OpenHands repo'
@@ -532,18 +532,13 @@ class TestUserMsgStorage:
],
ids=['with_thread', 'without_thread', 'different_timestamps'],
)
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
async def test_store_user_msg_for_form(
self,
mock_get_redis_client_async,
slack_manager,
message_ts,
thread_ts,
user_msg,
self, mock_sio, slack_manager, message_ts, thread_ts, user_msg
):
"""Test storing user message in Redis with various timestamp combinations."""
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
# Should not raise an exception on success
await slack_manager._store_user_msg_for_form(message_ts, thread_ts, user_msg)
@@ -562,16 +557,16 @@ class TestUserMsgStorage:
],
ids=['connection_error', 'timeout_error', 'generic_exception'],
)
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
async def test_store_user_msg_for_form_redis_failure(
self, mock_get_redis_client_async, slack_manager, exception_type, exception_msg
self, mock_sio, slack_manager, exception_type, exception_msg
):
"""Test that Redis failures during store raise SlackError."""
from integrations.slack.slack_errors import SlackError, SlackErrorCode
mock_redis = AsyncMock()
mock_redis.set.side_effect = exception_type(exception_msg)
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
message_ts = '1234567890.123456'
thread_ts = '1234567890.111111'
@@ -596,18 +591,14 @@ class TestUserMsgStorage:
],
ids=['bytes_response', 'string_response'],
)
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
async def test_retrieve_user_msg_for_form(
self,
mock_get_redis_client_async,
slack_manager,
redis_return_value,
expected_result,
self, mock_sio, slack_manager, redis_return_value, expected_result
):
"""Test retrieving user message from Redis with various response types."""
mock_redis = AsyncMock()
mock_redis.get.return_value = redis_return_value
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
message_ts = '1234567890.123456'
thread_ts = '1234567890.111111'
@@ -618,16 +609,16 @@ class TestUserMsgStorage:
mock_redis.get.assert_called_once_with(expected_key)
assert result == expected_result
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
async def test_retrieve_user_msg_for_form_key_not_found(
self, mock_get_redis_client_async, slack_manager
self, mock_sio, slack_manager
):
"""Test that missing key raises SlackError with SESSION_EXPIRED."""
from integrations.slack.slack_errors import SlackError, SlackErrorCode
mock_redis = AsyncMock()
mock_redis.get.return_value = None
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
message_ts = '1234567890.123456'
thread_ts = '1234567890.111111'
@@ -646,16 +637,16 @@ class TestUserMsgStorage:
],
ids=['connection_error', 'timeout_error'],
)
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
async def test_retrieve_user_msg_for_form_redis_failure(
self, mock_get_redis_client_async, slack_manager, exception_type, exception_msg
self, mock_sio, slack_manager, exception_type, exception_msg
):
"""Test that Redis failures during retrieve raise SlackError."""
from integrations.slack.slack_errors import SlackError, SlackErrorCode
mock_redis = AsyncMock()
mock_redis.get.side_effect = exception_type(exception_msg)
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
message_ts = '1234567890.123456'
thread_ts = '1234567890.111111'
@@ -670,18 +661,18 @@ class TestUserMsgStorage:
class TestIsJobRequestedWithUserMsgStorage:
"""Test that is_job_requested properly stores user message for form flow."""
@patch('integrations.slack.slack_manager.get_redis_client_async')
@patch('integrations.slack.slack_manager.sio')
@patch.object(SlackManager, 'send_message', new_callable=AsyncMock)
async def test_stores_user_msg_when_showing_repo_selector(
self,
mock_send_message,
mock_get_redis_client_async,
mock_sio,
slack_manager,
slack_new_conversation_view,
):
"""Test that user_msg is stored in Redis when repo selector is shown."""
mock_redis = AsyncMock()
mock_get_redis_client_async.return_value = mock_redis
mock_sio.manager.redis = mock_redis
# Setup: user message without any repo mention (no repo inferred)
slack_new_conversation_view.user_msg = 'Hello, can you help me?'

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.app_server.integrations.service_types import ProviderType
from openhands.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.app_server.integrations.provider.ProviderHandler.get_service'
'openhands.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

@@ -1020,14 +1020,9 @@ def test_create_user_settings_from_entities_with_org_fallback():
@pytest.mark.asyncio
async def test_acquire_user_creation_lock_redis_error():
"""Test that _acquire_user_creation_lock returns True when Redis has an error."""
from redis import exceptions as redis_exceptions
mock_redis = AsyncMock()
mock_redis.set.side_effect = redis_exceptions.RedisError('Connection refused')
with patch.object(UserStore, '_get_redis_client', return_value=mock_redis):
async def test_acquire_user_creation_lock_no_redis():
"""Test that _acquire_user_creation_lock returns True when Redis is unavailable."""
with patch.object(UserStore, '_get_redis_client', return_value=None):
result = await UserStore._acquire_user_creation_lock('test-user-id')
assert result is True
@@ -1059,14 +1054,9 @@ async def test_acquire_user_creation_lock_not_acquired():
@pytest.mark.asyncio
async def test_release_user_creation_lock_redis_error():
"""Test that _release_user_creation_lock returns True when Redis has an error."""
from redis import exceptions as redis_exceptions
mock_redis = AsyncMock()
mock_redis.delete.side_effect = redis_exceptions.RedisError('Connection refused')
with patch.object(UserStore, '_get_redis_client', return_value=mock_redis):
async def test_release_user_creation_lock_no_redis():
"""Test that _release_user_creation_lock returns True when Redis is unavailable."""
with patch.object(UserStore, '_get_redis_client', return_value=None):
result = await UserStore._release_user_creation_lock('test-user-id')
assert result is True

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,8 +70,6 @@ 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,
)
@@ -94,6 +92,8 @@ 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

@@ -1,73 +0,0 @@
"""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,7 +12,6 @@ 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
@@ -61,7 +60,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_DIR / conversation_id.hex
path = path / 'v1_conversations' / conversation_id.hex
return path
async def get_event(self, conversation_id: UUID, event_id: UUID) -> Event | None:

View File

@@ -34,7 +34,6 @@ 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
@@ -44,13 +43,14 @@ from openhands.app_server.user.specifiy_user_context import (
USER_CONTEXT_ATTR,
SpecifyUserContext,
)
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.integrations.provider import ProviderType
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

@@ -1,77 +0,0 @@
# 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

@@ -1,21 +0,0 @@
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

@@ -1,30 +0,0 @@
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

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

View File

@@ -18,13 +18,6 @@ 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 (
@@ -32,9 +25,16 @@ 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.app_server.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.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

@@ -12,31 +12,29 @@ from openhands.app_server.config import (
get_app_conversation_info_service,
get_global_config,
)
from openhands.app_server.integrations.azure_devops.azure_devops_service import (
AzureDevOpsServiceImpl,
)
from openhands.app_server.integrations.bitbucket.bitbucket_service import (
BitBucketServiceImpl,
)
from openhands.app_server.integrations.bitbucket_data_center.bitbucket_dc_service import (
BitbucketDCServiceImpl,
)
from openhands.app_server.integrations.github.github_service import GithubServiceImpl
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.app_server.integrations.provider import ProviderToken
from openhands.app_server.integrations.service_types import GitService, ProviderType
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import (
USER_CONTEXT_ATTR,
SpecifyUserContext,
)
from openhands.app_server.user_auth import (
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.azure_devops.azure_devops_service import (
AzureDevOpsServiceImpl,
)
from openhands.integrations.bitbucket.bitbucket_service import BitBucketServiceImpl
from openhands.integrations.bitbucket_data_center.bitbucket_dc_service import (
BitbucketDCServiceImpl,
)
from openhands.integrations.github.github_service import GithubServiceImpl
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.integrations.provider import ProviderToken
from openhands.integrations.service_types import GitService, ProviderType
from openhands.server.types import AppMode
from openhands.server.user_auth import (
get_access_token,
get_provider_tokens,
get_user_id,
)
from openhands.core.logger import openhands_logger as logger
from openhands.server.types import AppMode
mcp_server = FastMCP('mcp', mask_error_details=True)

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