open-webui/backend/open_webui/utils/valves.py
Classic298 c9f9c4b1da
fix: functions and tools with a required Valves field cannot be saved (#32165)
Since the recent change that keeps a history of function and tool edits, saving a new function or tool whose Valves (its admin settings, such as an API key) have a required field with no default was refused with "Current Valves are incompatible with this code: ... Field required", because a new one has no Valves set yet. Editing the code to add a new required field failed the same way. Missing required fields no longer block saving, so the function or tool can be installed first and its Valves filled in afterwards, as before that change. Saved Valves that have the wrong type for the new code are still rejected.

Fixes #32145
2026-10-10 19:59:52 +04:00

63 lines
2.1 KiB
Python

import base64
import hashlib
import logging
from functools import lru_cache
from cryptography.fernet import Fernet, InvalidToken
from fastapi import HTTPException
from pydantic import ValidationError
from open_webui.env import ENABLE_VALVE_ENCRYPTION, WEBUI_SECRET_KEY
from open_webui.utils.json_codec import JSONCodec
log = logging.getLogger(__name__)
@lru_cache(maxsize=1)
def _fernet() -> Fernet:
key = WEBUI_SECRET_KEY.encode()
if len(WEBUI_SECRET_KEY) != 44:
key = base64.urlsafe_b64encode(hashlib.sha256(key).digest())
return Fernet(key)
def encrypt_valves(valves: dict) -> dict | str:
if not ENABLE_VALVE_ENCRYPTION:
return valves
return _fernet().encrypt(JSONCodec.dumps(valves).encode()).decode()
def decrypt_valves(valves) -> dict:
if not valves:
return {}
if isinstance(valves, dict):
return valves
if not isinstance(valves, str):
return {}
try:
decrypted = JSONCodec.loads(_fernet().decrypt(valves.encode()).decode())
except (InvalidToken, JSONCodec.JSONDecodeError) as e:
log.warning('Failed to decrypt valves: %s', type(e).__name__)
return {}
return decrypted if isinstance(decrypted, dict) else {}
def validate_valves(module, valves):
if hasattr(module, 'Valves'):
values = decrypt_valves(valves) or {}
try:
# Validate without replacing the stored values (including unused keys/secrets).
module.Valves(**{key: value for key, value in values.items() if value is not None})
except Exception as error:
# Required fields with no stored value yet get filled in after saving.
if isinstance(error, ValidationError) and all(item['type'] == 'missing' for item in error.errors()):
return
detail = (
'; '.join(
f'{".".join(map(str, item["loc"]))}: {item["msg"]}' for item in error.errors(include_input=False)
)
if isinstance(error, ValidationError)
else str(error)
)
raise HTTPException(400, f'Current Valves are incompatible with this code: {detail}') from error