Refactor: rename user secrets table to custom secrets (#11525)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra 2025-10-27 12:58:07 -04:00 committed by GitHub
parent 26c636d63e
commit eb616dfae4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 180 additions and 143 deletions

View File

@ -31,7 +31,7 @@ from server.utils.conversation_callback_utils import register_callback_processor
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.utils.async_utils import call_sync_from_async
@ -250,7 +250,7 @@ class GithubManager(Manager):
f'[GitHub] Creating new conversation for user {user_info.username}'
)
secret_store = UserSecrets(
secret_store = Secrets(
provider_tokens=MappingProxyType(
{
ProviderType.GITHUB: ProviderToken(

View File

@ -25,7 +25,7 @@ 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
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
class GitlabManager(Manager):
@ -198,7 +198,7 @@ class GitlabManager(Manager):
f'[GitLab] Creating new conversation for user {user_info.username}'
)
secret_store = UserSecrets(
secret_store = Secrets(
provider_tokens=MappingProxyType(
{
ProviderType.GITLAB: ProviderToken(

View File

@ -57,7 +57,7 @@ class JiraNewConversationView(JiraViewInterface):
raise StartingConvoException('No repository selected for this conversation')
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_user_secrets()
user_secrets = await self.saas_user_auth.get_secrets()
instructions, user_msg = self._get_instructions(jinja_env)
try:

View File

@ -60,7 +60,7 @@ class JiraDcNewConversationView(JiraDcViewInterface):
raise StartingConvoException('No repository selected for this conversation')
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_user_secrets()
user_secrets = await self.saas_user_auth.get_secrets()
instructions, user_msg = self._get_instructions(jinja_env)
try:

View File

@ -57,7 +57,7 @@ class LinearNewConversationView(LinearViewInterface):
raise StartingConvoException('No repository selected for this conversation')
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_user_secrets()
user_secrets = await self.saas_user_auth.get_secrets()
instructions, user_msg = self._get_instructions(jinja_env)
try:

View File

@ -186,7 +186,7 @@ class SlackNewConversationView(SlackViewInterface):
self._verify_necessary_values_are_set()
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_user_secrets()
user_secrets = await self.saas_user_auth.get_secrets()
user_instructions, conversation_instructions = self._get_instructions(jinja)
# Determine git provider from repository

View File

@ -0,0 +1,39 @@
"""rename user_secrets table to custom_secrets
Revision ID: 079
Revises: 078
Create Date: 2025-10-27 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '079'
down_revision: Union[str, None] = '078'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Rename the table from user_secrets to custom_secrets
op.rename_table('user_secrets', 'custom_secrets')
# Rename the index to match the new table name
op.drop_index('idx_user_secrets_keycloak_user_id', 'custom_secrets')
op.create_index(
'idx_custom_secrets_keycloak_user_id', 'custom_secrets', ['keycloak_user_id']
)
def downgrade() -> None:
# Rename the index back to the original name
op.drop_index('idx_custom_secrets_keycloak_user_id', 'custom_secrets')
op.create_index(
'idx_user_secrets_keycloak_user_id', 'custom_secrets', ['keycloak_user_id']
)
# Rename the table back from custom_secrets to user_secrets
op.rename_table('custom_secrets', 'user_secrets')

View File

@ -31,7 +31,7 @@ from openhands.integrations.provider import (
)
from openhands.server.settings import Settings
from openhands.server.user_auth.user_auth import AuthType, UserAuth
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.settings.settings_store import SettingsStore
token_manager = TokenManager()
@ -52,7 +52,7 @@ class SaasUserAuth(UserAuth):
settings_store: SaasSettingsStore | None = None
secrets_store: SaasSecretsStore | None = None
_settings: Settings | None = None
_user_secrets: UserSecrets | None = None
_secrets: Secrets | None = None
accepted_tos: bool | None = None
auth_type: AuthType = AuthType.COOKIE
@ -119,13 +119,13 @@ class SaasUserAuth(UserAuth):
self.secrets_store = secrets_store
return secrets_store
async def get_user_secrets(self):
user_secrets = self._user_secrets
async def get_secrets(self):
user_secrets = self._secrets
if user_secrets:
return user_secrets
secrets_store = await self.get_secrets_store()
user_secrets = await secrets_store.load()
self._user_secrets = user_secrets
self._secrets = user_secrets
return user_secrets
async def get_access_token(self) -> SecretStr | None:
@ -148,7 +148,7 @@ class SaasUserAuth(UserAuth):
if not access_token:
raise AuthError()
user_secrets = await self.get_user_secrets()
user_secrets = await self.get_secrets()
try:
# TODO: I think we can do this in a single request if we refactor

View File

@ -7,11 +7,11 @@ from dataclasses import dataclass
from cryptography.fernet import Fernet
from sqlalchemy.orm import sessionmaker
from storage.database import session_maker
from storage.stored_user_secrets import StoredUserSecrets
from storage.stored_custom_secrets import StoredCustomSecrets
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.secrets.secrets_store import SecretsStore
@ -21,20 +21,20 @@ class SaasSecretsStore(SecretsStore):
session_maker: sessionmaker
config: OpenHandsConfig
async def load(self) -> UserSecrets | None:
async def load(self) -> Secrets | None:
if not self.user_id:
return None
with self.session_maker() as session:
# Fetch all secrets for the given user ID
settings = (
session.query(StoredUserSecrets)
.filter(StoredUserSecrets.keycloak_user_id == self.user_id)
session.query(StoredCustomSecrets)
.filter(StoredCustomSecrets.keycloak_user_id == self.user_id)
.all()
)
if not settings:
return UserSecrets()
return Secrets()
kwargs = {}
for secret in settings:
@ -45,14 +45,14 @@ class SaasSecretsStore(SecretsStore):
self._decrypt_kwargs(kwargs)
return UserSecrets(custom_secrets=kwargs) # type: ignore[arg-type]
return Secrets(custom_secrets=kwargs) # type: ignore[arg-type]
async def store(self, item: UserSecrets):
async def store(self, item: Secrets):
with self.session_maker() as session:
# Incoming secrets are always the most updated ones
# Delete all existing records and override with incoming ones
session.query(StoredUserSecrets).filter(
StoredUserSecrets.keycloak_user_id == self.user_id
session.query(StoredCustomSecrets).filter(
StoredCustomSecrets.keycloak_user_id == self.user_id
).delete()
# Prepare the new secrets data
@ -74,7 +74,7 @@ class SaasSecretsStore(SecretsStore):
# Add the new secrets
for secret_name, secret_value, description in secret_tuples:
new_secret = StoredUserSecrets(
new_secret = StoredCustomSecrets(
keycloak_user_id=self.user_id,
secret_name=secret_name,
secret_value=secret_value,

View File

@ -2,8 +2,8 @@ from sqlalchemy import Column, Identity, Integer, String
from storage.base import Base
class StoredUserSecrets(Base): # type: ignore
__tablename__ = 'user_secrets'
class StoredCustomSecrets(Base): # type: ignore
__tablename__ = 'custom_secrets'
id = Column(Integer, Identity(), primary_key=True)
keycloak_user_id = Column(String, nullable=True, index=True)
secret_name = Column(String, nullable=False)

View File

@ -309,7 +309,7 @@ class TestJiraViewEdgeCases:
mock_agent_loop_info,
):
"""Test conversation creation when user has no secrets"""
new_conversation_view.saas_user_auth.get_user_secrets.return_value = None
new_conversation_view.saas_user_auth.get_secrets.return_value = None
mock_create_conversation.return_value = mock_agent_loop_info
mock_store.create_conversation = AsyncMock()

View File

@ -309,7 +309,7 @@ class TestJiraDcViewEdgeCases:
mock_agent_loop_info,
):
"""Test conversation creation when user has no secrets"""
new_conversation_view.saas_user_auth.get_user_secrets.return_value = None
new_conversation_view.saas_user_auth.get_secrets.return_value = None
mock_create_conversation.return_value = mock_agent_loop_info
mock_store.create_conversation = AsyncMock()

View File

@ -309,7 +309,7 @@ class TestLinearViewEdgeCases:
mock_agent_loop_info,
):
"""Test conversation creation when user has no secrets"""
new_conversation_view.saas_user_auth.get_user_secrets.return_value = None
new_conversation_view.saas_user_auth.get_secrets.return_value = None
mock_create_conversation.return_value = mock_agent_loop_info
mock_store.create_conversation = AsyncMock()

View File

@ -5,11 +5,11 @@ from unittest.mock import MagicMock
import pytest
from pydantic import SecretStr
from storage.saas_secrets_store import SaasSecretsStore
from storage.stored_user_secrets import StoredUserSecrets
from storage.stored_custom_secrets import StoredCustomSecrets
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.integrations.provider import CustomSecret
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
@pytest.fixture
@ -27,8 +27,8 @@ def secrets_store(session_maker, mock_config):
class TestSaasSecretsStore:
@pytest.mark.asyncio
async def test_store_and_load(self, secrets_store):
# Create a UserSecrets object with some test data
user_secrets = UserSecrets(
# Create a Secrets object with some test data
user_secrets = Secrets(
custom_secrets=MappingProxyType(
{
'api_token': CustomSecret.from_value(
@ -60,8 +60,8 @@ class TestSaasSecretsStore:
@pytest.mark.asyncio
async def test_encryption_decryption(self, secrets_store):
# Create a UserSecrets object with sensitive data
user_secrets = UserSecrets(
# Create a Secrets object with sensitive data
user_secrets = Secrets(
custom_secrets=MappingProxyType(
{
'api_token': CustomSecret.from_value(
@ -87,8 +87,8 @@ class TestSaasSecretsStore:
# Verify the data is encrypted in the database
with secrets_store.session_maker() as session:
stored = (
session.query(StoredUserSecrets)
.filter(StoredUserSecrets.keycloak_user_id == 'user-id')
session.query(StoredCustomSecrets)
.filter(StoredCustomSecrets.keycloak_user_id == 'user-id')
.first()
)
@ -154,7 +154,7 @@ class TestSaasSecretsStore:
@pytest.mark.asyncio
async def test_update_existing_secrets(self, secrets_store):
# Create and store initial secrets
initial_secrets = UserSecrets(
initial_secrets = Secrets(
custom_secrets=MappingProxyType(
{
'api_token': CustomSecret.from_value(
@ -169,7 +169,7 @@ class TestSaasSecretsStore:
await secrets_store.store(initial_secrets)
# Create and store updated secrets
updated_secrets = UserSecrets(
updated_secrets = Secrets(
custom_secrets=MappingProxyType(
{
'api_token': CustomSecret.from_value(

View File

@ -71,7 +71,7 @@ class AuthUserContext(UserContext):
results = {}
# Include custom secrets...
secrets = await self.user_auth.get_user_secrets()
secrets = await self.user_auth.get_secrets()
if secrets:
for name, custom_secret in secrets.custom_secrets.items():
results[name] = StaticSecret(value=custom_secret.secret)

View File

@ -28,7 +28,7 @@ from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
from openhands.server.services.conversation_stats import ConversationStats
from openhands.storage import get_file_store
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.utils.async_utils import GENERAL_TIMEOUT, call_async_from_sync
@ -109,9 +109,9 @@ def get_provider_tokens():
bitbucket_token = SecretStr(os.environ['BITBUCKET_TOKEN'])
provider_tokens[ProviderType.BITBUCKET] = ProviderToken(token=bitbucket_token)
# Wrap provider tokens in UserSecrets if any tokens were found
# Wrap provider tokens in Secrets if any tokens were found
secret_store = (
UserSecrets(provider_tokens=provider_tokens) if provider_tokens else None # type: ignore[arg-type]
Secrets(provider_tokens=provider_tokens) if provider_tokens else None # type: ignore[arg-type]
)
return secret_store.provider_tokens if secret_store else None

View File

@ -71,8 +71,8 @@ from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.server.user_auth import (
get_auth_type,
get_provider_tokens,
get_secrets,
get_user_id,
get_user_secrets,
get_user_settings,
get_user_settings_store,
)
@ -85,8 +85,8 @@ from openhands.storage.data_models.conversation_metadata import (
ConversationTrigger,
)
from openhands.storage.data_models.conversation_status import ConversationStatus
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.data_models.settings import Settings
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.locations import get_experiment_config_filename
from openhands.storage.settings.settings_store import SettingsStore
from openhands.utils.async_utils import wait_all
@ -210,7 +210,7 @@ async def new_conversation(
data: InitSessionRequest,
user_id: str = Depends(get_user_id),
provider_tokens: PROVIDER_TOKEN_TYPE = Depends(get_provider_tokens),
user_secrets: UserSecrets = Depends(get_user_secrets),
user_secrets: Secrets = Depends(get_secrets),
auth_type: AuthType | None = Depends(get_auth_type),
) -> ConversationResponse:
"""Initialize a new session or join an existing one.

View File

@ -14,11 +14,11 @@ from openhands.server.settings import (
)
from openhands.server.user_auth import (
get_provider_tokens,
get_secrets,
get_secrets_store,
get_user_secrets,
)
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.data_models.settings import Settings
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.secrets.secrets_store import SecretsStore
from openhands.storage.settings.settings_store import SettingsStore
@ -32,20 +32,18 @@ app = APIRouter(prefix='/api', dependencies=get_dependencies())
async def invalidate_legacy_secrets_store(
settings: Settings, settings_store: SettingsStore, secrets_store: SecretsStore
) -> UserSecrets | None:
) -> Secrets | 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
This function moves the values from Settings to Secrets, and deletes the values in Settings
While this function in called multiple times, the migration only ever happens once
"""
if len(settings.secrets_store.provider_tokens.items()) > 0:
user_secrets = UserSecrets(
provider_tokens=settings.secrets_store.provider_tokens
)
user_secrets = Secrets(provider_tokens=settings.secrets_store.provider_tokens)
await secrets_store.store(user_secrets)
# Invalidate old tokens via settings store serializer
invalidated_secrets_settings = settings.model_copy(
update={'secrets_store': UserSecrets()}
update={'secrets_store': Secrets()}
)
await settings_store.store(invalidated_secrets_settings)
@ -120,7 +118,7 @@ async def store_provider_tokens(
try:
user_secrets = await secrets_store.load()
if not user_secrets:
user_secrets = UserSecrets()
user_secrets = Secrets()
if provider_info.provider_tokens:
existing_providers = [provider for provider in user_secrets.provider_tokens]
@ -183,7 +181,7 @@ async def unset_provider_tokens(
@app.get('/secrets', response_model=GETCustomSecrets)
async def load_custom_secrets_names(
user_secrets: UserSecrets | None = Depends(get_user_secrets),
user_secrets: Secrets | None = Depends(get_secrets),
) -> GETCustomSecrets | JSONResponse:
try:
if not user_secrets:
@ -235,8 +233,8 @@ async def create_custom_secret(
description=secret_description or '',
)
# Create a new UserSecrets that preserves provider tokens
updated_user_secrets = UserSecrets(
# Create a new Secrets that preserves provider tokens
updated_user_secrets = Secrets(
custom_secrets=custom_secrets, # type: ignore[arg-type]
provider_tokens=existing_secrets.provider_tokens
if existing_secrets
@ -290,7 +288,7 @@ async def update_custom_secret(
description=secret_description or '',
)
updated_secrets = UserSecrets(
updated_secrets = Secrets(
custom_secrets=custom_secrets, # type: ignore[arg-type]
provider_tokens=existing_secrets.provider_tokens,
)
@ -330,8 +328,8 @@ async def delete_custom_secret(
# Remove the secret
custom_secrets.pop(secret_id)
# Create a new UserSecrets that preserves provider tokens and remaining secrets
updated_secrets = UserSecrets(
# Create a new Secrets that preserves provider tokens and remaining secrets
updated_secrets = Secrets(
custom_secrets=custom_secrets, # type: ignore[arg-type]
provider_tokens=existing_secrets.provider_tokens,
)

View File

@ -27,7 +27,7 @@ from openhands.storage.data_models.conversation_metadata import (
ConversationMetadata,
ConversationTrigger,
)
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.utils.conversation_summary import get_default_conversation_title
@ -232,7 +232,7 @@ async def setup_init_conversation_settings(
settings = await settings_store.load()
secrets_store = await SecretsStoreImpl.get_instance(config, user_id)
user_secrets: UserSecrets | None = await secrets_store.load()
user_secrets: Secrets | None = await secrets_store.load()
if not settings:
from socketio.exceptions import ConnectionRefusedError

View File

@ -30,7 +30,7 @@ from openhands.runtime.base import Runtime
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.server.services.conversation_stats import ConversationStats
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.files import FileStore
from openhands.utils.async_utils import EXECUTOR, call_sync_from_async
from openhands.utils.shutdown_listener import should_continue
@ -128,7 +128,7 @@ class AgentSession:
finished = False # For monitoring
runtime_connected = False
restored_state = False
custom_secrets_handler = UserSecrets(
custom_secrets_handler = Secrets(
custom_secrets=custom_secrets if custom_secrets else {} # type: ignore[arg-type]
)
try:
@ -316,7 +316,7 @@ class AgentSession:
if self.runtime is not None:
raise RuntimeError('Runtime already created')
custom_secrets_handler = UserSecrets(custom_secrets=custom_secrets or {}) # type: ignore[arg-type]
custom_secrets_handler = Secrets(custom_secrets=custom_secrets or {}) # type: ignore[arg-type]
env_vars = custom_secrets_handler.get_env_vars()
self.logger.debug(f'Initializing runtime `{runtime_name}` now...')

View File

@ -4,7 +4,7 @@ from pydantic import SecretStr
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
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.data_models.secrets import Secrets
from openhands.storage.secrets.secrets_store import SecretsStore
from openhands.storage.settings.settings_store import SettingsStore
@ -39,9 +39,9 @@ async def get_secrets_store(request: Request) -> SecretsStore:
return secrets_store
async def get_user_secrets(request: Request) -> UserSecrets | None:
async def get_secrets(request: Request) -> Secrets | None:
user_auth = await get_user_auth(request)
user_secrets = await user_auth.get_user_secrets()
user_secrets = await user_auth.get_secrets()
return user_secrets

View File

@ -7,7 +7,7 @@ from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.server import shared
from openhands.server.settings import Settings
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.secrets.secrets_store import SecretsStore
from openhands.storage.settings.settings_store import SettingsStore
@ -19,7 +19,7 @@ class DefaultUserAuth(UserAuth):
_settings: Settings | None = None
_settings_store: SettingsStore | None = None
_secrets_store: SecretsStore | None = None
_user_secrets: UserSecrets | None = None
_secrets: Secrets | None = None
async def get_user_id(self) -> str | None:
"""The default implementation does not support multi tenancy, so user_id is always None"""
@ -73,17 +73,17 @@ class DefaultUserAuth(UserAuth):
self._secrets_store = secret_store
return secret_store
async def get_user_secrets(self) -> UserSecrets | None:
user_secrets = self._user_secrets
async def get_secrets(self) -> Secrets | None:
user_secrets = self._secrets
if user_secrets:
return user_secrets
secrets_store = await self.get_secrets_store()
user_secrets = await secrets_store.load()
self._user_secrets = user_secrets
self._secrets = user_secrets
return user_secrets
async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None:
user_secrets = await self.get_user_secrets()
user_secrets = await self.get_secrets()
if user_secrets is None:
return None
return user_secrets.provider_tokens

View File

@ -9,7 +9,7 @@ from pydantic import SecretStr
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.server.settings import Settings
from openhands.server.shared import server_config
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.secrets.secrets_store import SecretsStore
from openhands.storage.settings.settings_store import SettingsStore
from openhands.utils.import_utils import get_impl
@ -69,7 +69,7 @@ class UserAuth(ABC):
"""Get secrets store"""
@abstractmethod
async def get_user_secrets(self) -> UserSecrets | None:
async def get_secrets(self) -> Secrets | None:
"""Get the user's secrets"""
def get_auth_type(self) -> AuthType | None:

View File

@ -23,7 +23,7 @@ from openhands.integrations.provider import (
from openhands.integrations.service_types import ProviderType
class UserSecrets(BaseModel):
class Secrets(BaseModel):
provider_tokens: PROVIDER_TOKEN_TYPE_WITH_JSON_SCHEMA = Field(
default_factory=lambda: MappingProxyType({})
)
@ -96,7 +96,7 @@ class UserSecrets(BaseModel):
) -> dict[str, MappingProxyType | None]:
"""Custom deserializer to convert dictionary into MappingProxyType"""
if not isinstance(data, dict):
raise ValueError('UserSecrets must be initialized with a dictionary')
raise ValueError('Secrets must be initialized with a dictionary')
new_data: dict[str, MappingProxyType | None] = {}

View File

@ -14,7 +14,7 @@ from pydantic import (
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.config.utils import load_openhands_config
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
class Settings(BaseModel):
@ -30,7 +30,7 @@ class Settings(BaseModel):
llm_base_url: str | None = None
remote_runtime_resource_factor: int | None = None
# Planned to be removed from settings
secrets_store: UserSecrets = Field(default_factory=UserSecrets, frozen=True)
secrets_store: Secrets = Field(default_factory=Secrets, frozen=True)
enable_default_condenser: bool = True
enable_sound_notifications: bool = False
enable_proactive_conversation_starters: bool = True
@ -76,7 +76,7 @@ class Settings(BaseModel):
@model_validator(mode='before')
@classmethod
def convert_provider_tokens(cls, data: dict | object) -> dict | object:
"""Convert provider tokens from JSON format to UserSecrets format."""
"""Convert provider tokens from JSON format to Secrets format."""
if not isinstance(data, dict):
return data
@ -87,10 +87,10 @@ class Settings(BaseModel):
custom_secrets = secrets_store.get('custom_secrets')
tokens = secrets_store.get('provider_tokens')
secret_store = UserSecrets(provider_tokens={}, custom_secrets={}) # type: ignore[arg-type]
secret_store = Secrets(provider_tokens={}, custom_secrets={}) # type: ignore[arg-type]
if isinstance(tokens, dict):
converted_store = UserSecrets(provider_tokens=tokens) # type: ignore[arg-type]
converted_store = Secrets(provider_tokens=tokens) # type: ignore[arg-type]
secret_store = secret_store.model_copy(
update={'provider_tokens': converted_store.provider_tokens}
)
@ -98,7 +98,7 @@ class Settings(BaseModel):
secret_store.model_copy(update={'provider_tokens': tokens})
if isinstance(custom_secrets, dict):
converted_store = UserSecrets(custom_secrets=custom_secrets) # type: ignore[arg-type]
converted_store = Secrets(custom_secrets=custom_secrets) # type: ignore[arg-type]
secret_store = secret_store.model_copy(
update={'custom_secrets': converted_store.custom_secrets}
)
@ -119,7 +119,7 @@ class Settings(BaseModel):
return v
@field_serializer('secrets_store')
def secrets_store_serializer(self, secrets: UserSecrets, info: SerializationInfo):
def secrets_store_serializer(self, secrets: Secrets, info: SerializationInfo):
"""Custom serializer for secrets store."""
"""Force invalidate secret store"""
return {'provider_tokens': {}}

View File

@ -5,7 +5,7 @@ from dataclasses import dataclass
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.storage import get_file_store
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.files import FileStore
from openhands.storage.secrets.secrets_store import SecretsStore
from openhands.utils.async_utils import call_sync_from_async
@ -16,7 +16,7 @@ class FileSecretsStore(SecretsStore):
file_store: FileStore
path: str = 'secrets.json'
async def load(self) -> UserSecrets | None:
async def load(self) -> Secrets | None:
try:
json_str = await call_sync_from_async(self.file_store.read, self.path)
kwargs = json.loads(json_str)
@ -26,12 +26,12 @@ class FileSecretsStore(SecretsStore):
if v.get('token')
}
kwargs['provider_tokens'] = provider_tokens
secrets = UserSecrets(**kwargs)
secrets = Secrets(**kwargs)
return secrets
except FileNotFoundError:
return None
async def store(self, secrets: UserSecrets) -> None:
async def store(self, secrets: Secrets) -> None:
json_str = secrets.model_dump_json(context={'expose_secrets': True})
await call_sync_from_async(self.file_store.write, self.path, json_str)

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
class SecretsStore(ABC):
@ -21,11 +21,11 @@ class SecretsStore(ABC):
"""
@abstractmethod
async def load(self) -> UserSecrets | None:
async def load(self) -> Secrets | None:
"""Load secrets."""
@abstractmethod
async def store(self, secrets: UserSecrets) -> None:
async def store(self, secrets: Secrets) -> None:
"""Store secrets."""
@classmethod

View File

@ -9,8 +9,8 @@ from openhands.integrations.provider import (
ProviderToken,
ProviderType,
)
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.data_models.settings import Settings
from openhands.storage.data_models.user_secrets import UserSecrets
def test_provider_token_immutability():
@ -34,8 +34,8 @@ def test_provider_token_immutability():
def test_secret_store_immutability():
"""Test that UserSecrets is immutable"""
store = UserSecrets(
"""Test that Secrets is immutable"""
store = Secrets(
provider_tokens={ProviderType.GITHUB: ProviderToken(token=SecretStr('test'))}
)
@ -69,7 +69,7 @@ def test_secret_store_immutability():
def test_settings_immutability():
"""Test that Settings secrets_store is immutable"""
settings = Settings(
secrets_store=UserSecrets(
secrets_store=Secrets(
provider_tokens={
ProviderType.GITHUB: ProviderToken(token=SecretStr('test'))
}
@ -78,7 +78,7 @@ def test_settings_immutability():
# Test direct modification of secrets_store
with pytest.raises(ValidationError):
settings.secrets_store = UserSecrets()
settings.secrets_store = Secrets()
# Test nested modification attempts
with pytest.raises((TypeError, AttributeError)):
@ -87,7 +87,7 @@ def test_settings_immutability():
)
# Test model_copy creates new instance
new_store = UserSecrets(
new_store = Secrets(
provider_tokens={
ProviderType.GITHUB: ProviderToken(token=SecretStr('new_token'))
}
@ -140,10 +140,10 @@ def test_provider_handler_immutability():
def test_token_conversion():
"""Test token conversion in UserSecrets.create"""
"""Test token conversion in Secrets.create"""
# Test with string token
store1 = Settings(
secrets_store=UserSecrets(
secrets_store=Secrets(
provider_tokens={
ProviderType.GITHUB: ProviderToken(token=SecretStr('test_token'))
}
@ -159,7 +159,7 @@ def test_token_conversion():
assert store1.secrets_store.provider_tokens[ProviderType.GITHUB].user_id is None
# Test with dict token
store2 = UserSecrets(
store2 = Secrets(
provider_tokens={'github': {'token': 'test_token', 'user_id': 'user1'}}
)
assert (
@ -170,14 +170,14 @@ def test_token_conversion():
# Test with ProviderToken
token = ProviderToken(token=SecretStr('test_token'), user_id='user2')
store3 = UserSecrets(provider_tokens={ProviderType.GITHUB: token})
store3 = Secrets(provider_tokens={ProviderType.GITHUB: token})
assert (
store3.provider_tokens[ProviderType.GITHUB].token.get_secret_value()
== 'test_token'
)
assert store3.provider_tokens[ProviderType.GITHUB].user_id == 'user2'
store4 = UserSecrets(
store4 = Secrets(
provider_tokens={
ProviderType.GITHUB: 123 # Invalid type
}
@ -186,10 +186,10 @@ def test_token_conversion():
assert ProviderType.GITHUB not in store4.provider_tokens
# Test with empty/None token
store5 = UserSecrets(provider_tokens={ProviderType.GITHUB: None})
store5 = Secrets(provider_tokens={ProviderType.GITHUB: None})
assert ProviderType.GITHUB not in store5.provider_tokens
store6 = UserSecrets(
store6 = Secrets(
provider_tokens={
'invalid_provider': 'test_token' # Invalid provider type
}

View File

@ -82,7 +82,7 @@ def test_client():
def create_new_test_conversation(
test_request: InitSessionRequest, auth_type: AuthType | None = None
):
# Create a mock UserSecrets object with the required custom_secrets attribute
# Create a mock Secrets object with the required custom_secrets attribute
mock_user_secrets = MagicMock()
mock_user_secrets.custom_secrets = MappingProxyType({})

View File

@ -18,7 +18,7 @@ from openhands.server.routes.secrets import (
app as secrets_app,
)
from openhands.storage import get_file_store
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.secrets.file_secrets_store import FileSecretsStore
@ -62,7 +62,7 @@ async def test_load_custom_secrets_names(test_client, file_secrets_store):
provider_tokens = {
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
}
user_secrets = UserSecrets(
user_secrets = Secrets(
custom_secrets=custom_secrets, provider_tokens=provider_tokens
)
@ -101,7 +101,7 @@ async def test_load_custom_secrets_names_empty(test_client, file_secrets_store):
provider_tokens = {
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
}
user_secrets = UserSecrets(provider_tokens=provider_tokens, custom_secrets={})
user_secrets = Secrets(provider_tokens=provider_tokens, custom_secrets={})
# Store the initial settings
await file_secrets_store.store(user_secrets)
@ -123,7 +123,7 @@ async def test_add_custom_secret(test_client, file_secrets_store):
provider_tokens = {
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
}
user_secrets = UserSecrets(provider_tokens=provider_tokens)
user_secrets = Secrets(provider_tokens=provider_tokens)
# Store the initial settings
await file_secrets_store.store(user_secrets)
@ -184,7 +184,7 @@ async def test_update_existing_custom_secret(test_client, file_secrets_store):
provider_tokens = {
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
}
user_secrets = UserSecrets(
user_secrets = Secrets(
custom_secrets=custom_secrets, provider_tokens=provider_tokens
)
@ -223,7 +223,7 @@ async def test_add_multiple_custom_secrets(test_client, file_secrets_store):
provider_tokens = {
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
}
user_secrets = UserSecrets(
user_secrets = Secrets(
custom_secrets=custom_secrets, provider_tokens=provider_tokens
)
@ -285,7 +285,7 @@ async def test_delete_custom_secret(test_client, file_secrets_store):
provider_tokens = {
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
}
user_secrets = UserSecrets(
user_secrets = Secrets(
custom_secrets=custom_secrets, provider_tokens=provider_tokens
)
@ -323,7 +323,7 @@ async def test_delete_nonexistent_custom_secret(test_client, file_secrets_store)
provider_tokens = {
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
}
user_secrets = UserSecrets(
user_secrets = Secrets(
custom_secrets=custom_secrets, provider_tokens=provider_tokens
)
@ -355,7 +355,7 @@ async def test_add_git_providers_with_host(test_client, file_secrets_store):
provider_tokens = {
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
}
user_secrets = UserSecrets(provider_tokens=provider_tokens)
user_secrets = Secrets(provider_tokens=provider_tokens)
await file_secrets_store.store(user_secrets)
# Mock check_provider_tokens to return empty string (no error)
@ -394,7 +394,7 @@ async def test_add_git_providers_update_host_only(test_client, file_secrets_stor
token=SecretStr('github-token'), host='github.com'
)
}
user_secrets = UserSecrets(provider_tokens=provider_tokens)
user_secrets = Secrets(provider_tokens=provider_tokens)
await file_secrets_store.store(user_secrets)
# Mock check_provider_tokens to return empty string (no error)
@ -433,7 +433,7 @@ async def test_add_git_providers_invalid_token_with_host(
):
"""Test adding an invalid token with a host."""
# Create initial user secrets
user_secrets = UserSecrets()
user_secrets = Secrets()
await file_secrets_store.store(user_secrets)
# Mock validate_provider_token to return None (invalid token)
@ -456,7 +456,7 @@ async def test_add_git_providers_invalid_token_with_host(
async def test_add_multiple_git_providers_with_hosts(test_client, file_secrets_store):
"""Test adding multiple git providers with different hosts."""
# Create initial user secrets
user_secrets = UserSecrets()
user_secrets = Secrets()
await file_secrets_store.store(user_secrets)
# Mock check_provider_tokens to return empty string (no error)

View File

@ -9,7 +9,7 @@ from pydantic import SecretStr
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.server.app import app
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.memory import InMemoryFileStore
from openhands.storage.secrets.secrets_store import SecretsStore
from openhands.storage.settings.file_settings_store import FileSettingsStore
@ -43,7 +43,7 @@ class MockUserAuth(UserAuth):
async def get_secrets_store(self) -> SecretsStore | None:
return None
async def get_user_secrets(self) -> UserSecrets | None:
async def get_secrets(self) -> Secrets | None:
return None
@classmethod

View File

@ -14,8 +14,8 @@ from openhands.server.routes.secrets import (
from openhands.server.routes.settings import store_llm_settings
from openhands.server.settings import POSTProviderModel
from openhands.storage import get_file_store
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.data_models.settings import Settings
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.secrets.file_secrets_store import FileSecretsStore
@ -220,9 +220,9 @@ async def test_store_provider_tokens_new_tokens(test_client, file_secrets_store)
mock_store = MagicMock()
mock_store.load = AsyncMock(return_value=None) # No existing settings
UserSecrets()
Secrets()
user_secrets = await file_secrets_store.store(UserSecrets())
user_secrets = await file_secrets_store.store(Secrets())
response = test_client.post('/api/add-git-providers', json=provider_tokens)
assert response.status_code == 200
@ -242,8 +242,8 @@ async def test_store_provider_tokens_update_existing(test_client, file_secrets_s
github_token = ProviderToken(token=SecretStr('old-token'))
provider_tokens = {ProviderType.GITHUB: github_token}
# Create a UserSecrets with the provider tokens
user_secrets = UserSecrets(provider_tokens=provider_tokens)
# Create a Secrets with the provider tokens
user_secrets = Secrets(provider_tokens=provider_tokens)
await file_secrets_store.store(user_secrets)
@ -268,7 +268,7 @@ async def test_store_provider_tokens_keep_existing(test_client, file_secrets_sto
# Create existing secrets with a GitHub token
github_token = ProviderToken(token=SecretStr('existing-token'))
provider_tokens = {ProviderType.GITHUB: github_token}
user_secrets = UserSecrets(provider_tokens=provider_tokens)
user_secrets = Secrets(provider_tokens=provider_tokens)
await file_secrets_store.store(user_secrets)

View File

@ -9,7 +9,7 @@ from pydantic import SecretStr
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.server.app import app
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
from openhands.storage.memory import InMemoryFileStore
from openhands.storage.secrets.secrets_store import SecretsStore
from openhands.storage.settings.file_settings_store import FileSettingsStore
@ -43,7 +43,7 @@ class MockUserAuth(UserAuth):
async def get_secrets_store(self) -> SecretsStore | None:
return None
async def get_user_secrets(self) -> UserSecrets | None:
async def get_secrets(self) -> Secrets | None:
return None
@classmethod

View File

@ -10,12 +10,12 @@ from openhands.integrations.provider import (
ProviderToken,
ProviderType,
)
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.data_models.secrets import Secrets
class TestUserSecrets:
class TestSecrets:
def test_adding_only_provider_tokens(self):
"""Test adding only provider tokens to the UserSecrets."""
"""Test adding only provider tokens to the Secrets."""
# Create provider tokens
github_token = ProviderToken(
token=SecretStr('github-token-123'), user_id='user1'
@ -31,7 +31,7 @@ class TestUserSecrets:
}
# Initialize the store with a dict that will be converted to MappingProxyType
store = UserSecrets(provider_tokens=provider_tokens)
store = Secrets(provider_tokens=provider_tokens)
# Verify the tokens were added correctly
assert isinstance(store.provider_tokens, MappingProxyType)
@ -52,7 +52,7 @@ class TestUserSecrets:
assert len(store.custom_secrets) == 0
def test_adding_only_custom_secrets(self):
"""Test adding only custom secrets to the UserSecrets."""
"""Test adding only custom secrets to the Secrets."""
# Create custom secrets
custom_secrets = {
'API_KEY': CustomSecret(
@ -64,7 +64,7 @@ class TestUserSecrets:
}
# Initialize the store with custom secrets
store = UserSecrets(custom_secrets=custom_secrets)
store = Secrets(custom_secrets=custom_secrets)
# Verify the custom secrets were added correctly
assert isinstance(store.custom_secrets, MappingProxyType)
@ -95,7 +95,7 @@ class TestUserSecrets:
custom_secrets_proxy = MappingProxyType({'API_KEY': custom_secret})
# Test with dict for provider_tokens and MappingProxyType for custom_secrets
store1 = UserSecrets(
store1 = Secrets(
provider_tokens=provider_tokens_dict, custom_secrets=custom_secrets_proxy
)
@ -120,7 +120,7 @@ class TestUserSecrets:
'API_KEY': {'secret': 'api-key-123', 'description': 'API key'}
}
store2 = UserSecrets(
store2 = Secrets(
provider_tokens=provider_tokens_proxy, custom_secrets=custom_secrets_dict
)
@ -146,7 +146,7 @@ class TestUserSecrets:
)
}
initial_store = UserSecrets(
initial_store = Secrets(
provider_tokens=MappingProxyType({ProviderType.GITHUB: github_token}),
custom_secrets=MappingProxyType(custom_secret),
)
@ -212,7 +212,7 @@ class TestUserSecrets:
)
def test_serialization_with_expose_secrets(self):
"""Test serializing the UserSecrets with expose_secrets=True."""
"""Test serializing the Secrets with expose_secrets=True."""
# Create a store with both provider tokens and custom secrets
github_token = ProviderToken(
token=SecretStr('github-token-123'), user_id='user1'
@ -223,7 +223,7 @@ class TestUserSecrets:
)
}
store = UserSecrets(
store = Secrets(
provider_tokens=MappingProxyType({ProviderType.GITHUB: github_token}),
custom_secrets=MappingProxyType(custom_secrets),
)
@ -290,7 +290,7 @@ class TestUserSecrets:
}
# Initialize the store
store = UserSecrets(provider_tokens=mixed_provider_tokens)
store = Secrets(provider_tokens=mixed_provider_tokens)
# Verify all tokens are converted to SecretStr
assert isinstance(store.provider_tokens, MappingProxyType)
@ -322,7 +322,7 @@ class TestUserSecrets:
}
# Initialize the store
store = UserSecrets(custom_secrets=custom_secrets_dict)
store = Secrets(custom_secrets=custom_secrets_dict)
# Verify all secrets are converted to CustomSecret objects
assert isinstance(store.custom_secrets, MappingProxyType)