fix: don't run a disabled function's user-valve option methods

Any logged-in user can open the user-valve settings of a function an admin has turned off. Resolving that form's dynamic select options calls the admin-written option method server-side and hands it the requesting user, so code belonging to a disabled function runs on a normal user's request.

Dynamic options are now only resolved while the function is active. Users can still open and save user valves of a disabled function, they just get the static schema, so seeding valves before switching a function on keeps working. The gate is is_active alone: user valves exist to be user-facing, and gating on global/role/access instead would break that.

When options cannot be resolved at all (disabled function, missing method, method raises or returns a non-list) the select's input hint is now dropped rather than passed on unresolved. Previously the raw method name reached the browser as the option list, which crashed the valves form with "options.map is not a function" or rendered one dropdown entry per character of the method name.
This commit is contained in:
Classic298 2026-08-03 01:59:11 +02:00
parent 29541cbb52
commit 969e30a273
2 changed files with 31 additions and 27 deletions

View file

@ -598,8 +598,8 @@ async def get_function_user_valves_spec_by_id(
if hasattr(function_module, 'UserValves'):
UserValves = function_module.UserValves
schema = UserValves.schema()
# Resolve dynamic options for select dropdowns
schema = resolve_valves_schema_options(UserValves, schema, user)
# Any verified user can reach this, so never run a disabled function's option methods
schema = resolve_valves_schema_options(UserValves, schema, user, resolve_dynamic=function.is_active)
return schema
return None
else:

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import inspect
import logging
import os
import re
@ -24,7 +25,9 @@ from open_webui.models.tools import Tools
log = logging.getLogger(__name__)
def resolve_valves_schema_options(valves_class: type, schema: dict, user: Any = None) -> dict:
def resolve_valves_schema_options(
valves_class: type, schema: dict, user: Any = None, resolve_dynamic: bool = True
) -> dict:
"""
Resolve dynamic options in a Valves schema.
@ -64,9 +67,10 @@ def resolve_valves_schema_options(valves_class: type, schema: dict, user: Any =
valves_class: The Valves or UserValves Pydantic model class
schema: The JSON schema dict from valves_class.schema()
user: Optional user object passed to methods that accept __user__
resolve_dynamic: When False, no option method is called
Returns:
Modified schema dict with resolved options
Modified schema dict with resolved options; unresolvable select options lose their `input` hint
"""
if not schema or 'properties' not in schema:
return schema
@ -104,41 +108,41 @@ def resolve_valves_schema_options(valves_class: type, schema: dict, user: Any =
resolved_options = options
# Case 2: options is a string - treat as method name
elif isinstance(options, str) and options:
elif isinstance(options, str) and options and resolve_dynamic:
method = getattr(valves_class, options, None)
if method is None or not callable(method):
log.warning(f"options '{options}' not found or not callable on {valves_class.__name__}")
continue
else:
try:
sig = inspect.signature(method)
params = sig.parameters
try:
import inspect
# Prepare kwargs based on what the method accepts
kwargs = {}
if '__user__' in params and user is not None:
kwargs['__user__'] = user.model_dump() if hasattr(user, 'model_dump') else user
if 'user' in params and user is not None:
kwargs['user'] = user.model_dump() if hasattr(user, 'model_dump') else user
sig = inspect.signature(method)
params = sig.parameters
resolved_options = method(**kwargs) if kwargs else method()
# Prepare kwargs based on what the method accepts
kwargs = {}
if '__user__' in params and user is not None:
kwargs['__user__'] = user.model_dump() if hasattr(user, 'model_dump') else user
if 'user' in params and user is not None:
kwargs['user'] = user.model_dump() if hasattr(user, 'model_dump') else user
# Validate return type
if not isinstance(resolved_options, list):
log.warning(f"Method '{options}' did not return a list for {prop_name}")
resolved_options = None
resolved_options = method(**kwargs) if kwargs else method()
except Exception as e:
log.warning(f'Failed to resolve options for {prop_name}: {e}')
# Validate return type
if not isinstance(resolved_options, list):
log.warning(f"Method '{options}' did not return a list for {prop_name}")
continue
schema['properties'][prop_name] = dict(prop_schema)
except Exception as e:
log.warning(f'Failed to resolve options for {prop_name}: {e}')
continue
else:
# Invalid options type - skip
if resolved_options is None:
if input_config.get('type') in ('select', 'multiselect'):
# Without a usable option list the client renders the field broken or blank
schema['properties'][prop_name].pop('input', None)
continue
# Update the schema with resolved options
schema['properties'][prop_name] = dict(prop_schema)
if 'input' not in schema['properties'][prop_name]:
schema['properties'][prop_name]['input'] = {'type': 'select'}
else: