mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
@@ -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(
|
||||
|
||||
@@ -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'},
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
return FileSecretsStore(file_store)
|
||||
|
||||
@@ -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."""
|
||||
async def get_instance(cls, config: AppConfig, user_id: str | None) -> SecretsStore:
|
||||
"""Get a store for the user represented by the token given."""
|
||||
|
||||
Reference in New Issue
Block a user