feat(llm): mask API keys in UI responses and prevent masked-key leakage in fetch/test/save payloads

- Add mask_api_key() and is_masked_or_placeholder() to llm_provider service
- Return masked keys in all provider CRUD endpoints
- Reject masked/placeholder keys in fetch_models and test_provider_config
- Show masked key with Change button in ProviderConfig.svelte edit form
- Exclude masked keys from fetch-models, test, and submit payloads on frontend
- Update semantics-core skill with clarified complexity tier rules
- Switch agent modes from subagent to all
This commit is contained in:
2026-05-15 23:34:09 +03:00
parent b3572ce443
commit 30ba70933d
203 changed files with 158083 additions and 28396 deletions

View File

@@ -16,7 +16,7 @@ from ...core.logger import logger
from ...dependencies import get_current_user as get_current_active_user
from ...plugins.llm_analysis.models import LLMProviderConfig, LLMProviderType
from ...schemas.auth import User
from ...services.llm_provider import LLMProviderService
from ...services.llm_provider import LLMProviderService, is_masked_or_placeholder, mask_api_key
# #region FetchModelsRequest [C:1] [TYPE Class]
@@ -36,7 +36,7 @@ router = APIRouter(tags=["LLM"])
# #endregion router
# #region _is_valid_runtime_api_key [TYPE Function]
# #region _is_valid_runtime_api_key [C:4] [TYPE Function]
# @BRIEF Validate decrypted runtime API key presence/shape.
# @PRE: value can be None.
# @POST: Returns True only for non-placeholder key.
@@ -53,7 +53,7 @@ def _is_valid_runtime_api_key(value: str | None) -> bool:
# #endregion _is_valid_runtime_api_key
# #region get_providers [TYPE Function]
# #region get_providers [C:4] [TYPE Function]
# @BRIEF Retrieve all LLM provider configurations.
# @PRE: User is authenticated.
# @POST: Returns list of LLMProviderConfig.
@@ -78,7 +78,7 @@ async def get_providers(
provider_type=LLMProviderType(p.provider_type),
name=p.name,
base_url=p.base_url,
api_key="********",
api_key=mask_api_key(service.get_decrypted_api_key(p.id)) if p.api_key else "",
default_model=p.default_model,
is_active=p.is_active,
)
@@ -89,7 +89,7 @@ async def get_providers(
# #endregion get_providers
# #region fetch_models [TYPE Function]
# #region fetch_models [C:4] [TYPE Function]
# @BRIEF Fetch available models from an LLM provider by base_url+provider_type, or by provider_id.
# @PRE: User is authenticated. Either provider_id or base_url+provider_type must be provided.
# @POST: Returns a list of available model IDs.
@@ -109,6 +109,11 @@ async def fetch_models(
api_key = payload.api_key or ""
provider_id = payload.provider_id or ""
# Treat masked/placeholder API keys as if no api_key was provided;
# they are display-only and cannot be used for real API calls.
if is_masked_or_placeholder(api_key):
api_key = ""
# Early validation: at least one of api_key or provider_id is required
if not api_key and not provider_id:
raise HTTPException(
@@ -163,7 +168,7 @@ async def fetch_models(
# #endregion fetch_models
# #region get_llm_status [TYPE Function]
# #region get_llm_status [C:4] [TYPE Function]
# @BRIEF Returns whether LLM runtime is configured for dashboard validation.
# @PRE: User is authenticated.
# @POST: configured=true only when an active provider with valid decrypted key exists.
@@ -234,7 +239,7 @@ async def get_llm_status(
# #endregion get_llm_status
# #region create_provider [TYPE Function]
# #region create_provider [C:4] [TYPE Function]
# @BRIEF Create a new LLM provider configuration.
# @PRE: User is authenticated and has admin permissions.
# @POST: Returns the created LLMProviderConfig.
@@ -258,7 +263,7 @@ async def create_provider(
provider_type=LLMProviderType(provider.provider_type),
name=provider.name,
base_url=provider.base_url,
api_key="********",
api_key=mask_api_key(config.api_key),
default_model=provider.default_model,
is_active=provider.is_active,
)
@@ -267,7 +272,7 @@ async def create_provider(
# #endregion create_provider
# #region update_provider [TYPE Function]
# #region update_provider [C:4] [TYPE Function]
# @BRIEF Update an existing LLM provider configuration.
# @PRE: User is authenticated and has admin permissions.
# @POST: Returns the updated LLMProviderConfig.
@@ -293,7 +298,7 @@ async def update_provider(
provider_type=LLMProviderType(provider.provider_type),
name=provider.name,
base_url=provider.base_url,
api_key="********",
api_key=mask_api_key(service.get_decrypted_api_key(provider.id)) if provider.api_key else "",
default_model=provider.default_model,
is_active=provider.is_active,
)
@@ -302,7 +307,7 @@ async def update_provider(
# #endregion update_provider
# #region delete_provider [TYPE Function]
# #region delete_provider [C:4] [TYPE Function]
# @BRIEF Delete an LLM provider configuration.
# @PRE: User is authenticated and has admin permissions.
# @POST: Returns success status.
@@ -325,7 +330,7 @@ async def delete_provider(
# #endregion delete_provider
# #region test_connection [TYPE Function]
# #region test_connection [C:4] [TYPE Function]
# @BRIEF Test connection to an LLM provider.
# @PRE: User is authenticated.
# @POST: Returns success status and message.
@@ -380,7 +385,7 @@ async def test_connection(
# #endregion test_connection
# #region test_provider_config [TYPE Function]
# #region test_provider_config [C:4] [TYPE Function]
# @BRIEF Test connection with a provided configuration (not yet saved).
# @PRE: User is authenticated.
# @POST: Returns success status and message.
@@ -400,8 +405,8 @@ async def test_provider_config(
extra={"src": "llm_routes.test_provider_config"},
)
# Check if API key is provided
if not config.api_key or config.api_key == "********":
# Check if API key is provided (reject empty, placeholder, or masked keys)
if is_masked_or_placeholder(config.api_key):
raise HTTPException(
status_code=400, detail="API key is required for testing connection"
)