feat: secrets manager settings (#8068)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
This commit is contained in:
sp.wack
2025-05-15 19:30:10 +04:00
committed by GitHub
parent 7a4ea23b9d
commit 04d585513c
27 changed files with 1571 additions and 105 deletions

View File

@@ -2,12 +2,14 @@ from fastapi import APIRouter, Depends, status
from fastapi.responses import JSONResponse
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import CustomSecret
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.integrations.service_types import ProviderType
from openhands.integrations.utils import validate_provider_token
from openhands.server.settings import (
CustomSecretModel,
CustomSecretWithoutValueModel,
GETCustomSecrets,
POSTCustomSecrets,
POSTProviderModel,
)
from openhands.server.user_auth import (
@@ -188,7 +190,15 @@ async def load_custom_secrets_names(
content={'error': 'User secrets not found'},
)
custom_secrets = list(user_secrets.custom_secrets.keys())
custom_secrets: list[CustomSecretWithoutValueModel] = []
if user_secrets.custom_secrets:
for secret_name, secret_value in user_secrets.custom_secrets.items():
custom_secret = CustomSecretWithoutValueModel(
name=secret_name,
description=secret_value.description,
)
custom_secrets.append(custom_secret)
return GETCustomSecrets(custom_secrets=custom_secrets)
except Exception as e:
@@ -201,7 +211,7 @@ async def load_custom_secrets_names(
@app.post('/secrets', response_model=dict[str, str])
async def create_custom_secret(
incoming_secret: POSTCustomSecrets,
incoming_secret: CustomSecretModel,
secrets_store: SecretsStore = Depends(get_secrets_store),
) -> JSONResponse:
try:
@@ -209,14 +219,20 @@ async def create_custom_secret(
if existing_secrets:
custom_secrets = dict(existing_secrets.custom_secrets)
for secret_name, secret_value in incoming_secret.custom_secrets.items():
if secret_name in custom_secrets:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'message': f'Secret {secret_name} already exists'},
)
secret_name = incoming_secret.name
secret_value = incoming_secret.value
secret_description = incoming_secret.description
custom_secrets[secret_name] = secret_value
if secret_name in custom_secrets:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'message': f'Secret {secret_name} already exists'},
)
custom_secrets[secret_name] = CustomSecret(
secret=secret_value,
description=secret_description or '',
)
# Create a new UserSecrets that preserves provider tokens
updated_user_secrets = UserSecrets(
@@ -227,7 +243,7 @@ async def create_custom_secret(
await secrets_store.store(updated_user_secrets)
return JSONResponse(
status_code=status.HTTP_200_OK,
status_code=status.HTTP_201_CREATED,
content={'message': 'Secret created successfully'},
)
except Exception as e:
@@ -241,7 +257,7 @@ async def create_custom_secret(
@app.put('/secrets/{secret_id}', response_model=dict[str, str])
async def update_custom_secret(
secret_id: str,
incoming_secret: POSTCustomSecrets,
incoming_secret: CustomSecretWithoutValueModel,
secrets_store: SecretsStore = Depends(get_secrets_store),
) -> JSONResponse:
try:
@@ -254,13 +270,23 @@ async def update_custom_secret(
content={'error': f'Secret with ID {secret_id} not found'},
)
secret_name = incoming_secret.name
secret_description = incoming_secret.description
custom_secrets = dict(existing_secrets.custom_secrets)
custom_secrets.pop(secret_id)
existing_secret = custom_secrets.pop(secret_id)
for secret_name, secret_value in incoming_secret.custom_secrets.items():
custom_secrets[secret_name] = secret_value
if secret_name != secret_id and secret_name in custom_secrets:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'message': f'Secret {secret_name} already exists'},
)
custom_secrets[secret_name] = CustomSecret(
secret=existing_secret.secret,
description=secret_description or '',
)
# Create a new UserSecrets that preserves provider tokens
updated_secrets = UserSecrets(
custom_secrets=custom_secrets,
provider_tokens=existing_secrets.provider_tokens,

View File

@@ -6,7 +6,7 @@ from pydantic import (
)
from openhands.core.config.mcp_config import MCPConfig
from openhands.integrations.provider import ProviderToken
from openhands.integrations.provider import CustomSecret, ProviderToken
from openhands.integrations.service_types import ProviderType
from openhands.storage.data_models.settings import Settings
@@ -25,7 +25,7 @@ class POSTCustomSecrets(BaseModel):
Adding new custom secret
"""
custom_secrets: dict[str, str | SecretStr] = {}
custom_secrets: dict[str, CustomSecret] = {}
class GETSettingsModel(Settings):
@@ -41,9 +41,26 @@ class GETSettingsModel(Settings):
model_config = {'use_enum_values': True}
class CustomSecretWithoutValueModel(BaseModel):
"""
Custom secret model without value
"""
name: str
description: str | None = None
class CustomSecretModel(CustomSecretWithoutValueModel):
"""
Custom secret model with value
"""
value: SecretStr
class GETCustomSecrets(BaseModel):
"""
Custom secrets names
"""
custom_secrets: list[str] | None = None
custom_secrets: list[CustomSecretWithoutValueModel] | None = None