diff --git a/openhands/server/routes/manage_conversations.py b/openhands/server/routes/manage_conversations.py index 71c86d7eab..cf9a996d98 100644 --- a/openhands/server/routes/manage_conversations.py +++ b/openhands/server/routes/manage_conversations.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from fastapi import APIRouter, Body, Depends, status from fastapi.responses import JSONResponse -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel from openhands.core.config.llm_config import LLMConfig from openhands.core.logger import openhands_logger as logger @@ -61,10 +61,9 @@ class InitSessionRequest(BaseModel): image_urls: list[str] | None = None replay_json: str | None = None suggested_task: SuggestedTask | None = None - - model_config = { - "extra": "forbid" - } + + model_config = {'extra': 'forbid'} + async def _create_new_conversation( user_id: str | None, @@ -246,7 +245,7 @@ async def new_conversation( }, status_code=status.HTTP_400_BAD_REQUEST, ) - + @app.get('/conversations') async def search_conversations( diff --git a/openhands/server/routes/secrets.py b/openhands/server/routes/secrets.py index 093b993417..e4fb07f746 100644 --- a/openhands/server/routes/secrets.py +++ b/openhands/server/routes/secrets.py @@ -1,32 +1,33 @@ from fastapi import APIRouter, Depends, status from fastapi.responses import JSONResponse -from pydantic import SecretStr -from openhands.integrations.service_types import ProviderType +from openhands.core.logger import openhands_logger as logger from openhands.integrations.utils import validate_provider_token -from openhands.server.settings import GETCustomSecrets, POSTCustomSecrets, POSTProviderModel -from openhands.server.user_auth import get_secrets_store, get_user_secrets, get_user_settings_store +from openhands.server.settings import ( + GETCustomSecrets, + POSTCustomSecrets, + POSTProviderModel, +) +from openhands.server.user_auth import ( + get_secrets_store, + get_user_secrets, +) from openhands.storage.data_models.settings import Settings from openhands.storage.data_models.user_secrets import UserSecrets from openhands.storage.settings.secret_store import SecretsStore from openhands.storage.settings.settings_store import SettingsStore -from openhands.core.logger import openhands_logger as logger app = APIRouter(prefix='/api') - - # ================================================= # SECTION: Handle git provider tokens # ================================================= async def invalidate_legacy_secrets_store( - settings: Settings, - settings_store: SettingsStore, - secrets_store: SecretsStore) -> UserSecrets | None: - + settings: Settings, settings_store: SettingsStore, secrets_store: SecretsStore +) -> UserSecrets | None: """ We are moving `secrets_store` (a field from `Settings` object) to its own dedicated store This function moves the values from Settings to UserSecrets, and deletes the values in Settings @@ -34,7 +35,9 @@ async def invalidate_legacy_secrets_store( """ if len(settings.secrets_store.provider_tokens.items()) > 0: - user_secrets = UserSecrets(provider_tokens=settings.secrets_store.provider_tokens) + user_secrets = UserSecrets( + provider_tokens=settings.secrets_store.provider_tokens + ) await secrets_store.store(user_secrets) # Invalidate old tokens via settings store serializer @@ -44,9 +47,8 @@ async def invalidate_legacy_secrets_store( await settings_store.store(invalidated_secrets_settings) return user_secrets - - return None + return None async def check_provider_tokens(provider_info: POSTProviderModel) -> str: @@ -55,9 +57,7 @@ async def check_provider_tokens(provider_info: POSTProviderModel) -> str: # Determine whether tokens are valid for token_type, token_value in provider_info.provider_tokens.items(): if token_value.token: - confirmed_token_type = await validate_provider_token( - token_value.token - ) + confirmed_token_type = await validate_provider_token(token_value.token) if not confirmed_token_type or confirmed_token_type != token_type: return f'Invalid token. Please make sure it is a valid {token_type.value} token.' @@ -66,8 +66,8 @@ async def check_provider_tokens(provider_info: POSTProviderModel) -> str: @app.post('/add-git-providers') async def store_provider_tokens( - provider_info: POSTProviderModel, - secrets_store: SecretsStore = Depends(get_secrets_store) + provider_info: POSTProviderModel, + secrets_store: SecretsStore = Depends(get_secrets_store), ) -> JSONResponse: provider_err_msg = await check_provider_tokens(provider_info) if provider_err_msg: @@ -75,32 +75,31 @@ async def store_provider_tokens( status_code=status.HTTP_401_UNAUTHORIZED, content={'error': provider_err_msg}, ) - + try: user_secrets = await secrets_store.load() - if user_secrets: if provider_info.provider_tokens: existing_providers = [ - provider - for provider in user_secrets.provider_tokens + provider for provider in user_secrets.provider_tokens ] # Merge incoming settings store with the existing one - for provider, token_value in list(provider_info.provider_tokens.items()): + for provider, token_value in list( + provider_info.provider_tokens.items() + ): if provider in existing_providers and not token_value.token: - existing_token = ( - user_secrets.provider_tokens.get(provider) - ) + existing_token = user_secrets.provider_tokens.get(provider) if existing_token and existing_token.token: provider_info.provider_tokens[provider] = existing_token else: # nothing passed in means keep current settings provider_info.provider_tokens = dict(user_secrets.provider_tokens) - - updated_secrets = user_secrets.model_copy(update={"provider_tokens":provider_info.provider_tokens}) + updated_secrets = user_secrets.model_copy( + update={'provider_tokens': provider_info.provider_tokens} + ) await secrets_store.store(updated_secrets) return JSONResponse( @@ -117,14 +116,12 @@ async def store_provider_tokens( @app.post('/unset-provider-tokens', response_model=dict[str, str]) async def unset_provider_tokens( - secrets_store: SecretsStore = Depends(get_secrets_store) + secrets_store: SecretsStore = Depends(get_secrets_store), ) -> JSONResponse: try: user_secrets = await secrets_store.load() if user_secrets: - updated_secrets = user_secrets.model_copy( - update={'provider_tokens': {}} - ) + updated_secrets = user_secrets.model_copy(update={'provider_tokens': {}}) await secrets_store.store(updated_secrets) return JSONResponse( @@ -140,14 +137,11 @@ async def unset_provider_tokens( ) - - # ================================================= # SECTION: Handle custom secrets # ================================================= - @app.get('/secrets', response_model=GETCustomSecrets) async def load_custom_secrets_names( user_secrets: UserSecrets | None = Depends(get_user_secrets), @@ -158,10 +152,10 @@ async def load_custom_secrets_names( status_code=status.HTTP_404_NOT_FOUND, content={'error': 'User secrets not found'}, ) - + custom_secrets = list(user_secrets.custom_secrets.keys()) return GETCustomSecrets(custom_secrets=custom_secrets) - + except Exception as e: logger.warning(f'Invalid token: {e}') return JSONResponse( @@ -186,9 +180,9 @@ async def create_custom_secret( status_code=status.HTTP_400_BAD_REQUEST, content={'message': f'Secret {secret_name} already exists'}, ) - + custom_secrets[secret_name] = secret_value - + # Create a new UserSecrets that preserves provider tokens updated_user_secrets = UserSecrets( custom_secrets=custom_secrets, @@ -208,10 +202,11 @@ async def create_custom_secret( content={'error': 'Something went wrong creating secret'}, ) + @app.put('/secrets/{secret_id}', response_model=dict[str, str]) async def update_custom_secret( - secret_id: str, - incoming_secret: POSTCustomSecrets, + secret_id: str, + incoming_secret: POSTCustomSecrets, secrets_store: SecretsStore = Depends(get_secrets_store), ) -> JSONResponse: try: @@ -289,4 +284,3 @@ async def delete_custom_secret( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={'error': 'Something went wrong deleting secret'}, ) - diff --git a/openhands/server/routes/settings.py b/openhands/server/routes/settings.py index 21855acfe1..f1ce9ded84 100644 --- a/openhands/server/routes/settings.py +++ b/openhands/server/routes/settings.py @@ -6,8 +6,6 @@ from openhands.integrations.provider import ( PROVIDER_TOKEN_TYPE, ProviderType, ) - - from openhands.server.routes.secrets import invalidate_legacy_secrets_store from openhands.server.settings import ( GETSettingsModel, @@ -18,8 +16,8 @@ from openhands.server.user_auth import ( get_secrets_store, get_user_settings_store, ) -from openhands.storage.settings.secret_store import SecretsStore from openhands.storage.data_models.settings import Settings +from openhands.storage.settings.secret_store import SecretsStore from openhands.storage.settings.settings_store import SettingsStore app = APIRouter(prefix='/api') @@ -29,24 +27,27 @@ app = APIRouter(prefix='/api') async def load_settings( provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens), settings_store: SettingsStore = Depends(get_user_settings_store), - secrets_store: SecretsStore = Depends(get_secrets_store) + secrets_store: SecretsStore = Depends(get_secrets_store), ) -> GETSettingsModel | JSONResponse: - settings = await settings_store.load() - + try: if not settings: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={'error': 'Settings not found'}, ) - - # On initial load, user secrets may not be populated with values migrated from settings store - user_secrets = await invalidate_legacy_secrets_store(settings, settings_store, secrets_store) - # If invalidation is successful, then the returned user secrets holds the most recent values - git_providers = user_secrets.provider_tokens if user_secrets else provider_tokens - provider_tokens_set: dict[ProviderType, str | None] = {} + # On initial load, user secrets may not be populated with values migrated from settings store + user_secrets = await invalidate_legacy_secrets_store( + settings, settings_store, secrets_store + ) + # If invalidation is successful, then the returned user secrets holds the most recent values + git_providers = ( + user_secrets.provider_tokens if user_secrets else provider_tokens + ) + + provider_tokens_set: dict[ProviderType, str | None] = {} if git_providers: for provider_type, provider_token in git_providers.items(): if provider_token.token or provider_token.user_id: diff --git a/openhands/server/user_auth/__init__.py b/openhands/server/user_auth/__init__.py index 0ecea4adb9..2ca913bc32 100644 --- a/openhands/server/user_auth/__init__.py +++ b/openhands/server/user_auth/__init__.py @@ -4,9 +4,9 @@ from pydantic import SecretStr from openhands.integrations.provider import PROVIDER_TOKEN_TYPE from openhands.integrations.service_types import ProviderType from openhands.server.settings import Settings +from openhands.server.user_auth.user_auth import AuthType, get_user_auth from openhands.storage.data_models.user_secrets import UserSecrets from openhands.storage.settings.secret_store import SecretsStore -from openhands.server.user_auth.user_auth import AuthType, get_user_auth from openhands.storage.settings.settings_store import SettingsStore diff --git a/openhands/server/user_auth/default_user_auth.py b/openhands/server/user_auth/default_user_auth.py index 00be69a287..600b953aac 100644 --- a/openhands/server/user_auth/default_user_auth.py +++ b/openhands/server/user_auth/default_user_auth.py @@ -69,7 +69,6 @@ class DefaultUserAuth(UserAuth): self._user_secrets = user_secrets return user_secrets - async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None: secrets_store = await self.get_user_secrets() provider_tokens = getattr(secrets_store, 'provider_tokens', None) diff --git a/openhands/server/user_auth/user_auth.py b/openhands/server/user_auth/user_auth.py index 09feb16fc5..a0ff8f7230 100644 --- a/openhands/server/user_auth/user_auth.py +++ b/openhands/server/user_auth/user_auth.py @@ -60,7 +60,7 @@ class UserAuth(ABC): @abstractmethod async def get_user_secrets(self) -> UserSecrets | None: """Get the user's secrets""" - + def get_auth_type(self) -> AuthType | None: return None diff --git a/openhands/storage/data_models/settings.py b/openhands/storage/data_models/settings.py index 711e6bcd54..671d6e4d2d 100644 --- a/openhands/storage/data_models/settings.py +++ b/openhands/storage/data_models/settings.py @@ -94,9 +94,7 @@ class Settings(BaseModel): """Custom serializer for secrets store.""" """Force invalidate secret store""" - return { - 'provider_tokens': {} - } + return {'provider_tokens': {}} @staticmethod def from_config() -> Settings | None: diff --git a/openhands/storage/data_models/user_secrets.py b/openhands/storage/data_models/user_secrets.py index 964076db95..ea6e7c01e7 100644 --- a/openhands/storage/data_models/user_secrets.py +++ b/openhands/storage/data_models/user_secrets.py @@ -1,5 +1,6 @@ from types import MappingProxyType from typing import Any + from pydantic import ( BaseModel, ConfigDict, @@ -10,7 +11,13 @@ from pydantic import ( model_validator, ) from pydantic.json import pydantic_encoder -from openhands.integrations.provider import CUSTOM_SECRETS_TYPE, PROVIDER_TOKEN_TYPE, PROVIDER_TOKEN_TYPE_WITH_JSON_SCHEMA, ProviderToken + +from openhands.integrations.provider import ( + CUSTOM_SECRETS_TYPE, + PROVIDER_TOKEN_TYPE, + PROVIDER_TOKEN_TYPE_WITH_JSON_SCHEMA, + ProviderToken, +) from openhands.integrations.service_types import ProviderType @@ -29,7 +36,6 @@ class UserSecrets(BaseModel): arbitrary_types_allowed=True, ) - @field_serializer('provider_tokens') def provider_tokens_serializer( self, provider_tokens: PROVIDER_TOKEN_TYPE, info: SerializationInfo diff --git a/openhands/storage/settings/file_secrets_store.py b/openhands/storage/settings/file_secrets_store.py index 8710ee3296..00b4c8e911 100644 --- a/openhands/storage/settings/file_secrets_store.py +++ b/openhands/storage/settings/file_secrets_store.py @@ -34,4 +34,4 @@ class FileSecretsStore(SecretsStore): cls, config: AppConfig, user_id: str | None ) -> FileSecretsStore: file_store = get_file_store(config.file_store, config.file_store_path) - return FileSecretsStore(file_store) \ No newline at end of file + return FileSecretsStore(file_store) diff --git a/openhands/storage/settings/secret_store.py b/openhands/storage/settings/secret_store.py index 6d6777628e..dcd53aec46 100644 --- a/openhands/storage/settings/secret_store.py +++ b/openhands/storage/settings/secret_store.py @@ -1,12 +1,11 @@ - from __future__ import annotations from abc import ABC, abstractmethod + from openhands.core.config.app_config import AppConfig from openhands.storage.data_models.user_secrets import UserSecrets - class SecretsStore(ABC): """Storage for secrets. May or may not support multiple users depending on the environment.""" @@ -20,7 +19,5 @@ class SecretsStore(ABC): @classmethod @abstractmethod - async def get_instance( - cls, config: AppConfig, user_id: str | None - ) -> SecretsStore: - """Get a store for the user represented by the token given.""" \ No newline at end of file + async def get_instance(cls, config: AppConfig, user_id: str | None) -> SecretsStore: + """Get a store for the user represented by the token given."""