mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-07 08:17:07 +00:00
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:
parent
29541cbb52
commit
969e30a273
2 changed files with 31 additions and 27 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue