"""Pydantic models for the service registry API.""" from __future__ import annotations from typing import Any from pydantic import BaseModel, Field, field_validator def _validate_config_keys(config: dict[str, Any]) -> dict[str, Any]: """Reject credential keys in non-secret service config. Secrets are sent in the separate ``secrets`` mapping; the plain ``config`` object must never hold them. """ forbidden = { "password", "token", "secret", "api_key", "apikey", "private_key", "passphrase", "credential", } def _check(value: Any) -> None: if isinstance(value, dict): for key, child in value.items(): if key.lower() in forbidden: raise ValueError( f"Credential key '{key}' is not allowed in service config" ) _check(child) elif isinstance(value, list): for item in value: _check(item) _check(config) return config class ServiceInstanceInput(BaseModel): """Payload for creating or updating a service instance.""" id: str | None = None service_type: str = Field(..., min_length=1) name: str = Field(..., min_length=1) config: dict[str, Any] = Field(default_factory=dict) secrets: dict[str, str] = Field(default_factory=dict) enabled: bool = True @field_validator("config") @classmethod def reject_credential_keys(cls, value: dict[str, Any]) -> dict[str, Any]: return _validate_config_keys(value or {}) class ServiceInstance(BaseModel): """Persisted service instance returned by the API (no plaintext secrets).""" id: str service_type: str name: str config: dict[str, Any] secrets_set: dict[str, bool] enabled: bool created_at: int updated_at: int class SecretFieldInfo(BaseModel): key: str label: str required: bool = False helper: str | None = None class WidgetKindInfo(BaseModel): kind: str name: str description: str config_schema: dict[str, Any] default_config: dict[str, Any] refresh_interval_ms: int class ServiceTypeInfo(BaseModel): """Metadata about a registered service type.""" service_type: str name: str description: str config_schema: dict[str, Any] secret_fields: list[SecretFieldInfo] widget_kinds: list[WidgetKindInfo]