diff --git a/backend/open_webui/utils/actions.py b/backend/open_webui/utils/actions.py index 585a409b72..1249dd60cb 100644 --- a/backend/open_webui/utils/actions.py +++ b/backend/open_webui/utils/actions.py @@ -9,7 +9,7 @@ from open_webui.models.functions import Functions from open_webui.models.users import UserModel from open_webui.socket.main import get_event_call, get_event_emitter from open_webui.utils.middleware import process_tool_result -from open_webui.utils.models import get_all_models +from open_webui.utils.models import check_model_access, get_all_models from open_webui.utils.plugin import get_function_module_from_cache logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) @@ -46,6 +46,23 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A raise Exception('Model not found') model = models[model_id] + # Availability gate — keep this route consistent with the actions a model + # actually surfaces to the client. Executing admin-authored Function code is + # intended; this only stops a disabled, unassigned, or access-restricted + # action from being reached by calling the route with a raw action_id. + if action.type != 'action' or not action.is_active: + raise Exception(f'Action not available: {action_id}') + + # Direct connections carry a client-supplied model the caller already owns, + # so scope the model-bound checks to server-resolved models. + if not getattr(request.state, 'direct', False) and user.role != 'admin': + await check_model_access(user, model) + # model['actions'] entries are '' or '.'; + # the function id is always the prefix. + surfaced_action_ids = {item.get('id', '').split('.', 1)[0] for item in model.get('actions', [])} + if action_id not in surfaced_action_ids: + raise Exception(f'Action not available: {action_id}') + __event_emitter__ = await get_event_emitter( { 'chat_id': data['chat_id'],