From 10724d057af13a826c52e92b1c01a031656768d5 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 2 Jul 2026 12:38:22 -0500 Subject: [PATCH 001/615] refac --- src/lib/components/workspace/Models/ModelEditor.svelte | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index b013e19956..acf6890813 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -296,9 +296,7 @@ }; onMount(async () => { - if (!$tools) { - await tools.set(await getTools(localStorage.token)); - } + await tools.set((await getTools(localStorage.token).catch(() => null)) ?? []); skillsList = (await getSkills(localStorage.token).catch(() => null)) ?? []; if (!$functions) { await functions.set(await getFunctions(localStorage.token)); From 44c2a27ce0695d8e9c7e72f9a84dc325cb15096a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 16:20:19 -0500 Subject: [PATCH 002/615] refac --- backend/open_webui/utils/context_compaction.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/open_webui/utils/context_compaction.py b/backend/open_webui/utils/context_compaction.py index e20f6b9cdc..209f56892f 100644 --- a/backend/open_webui/utils/context_compaction.py +++ b/backend/open_webui/utils/context_compaction.py @@ -205,9 +205,7 @@ def _parse_positive_int(value: Any) -> int | None: def _resolve_token_threshold(global_threshold: int, metadata: dict) -> int: configured_threshold = _parse_positive_int((metadata.get('params') or {}).get('compact_token_threshold')) - if configured_threshold is None: - return global_threshold - return min(configured_threshold, global_threshold) + return configured_threshold or global_threshold def _apply_latest_summary_checkpoint(messages: list[dict]) -> tuple[list[dict], str | None]: From bd6e0b61c2ae073aba9556ae46c345f4749acb84 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:28:05 -0500 Subject: [PATCH 003/615] refac --- .env.example | 3 +++ backend/open_webui/env.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.env.example b/.env.example index 55bf5d386f..c5b0adc4b9 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,9 @@ CORS_ALLOW_ORIGIN='*' # Set to false to keep memory tools enabled without adding memory context to the system context. ENABLE_MEMORY_SYSTEM_CONTEXT=true +# Set to false to disable built-in Python Tools and Functions plugin execution surfaces. +ENABLE_PLUGINS=true + # For production you should set this to match the proxy configuration (127.0.0.1) FORWARDED_ALLOW_IPS='*' diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index cd97bda4a8..da99fbb94d 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -1038,6 +1038,8 @@ SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = ( # TOOLS/FUNCTIONS PIP OPTIONS #################################### +ENABLE_PLUGINS = os.getenv('ENABLE_PLUGINS', 'True').lower() == 'true' + ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = ( os.getenv('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS', 'True').lower() == 'true' ) From 8e46450acd7ae11a4dee166d19a7c9833d991e79 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:28:34 -0500 Subject: [PATCH 004/615] refac --- backend/open_webui/events.py | 5 +- backend/open_webui/functions.py | 5 +- backend/open_webui/main.py | 2 + backend/open_webui/routers/functions.py | 11 ++- backend/open_webui/routers/tools.py | 34 ++++++---- backend/open_webui/utils/actions.py | 5 +- backend/open_webui/utils/filter.py | 7 ++ backend/open_webui/utils/middleware.py | 90 ++++++++++++++----------- backend/open_webui/utils/models.py | 31 ++++++--- backend/open_webui/utils/plugin.py | 11 +++ backend/open_webui/utils/tools.py | 7 ++ 11 files changed, 143 insertions(+), 65 deletions(-) diff --git a/backend/open_webui/events.py b/backend/open_webui/events.py index 71f9f15f1b..4aa30f9128 100644 --- a/backend/open_webui/events.py +++ b/backend/open_webui/events.py @@ -8,7 +8,7 @@ import uuid from types import SimpleNamespace from typing import Any -from open_webui.env import VERSION +from open_webui.env import ENABLE_PLUGINS, VERSION from open_webui.models.config import Config from pydantic import BaseModel, ConfigDict, Field, model_validator from open_webui.retrieval.web.utils import validate_url @@ -1025,6 +1025,9 @@ class WebhookEventSink: async def dispatch_event_functions(app: Any, event: Event, request: Any | None = None) -> None: + if not ENABLE_PLUGINS: + return + from open_webui.models.functions import Functions from open_webui.utils.plugin import get_function_module_from_cache diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py index 4a82cf7f26..0cbfa8b980 100644 --- a/backend/open_webui/functions.py +++ b/backend/open_webui/functions.py @@ -20,7 +20,7 @@ from starlette.responses import Response, StreamingResponse from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.constants import ERROR_MESSAGES -from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL +from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, ENABLE_PLUGINS, GLOBAL_LOG_LEVEL from open_webui.models.functions import Functions from open_webui.models.models import Models from open_webui.models.users import UserModel @@ -69,6 +69,9 @@ async def get_function_module_by_id(request: Request, pipe_id: str): async def get_function_models(request): + if not ENABLE_PLUGINS: + return [] + pipes = await Functions.get_functions_by_type('pipe', active_only=True) pipe_models = [] diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 82eed2f4d6..887594829c 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -83,6 +83,7 @@ from open_webui.env import ( ENABLE_COMPRESSION_MIDDLEWARE, ENABLE_CUSTOM_MODEL_FALLBACK, ENABLE_EASTER_EGGS, + ENABLE_PLUGINS, EXTERNAL_PWA_MANIFEST_URL, # OAuth Back-Channel Logout ENABLE_OAUTH_BACKCHANNEL_LOGOUT, @@ -1927,6 +1928,7 @@ async def get_app_config(request: Request): 'enable_public_active_users_count': ENABLE_PUBLIC_ACTIVE_USERS_COUNT, 'enable_easter_eggs': ENABLE_EASTER_EGGS, 'enable_direct_connections': config.get('direct.enable'), + 'enable_plugins': ENABLE_PLUGINS, 'enable_folders': config.get('folders.enable'), 'folder_max_file_count': config.get('folders.max_file_count'), 'enable_channels': config.get('channels.enable'), diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index 8917bb37f0..93d2879c18 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -10,7 +10,7 @@ import aiohttp from fastapi import APIRouter, Depends, HTTPException, Request, status from open_webui.config import CACHE_DIR from open_webui.constants import ERROR_MESSAGES -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, ENABLE_PLUGINS from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session from open_webui.models.functions import ( @@ -46,11 +46,17 @@ router = APIRouter() @router.get('/', response_model=list[FunctionResponse]) async def get_functions(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if not ENABLE_PLUGINS: + return [] + return await Functions.get_functions(db=db) @router.get('/list', response_model=list[FunctionUserResponse]) async def get_function_list(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + if not ENABLE_PLUGINS: + return [] + return await Functions.get_function_list(db=db) @@ -65,6 +71,9 @@ async def get_functions( user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session), ): + if not ENABLE_PLUGINS: + return [] + return await Functions.get_functions(include_valves=include_valves, db=db) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index c830753b5a..b8ef51a280 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -10,7 +10,7 @@ import aiohttp from fastapi import APIRouter, Depends, HTTPException, Request, status from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, CACHE_DIR from open_webui.constants import ERROR_MESSAGES -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, ENABLE_PLUGINS from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session from open_webui.models.access_grants import AccessGrants @@ -72,20 +72,23 @@ async def get_tools( tools = [] # Local Tools - tools_cache = get_tools_cache(request) - for tool in await Tools.get_tools(defer_content=True, db=db): - tool_module = tools_cache.get(tool.id) - has_user_valves = ( - hasattr(tool_module, 'UserValves') if tool_module else (tool.meta.has_user_valves if tool.meta else False) - ) - tools.append( - ToolUserResponse( - **{ - **tool.model_dump(), - 'has_user_valves': has_user_valves, - } + if ENABLE_PLUGINS: + tools_cache = get_tools_cache(request) + for tool in await Tools.get_tools(defer_content=True, db=db): + tool_module = tools_cache.get(tool.id) + has_user_valves = ( + hasattr(tool_module, 'UserValves') + if tool_module + else (tool.meta.has_user_valves if tool.meta else False) + ) + tools.append( + ToolUserResponse( + **{ + **tool.model_dump(), + 'has_user_valves': has_user_valves, + } + ) ) - ) # OpenAPI Tool Servers server_access_grants = {} @@ -199,6 +202,9 @@ async def get_tools( @router.get('/list', response_model=list[ToolAccessResponse]) async def get_tool_list(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if not ENABLE_PLUGINS: + return [] + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: tools = await Tools.get_tools(defer_content=True, db=db) else: diff --git a/backend/open_webui/utils/actions.py b/backend/open_webui/utils/actions.py index eb5a84d0d4..585a409b72 100644 --- a/backend/open_webui/utils/actions.py +++ b/backend/open_webui/utils/actions.py @@ -4,7 +4,7 @@ import sys from typing import Any from fastapi import Request -from open_webui.env import GLOBAL_LOG_LEVEL +from open_webui.env import ENABLE_PLUGINS, GLOBAL_LOG_LEVEL 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 @@ -17,6 +17,9 @@ log = logging.getLogger(__name__) async def chat_action(request: Request, action_id: str, form_data: dict, user: Any): + if not ENABLE_PLUGINS: + raise Exception('Plugins are disabled by ENABLE_PLUGINS=false') + if '.' in action_id: action_id, sub_action_id = action_id.split('.') else: diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 84aebdaacb..98eb24dd02 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -1,6 +1,7 @@ import inspect import logging +from open_webui.env import ENABLE_PLUGINS from open_webui.models.functions import Functions from open_webui.utils.plugin import ( get_function_module_from_cache, @@ -19,6 +20,9 @@ async def get_function_module(request, function_id, load_from_db=True): async def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = None): + if not ENABLE_PLUGINS: + return [] + async def get_priority(function_id): try: function_module = await get_function_module(request, function_id) @@ -64,6 +68,9 @@ async def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = # Grant these filters the discernment to pass what serves # and refuse what harms, for every soul in the house. async def process_filter_functions(request, filter_functions, filter_type, form_data, extra_params): + if not ENABLE_PLUGINS: + return form_data, {} + skip_files = None for function in filter_functions: diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index b507aa66c5..349ad06f1a 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -33,6 +33,7 @@ from open_webui.env import ( CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE, ENABLE_API_OUTLET_FILTERS, ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION, + ENABLE_PLUGINS, ENABLE_QUERIES_CACHE, ENABLE_REALTIME_CHAT_SAVE, ENABLE_RESPONSES_API_STATEFUL, @@ -2421,19 +2422,20 @@ async def process_chat_payload(request, form_data, user, metadata, model): except Exception as e: raise e - try: - filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) - filter_functions = await Functions.get_functions_by_ids(filter_ids) + if ENABLE_PLUGINS: + try: + filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + filter_functions = await Functions.get_functions_by_ids(filter_ids) - form_data, flags = await process_filter_functions( - request=request, - filter_functions=filter_functions, - filter_type='inlet', - form_data=form_data, - extra_params=extra_params, - ) - except Exception as e: - raise Exception(f'{e}') + form_data, flags = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='inlet', + form_data=form_data, + extra_params=extra_params, + ) + except Exception as e: + raise Exception(f'{e}') features = form_data.pop('features', None) or {} extra_params['__features__'] = features @@ -2618,6 +2620,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): mcp_tools_dict = {} if tool_ids: + db_tool_ids = [] for tool_id in tool_ids: if tool_id.startswith('server:mcp:'): try: @@ -2669,18 +2672,21 @@ async def process_chat_payload(request, form_data, user, metadata, model): } ) continue + elif ENABLE_PLUGINS: + db_tool_ids.append(tool_id) - tools_dict = await get_tools( - request, - tool_ids, - user, - { - **extra_params, - '__model__': models[task_model_id], - '__messages__': form_data['messages'], - '__files__': metadata.get('files', []), - }, - ) + if db_tool_ids: + tools_dict = await get_tools( + request, + db_tool_ids, + user, + { + **extra_params, + '__model__': models[task_model_id], + '__messages__': form_data['messages'], + '__files__': metadata.get('files', []), + }, + ) if mcp_tools_dict: tools_dict = {**tools_dict, **mcp_tools_dict} @@ -2737,7 +2743,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): # Inject builtin tools for native function calling based on enabled features and model capability. # Only inject when the request originates from the UI (identified by session_id). # API callers don't expect hidden tools; they can explicitly request tools via tool_ids. - if use_builtin_tools: + if ENABLE_PLUGINS and use_builtin_tools: # Add file context to user messages chat_id = metadata.get('chat_id') form_data['messages'] = await add_file_context(form_data.get('messages', []), chat_id, user) @@ -3371,16 +3377,19 @@ async def outlet_filter_handler(ctx): '__model__': model, } - filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) - filter_functions = await Functions.get_functions_by_ids(filter_ids) + if ENABLE_PLUGINS: + filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + filter_functions = await Functions.get_functions_by_ids(filter_ids) - outlet_result, _ = await process_filter_functions( - request=request, - filter_functions=filter_functions, - filter_type='outlet', - form_data=outlet_data, - extra_params=extra_params, - ) + outlet_result, _ = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='outlet', + form_data=outlet_data, + extra_params=extra_params, + ) + else: + outlet_result = outlet_data if outlet_result and outlet_result.get('messages'): if not is_temp_chat and messages_map: @@ -3620,10 +3629,14 @@ async def streaming_chat_response_handler(response, ctx): '__model__': model, } - filter_functions = [ - await Functions.get_function_by_id(filter_id) - for filter_id in await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) - ] + filter_functions = ( + [ + await Functions.get_function_by_id(filter_id) + for filter_id in await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + ] + if ENABLE_PLUGINS + else [] + ) # Standard streaming response handler # event_caller is optional — only needed for direct (client-side) tools @@ -3907,7 +3920,8 @@ async def streaming_chat_response_handler(response, ctx): model_capabilities = model.get('info', {}).get('meta', {}).get('capabilities') or {} builtin_tools_meta = model.get('info', {}).get('meta', {}).get('builtinTools', {}) DETECT_CODE_INTERPRETER = ( - bool(features.get('code_interpreter')) + ENABLE_PLUGINS + and bool(features.get('code_interpreter')) and builtin_tools_meta.get('code_interpreter', True) and await Config.get('code_interpreter.enable') and model_capabilities.get('code_interpreter', True) diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index 39e139b053..4012685965 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -9,7 +9,7 @@ from open_webui.config import ( BYPASS_ADMIN_ACCESS_CONTROL, DEFAULT_ARENA_MODEL, ) -from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL +from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, ENABLE_PLUGINS, GLOBAL_LOG_LEVEL from open_webui.functions import get_function_models from open_webui.models.access_grants import AccessGrants from open_webui.models.config import Config @@ -125,11 +125,23 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) ] models = models + arena_models - global_action_ids = {function.id for function in await Functions.get_global_action_functions()} - enabled_action_ids = {function.id for function in await Functions.get_functions_by_type('action', active_only=True)} + global_action_ids = ( + {function.id for function in await Functions.get_global_action_functions()} if ENABLE_PLUGINS else set() + ) + enabled_action_ids = ( + {function.id for function in await Functions.get_functions_by_type('action', active_only=True)} + if ENABLE_PLUGINS + else set() + ) - global_filter_ids = {function.id for function in await Functions.get_global_filter_functions()} - enabled_filter_ids = {function.id for function in await Functions.get_functions_by_type('filter', active_only=True)} + global_filter_ids = ( + {function.id for function in await Functions.get_global_filter_functions()} if ENABLE_PLUGINS else set() + ) + enabled_filter_ids = ( + {function.id for function in await Functions.get_functions_by_type('filter', active_only=True)} + if ENABLE_PLUGINS + else set() + ) custom_models = await Models.get_all_models() @@ -157,8 +169,9 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) if 'info' in model: if 'meta' in model['info']: - action_ids.extend(model['info']['meta'].get('actionIds', [])) - filter_ids.extend(model['info']['meta'].get('filterIds', [])) + if ENABLE_PLUGINS: + action_ids.extend(model['info']['meta'].get('actionIds', [])) + filter_ids.extend(model['info']['meta'].get('filterIds', [])) if 'params' in model['info']: del model['info']['params'] @@ -211,10 +224,10 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) if custom_model.meta: meta = custom_model.meta.model_dump() - if 'actionIds' in meta: + if ENABLE_PLUGINS and 'actionIds' in meta: action_ids.extend(meta['actionIds']) - if 'filterIds' in meta: + if ENABLE_PLUGINS and 'filterIds' in meta: filter_ids.extend(meta['filterIds']) model['action_ids'] = action_ids diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index 53531c4350..4e3c6e5423 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -13,6 +13,7 @@ from typing import Any from open_webui.env import ( ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS, + ENABLE_PLUGINS, OFFLINE_MODE, PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS, @@ -203,6 +204,9 @@ def replace_imports(content): # May the intent of the one who wrote it survive every # import and transformation, as a deed survives the generations. async def load_tool_module_by_id(tool_id, content=None): + if not ENABLE_PLUGINS: + raise RuntimeError('Plugins are disabled by ENABLE_PLUGINS=false') + if content is None: tool = await Tools.get_tool_by_id(tool_id) if not tool: @@ -251,6 +255,9 @@ async def load_tool_module_by_id(tool_id, content=None): async def load_function_module_by_id(function_id: str, content: str | None = None): + if not ENABLE_PLUGINS: + raise RuntimeError('Plugins are disabled by ENABLE_PLUGINS=false') + if content is None: function = await Functions.get_function_by_id(function_id) if not function: @@ -447,6 +454,10 @@ async def install_tool_and_function_dependencies(): and then installing them using pip. Duplicates or similar version specifications are handled by pip as much as possible. """ + if not ENABLE_PLUGINS: + log.info('ENABLE_PLUGINS is disabled, skipping tool and function dependencies.') + return + function_list = await Functions.get_functions(active_only=True) tool_list = await Tools.get_tools() diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 6c33882c50..6fc0f4172f 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -35,6 +35,7 @@ from open_webui.env import ( AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA, ENABLE_FORWARD_USER_INFO_HEADERS, + ENABLE_PLUGINS, FORWARD_SESSION_INFO_HEADER_CHAT_ID, FORWARD_SESSION_INFO_HEADER_MESSAGE_ID, REDIS_KEY_PREFIX, @@ -253,6 +254,9 @@ async def get_updated_tool_function(function: Callable, extra_params: dict): async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extra_params: dict) -> dict[str, dict]: """Load tools for the given tool_ids, checking access control.""" + if not ENABLE_PLUGINS: + return {} + if not tool_ids: return {} @@ -469,6 +473,9 @@ async def get_builtin_tools( Get built-in tools for native function calling. Only returns tools when BOTH the global config is enabled AND the model capability allows it. """ + if not ENABLE_PLUGINS: + return {} + tools_dict = {} builtin_functions = [] features = features or {} From a489e4f219fcfaa96fc22138f154d71b7d6e2f82 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:28:45 -0500 Subject: [PATCH 005/615] refac --- docs/SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index aced4883c9..9669b6a8e6 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -130,7 +130,7 @@ Your remediation guidance can include, for example: > Similar to rule "Default Configuration Testing": If you believe you have found a vulnerability that affects admins and is NOT caused by admin negligence or intentionally malicious actions, > **then we absolutely want to hear about it.** This policy is intended to filter social engineering attacks on admins, malicious plugins being deployed by admins and similar malicious actions, not to discourage legitimate security research. -10. **Tools & Functions Code Execution Is Intended Behavior:** Open WebUI's Tools and Functions feature is **designed** to execute user-provided Python code on the server. This is core, intentional functionality — not a vulnerability (see also 'Threat Model Understanding'). Function creation is **restricted to administrators only**. Tool creation is controlled by the `workspace.tools` permission, which is **disabled by default** for non-admin users and should only be granted to fully trusted users who are equivalent to system administrators in terms of trust. **Granting a user the ability to create Tools is equivalent to giving them shell access to the server**. If an administrator grants this permission to untrusted users, this constitutes intentional misconfiguration and is additionally covered by 'Admin Actions Are Out of Scope'. More generally, **reports describing ANY attack chain that involves Tools or Functions — including but not limited to code execution, file access, network requests, or environment variable access — will be closed as not a vulnerability / intended behavior.** This applies to both direct code execution and frontmatter-based package installation (`pip install`). +10. **Tools & Functions Code Execution Is Intended Behavior:** Open WebUI's Tools and Functions feature is **designed** to execute user-provided Python code on the server. This is core, intentional functionality — not a vulnerability (see also 'Threat Model Understanding'). Function creation is **restricted to administrators only**. Tool creation is controlled by the `workspace.tools` permission, which is **disabled by default** for non-admin users and should only be granted to fully trusted users who are equivalent to system administrators in terms of trust. **Granting a user the ability to create Tools is equivalent to giving them shell access to the server**. If an administrator grants this permission to untrusted users, this constitutes intentional misconfiguration and is additionally covered by 'Admin Actions Are Out of Scope'. Deployments that do not need built-in Python Tools or Functions plugin execution can set `ENABLE_PLUGINS=false`. More generally, **reports describing ANY attack chain that involves Tools or Functions — including but not limited to code execution, file access, network requests, or environment variable access — will be closed as not a vulnerability / intended behavior.** This applies to both direct code execution and frontmatter-based package installation (`pip install`). > [!IMPORTANT] > **For administrators:** Treat the `workspace.tools` permission as **root-equivalent access**. Only grant it to users you would trust with direct access to your server. If you enable this permission for untrusted users, you are accepting the risk of arbitrary code execution on your host. For more details, see our [Plugin Security documentation](https://docs.openwebui.com/features/extensibility/plugin/). From db802e28d359552369830108fe4d23f273145cb3 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:29:07 -0500 Subject: [PATCH 006/615] refac --- src/lib/stores/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 07df7a3736..c0c3c0543e 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -302,6 +302,7 @@ type Config = { enable_admin_analytics: boolean; enable_community_sharing: boolean; enable_memories: boolean; + enable_plugins?: boolean; enable_autocomplete_generation: boolean; enable_direct_connections: boolean; enable_version_update_check: boolean; From 951f96021a970fbd4837a4ee441565c0cf3d2824 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:29:51 -0500 Subject: [PATCH 007/615] refac --- backend/open_webui/tools/builtin.py | 3 ++- src/routes/(app)/workspace/+layout.svelte | 8 ++++++-- src/routes/(app)/workspace/+page.svelte | 4 ++-- src/routes/(app)/workspace/tools/+page.svelte | 12 +++++++++++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index f6631691d3..4315c7ffa7 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -2358,7 +2358,8 @@ async def list_knowledge( ) -> str: """ List knowledge bases, files, and notes attached to the current model. - Use this first to discover what knowledge is available before querying or reading files. + This is an inventory tool only; for questions that require content from + knowledge files, call query_knowledge_files instead. Without knowledge_id: returns KB summaries (name, description, file_count) plus standalone files and notes — no file listing inside KBs. With knowledge_id: includes paginated file listing for that specific KB. diff --git a/src/routes/(app)/workspace/+layout.svelte b/src/routes/(app)/workspace/+layout.svelte index c28053347c..f12479a267 100644 --- a/src/routes/(app)/workspace/+layout.svelte +++ b/src/routes/(app)/workspace/+layout.svelte @@ -2,6 +2,7 @@ import { onMount, getContext } from 'svelte'; import { WEBUI_NAME, + config, showSidebar, functions, user, @@ -33,7 +34,10 @@ !$user?.permissions?.workspace?.prompts ) { goto('/'); - } else if ($page.url.pathname.includes('/tools') && !$user?.permissions?.workspace?.tools) { + } else if ( + $page.url.pathname.includes('/tools') && + (!$config?.features?.enable_plugins || !$user?.permissions?.workspace?.tools) + ) { goto('/'); } else if ($page.url.pathname.includes('/skills') && !$user?.permissions?.workspace?.skills) { goto('/'); @@ -132,7 +136,7 @@ {/if} - {#if $user?.role === 'admin' || $user?.permissions?.workspace?.tools} + {#if $config?.features?.enable_plugins && ($user?.role === 'admin' || $user?.permissions?.workspace?.tools)} import { goto } from '$app/navigation'; - import { user } from '$lib/stores'; + import { config, user } from '$lib/stores'; import { onMount } from 'svelte'; onMount(() => { @@ -11,7 +11,7 @@ goto('/workspace/knowledge'); } else if ($user?.permissions?.workspace?.prompts) { goto('/workspace/prompts'); - } else if ($user?.permissions?.workspace?.tools) { + } else if ($config?.features?.enable_plugins && $user?.permissions?.workspace?.tools) { goto('/workspace/tools'); } else if ($user?.permissions?.workspace?.skills) { goto('/workspace/skills'); diff --git a/src/routes/(app)/workspace/tools/+page.svelte b/src/routes/(app)/workspace/tools/+page.svelte index 86b1b2b7c3..2650bff32f 100644 --- a/src/routes/(app)/workspace/tools/+page.svelte +++ b/src/routes/(app)/workspace/tools/+page.svelte @@ -1,7 +1,17 @@ - +{#if $config?.features?.enable_plugins} + +{/if} From 252e6fd855099e1c880f4def18aa09aedbe1733a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:30:02 -0500 Subject: [PATCH 008/615] refac --- src/routes/(app)/admin/+layout.svelte | 21 ++++++++++++------- src/routes/(app)/admin/functions/+page.svelte | 8 ++++++- .../(app)/admin/functions/create/+page.svelte | 5 +++++ .../(app)/admin/functions/edit/+page.svelte | 5 +++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/routes/(app)/admin/+layout.svelte b/src/routes/(app)/admin/+layout.svelte index 66e106d8d2..b263671868 100644 --- a/src/routes/(app)/admin/+layout.svelte +++ b/src/routes/(app)/admin/+layout.svelte @@ -15,6 +15,11 @@ onMount(async () => { if ($user?.role !== 'admin') { await goto('/'); + } else if ( + !$config?.features?.enable_plugins && + $page.url.pathname.includes('/admin/functions') + ) { + await goto('/admin'); } loaded = true; }); @@ -85,13 +90,15 @@ href="/admin/evaluations">{$i18n.t('Evaluations')} - {$i18n.t('Functions')} + {#if $config?.features?.enable_plugins} + {$i18n.t('Functions')} + {/if} import { onMount } from 'svelte'; - import { functions } from '$lib/stores'; + import { config, functions } from '$lib/stores'; + import { goto } from '$app/navigation'; import { getFunctions } from '$lib/apis/functions'; import Functions from '$lib/components/admin/Functions.svelte'; onMount(async () => { + if (!$config?.features?.enable_plugins) { + await goto('/admin'); + return; + } + await Promise.all([ (async () => { functions.set(await getFunctions(localStorage.token)); diff --git a/src/routes/(app)/admin/functions/create/+page.svelte b/src/routes/(app)/admin/functions/create/+page.svelte index bb13a759fd..bcb6be7326 100644 --- a/src/routes/(app)/admin/functions/create/+page.svelte +++ b/src/routes/(app)/admin/functions/create/+page.svelte @@ -61,6 +61,11 @@ }; onMount(() => { + if (!$config?.features?.enable_plugins) { + goto('/admin'); + return; + } + window.addEventListener('message', async (event) => { if ( !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:9999'].includes( diff --git a/src/routes/(app)/admin/functions/edit/+page.svelte b/src/routes/(app)/admin/functions/edit/+page.svelte index 6a456b983e..9926d1477d 100644 --- a/src/routes/(app)/admin/functions/edit/+page.svelte +++ b/src/routes/(app)/admin/functions/edit/+page.svelte @@ -60,6 +60,11 @@ }; onMount(async () => { + if (!$config?.features?.enable_plugins) { + goto('/admin'); + return; + } + console.log('mounted'); const id = $page.url.searchParams.get('id'); From ef8630d5565230ba6df255f64ea4c6a62d18a0d7 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:31:43 -0500 Subject: [PATCH 009/615] refac --- backend/open_webui/tools/builtin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index 4315c7ffa7..f6631691d3 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -2358,8 +2358,7 @@ async def list_knowledge( ) -> str: """ List knowledge bases, files, and notes attached to the current model. - This is an inventory tool only; for questions that require content from - knowledge files, call query_knowledge_files instead. + Use this first to discover what knowledge is available before querying or reading files. Without knowledge_id: returns KB summaries (name, description, file_count) plus standalone files and notes — no file listing inside KBs. With knowledge_id: includes paginated file listing for that specific KB. From 975f7b868aa35fffbe6bb2c7da6303a90d5a4166 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:32:45 -0500 Subject: [PATCH 010/615] refac --- src/lib/components/chat/Chat.svelte | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 869ab7a880..71568306b8 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -2530,8 +2530,10 @@ } else { toolServerIds.push(serverId); } - } else { + } else if (toolId.startsWith('server:') || $config?.features?.enable_plugins) { toolIds.push(toolId); + } else { + continue; } } @@ -2559,7 +2561,10 @@ files: (files?.length ?? 0) > 0 ? files : undefined, - filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined, + filter_ids: + $config?.features?.enable_plugins && selectedFilterIds.length > 0 + ? selectedFilterIds + : undefined, tool_ids: toolIds.length > 0 ? toolIds : undefined, skill_ids: skillIds.length > 0 ? skillIds : undefined, terminal_id: terminalEnabled ? (activeTerminalId ?? undefined) : undefined, From 5c389ad93f0668d4bab717d14bd189b679338ef2 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:33:30 -0500 Subject: [PATCH 011/615] refac --- backend/open_webui/config.py | 5 +++++ backend/open_webui/routers/chats.py | 4 ++++ backend/open_webui/utils/context_compaction.py | 11 +++++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index f89738b0db..19da0f561e 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2128,6 +2128,10 @@ ENABLE_CONTEXT_COMPACTION = os.getenv('ENABLE_CONTEXT_COMPACTION', 'False').lowe CONTEXT_COMPACTION_TOKEN_THRESHOLD = int(os.getenv('CONTEXT_COMPACTION_TOKEN_THRESHOLD', '80000')) +CONTEXT_COMPACTION_TOKEN_CAP = int( + os.getenv('CONTEXT_COMPACTION_TOKEN_CAP', os.getenv('CONTEXT_COMPACTION_TOKEN_THRESHOLD', '80000')) +) + CONTEXT_COMPACTION_PROMPT_TEMPLATE = os.getenv('CONTEXT_COMPACTION_PROMPT_TEMPLATE', '') TITLE_GENERATION_PROMPT_TEMPLATE = os.getenv('TITLE_GENERATION_PROMPT_TEMPLATE', '') @@ -3034,6 +3038,7 @@ DEFAULT_CONFIG = { 'task.model.external': TASK_MODEL_EXTERNAL, 'chat.context_compaction.enable': ENABLE_CONTEXT_COMPACTION, 'chat.context_compaction.token_threshold': CONTEXT_COMPACTION_TOKEN_THRESHOLD, + 'chat.context_compaction.token_cap': CONTEXT_COMPACTION_TOKEN_CAP, 'chat.context_compaction.prompt_template': CONTEXT_COMPACTION_PROMPT_TEMPLATE, 'task.title.prompt_template': TITLE_GENERATION_PROMPT_TEMPLATE, 'task.tags.prompt_template': TAGS_GENERATION_PROMPT_TEMPLATE, diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 98621ff80e..d9f6e6706b 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -51,6 +51,7 @@ SEARCH_FILTER_PREFIXES = ('tag:', 'folder:', 'pinned:', 'archived:', 'shared:') CHAT_CONFIG_KEYS = { 'ENABLE_CONTEXT_COMPACTION': 'chat.context_compaction.enable', 'CONTEXT_COMPACTION_TOKEN_THRESHOLD': 'chat.context_compaction.token_threshold', + 'CONTEXT_COMPACTION_TOKEN_CAP': 'chat.context_compaction.token_cap', 'CONTEXT_COMPACTION_PROMPT_TEMPLATE': 'chat.context_compaction.prompt_template', } @@ -58,6 +59,7 @@ CHAT_CONFIG_KEYS = { class ChatConfigForm(BaseModel): ENABLE_CONTEXT_COMPACTION: bool CONTEXT_COMPACTION_TOKEN_THRESHOLD: int + CONTEXT_COMPACTION_TOKEN_CAP: int CONTEXT_COMPACTION_PROMPT_TEMPLATE: str @@ -712,11 +714,13 @@ async def get_chat_config(user=Depends(get_admin_user)): @router.post('/config', response_model=ChatConfigForm) async def set_chat_config(form_data: ChatConfigForm, user=Depends(get_admin_user)): threshold = max(1, int(form_data.CONTEXT_COMPACTION_TOKEN_THRESHOLD)) + token_cap = max(1, int(form_data.CONTEXT_COMPACTION_TOKEN_CAP)) await Config.upsert( chat_config_updates( { **form_data.model_dump(), 'CONTEXT_COMPACTION_TOKEN_THRESHOLD': threshold, + 'CONTEXT_COMPACTION_TOKEN_CAP': token_cap, } ) ) diff --git a/backend/open_webui/utils/context_compaction.py b/backend/open_webui/utils/context_compaction.py index 209f56892f..2cdc4c3aef 100644 --- a/backend/open_webui/utils/context_compaction.py +++ b/backend/open_webui/utils/context_compaction.py @@ -53,7 +53,7 @@ async def compact_messages_for_request( return messages, None, False messages, previous_summary = _apply_latest_summary_checkpoint(messages) - token_threshold = _resolve_token_threshold(config['token_threshold'], metadata) + token_threshold = _resolve_token_threshold(config['token_threshold'], config['token_cap'], metadata) if not _exceeds_token_threshold(messages, system_prompt, previous_summary, token_threshold) or len(messages) <= 3: return messages, previous_summary, False @@ -186,11 +186,14 @@ async def _load_config() -> dict: values = await Config.get_many( 'chat.context_compaction.enable', 'chat.context_compaction.token_threshold', + 'chat.context_compaction.token_cap', 'chat.context_compaction.prompt_template', ) + token_threshold = _parse_positive_int(values.get('chat.context_compaction.token_threshold')) or 80000 return { 'enable': bool(values.get('chat.context_compaction.enable', False)), - 'token_threshold': int(values.get('chat.context_compaction.token_threshold', 80000) or 80000), + 'token_threshold': token_threshold, + 'token_cap': _parse_positive_int(values.get('chat.context_compaction.token_cap')) or token_threshold, 'prompt_template': values.get('chat.context_compaction.prompt_template', '') or '', } @@ -203,9 +206,9 @@ def _parse_positive_int(value: Any) -> int | None: return parsed if parsed > 0 else None -def _resolve_token_threshold(global_threshold: int, metadata: dict) -> int: +def _resolve_token_threshold(global_threshold: int, global_cap: int, metadata: dict) -> int: configured_threshold = _parse_positive_int((metadata.get('params') or {}).get('compact_token_threshold')) - return configured_threshold or global_threshold + return min(configured_threshold or global_threshold, global_cap) def _apply_latest_summary_checkpoint(messages: list[dict]) -> tuple[list[dict], str | None]: From 9951fbe5497d98f10bede814bdfc409837725ef2 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:37:04 -0500 Subject: [PATCH 012/615] refac --- .env.example | 2 +- backend/open_webui/utils/tools.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.env.example b/.env.example index c5b0adc4b9..6f674e6e50 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,7 @@ CORS_ALLOW_ORIGIN='*' # Set to false to keep memory tools enabled without adding memory context to the system context. ENABLE_MEMORY_SYSTEM_CONTEXT=true -# Set to false to disable built-in Python Tools and Functions plugin execution surfaces. +# Set to false to disable workspace Tools and Functions. ENABLE_PLUGINS=true # For production you should set this to match the proxy configuration (127.0.0.1) diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 6fc0f4172f..0e468ad313 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -473,9 +473,6 @@ async def get_builtin_tools( Get built-in tools for native function calling. Only returns tools when BOTH the global config is enabled AND the model capability allows it. """ - if not ENABLE_PLUGINS: - return {} - tools_dict = {} builtin_functions = [] features = features or {} From 0a8492b15db057a9c6958f0e2dba312fde0b2711 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:37:12 -0500 Subject: [PATCH 013/615] refac --- backend/open_webui/config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 19da0f561e..3f13ce973d 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2128,9 +2128,8 @@ ENABLE_CONTEXT_COMPACTION = os.getenv('ENABLE_CONTEXT_COMPACTION', 'False').lowe CONTEXT_COMPACTION_TOKEN_THRESHOLD = int(os.getenv('CONTEXT_COMPACTION_TOKEN_THRESHOLD', '80000')) -CONTEXT_COMPACTION_TOKEN_CAP = int( - os.getenv('CONTEXT_COMPACTION_TOKEN_CAP', os.getenv('CONTEXT_COMPACTION_TOKEN_THRESHOLD', '80000')) -) +_CONTEXT_COMPACTION_TOKEN_CAP = os.getenv('CONTEXT_COMPACTION_TOKEN_CAP') +CONTEXT_COMPACTION_TOKEN_CAP = int(_CONTEXT_COMPACTION_TOKEN_CAP) if _CONTEXT_COMPACTION_TOKEN_CAP else None CONTEXT_COMPACTION_PROMPT_TEMPLATE = os.getenv('CONTEXT_COMPACTION_PROMPT_TEMPLATE', '') From c89b6c50bc4e7cf4a1c27de49c3247392dcc688a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:37:17 -0500 Subject: [PATCH 014/615] refac --- backend/open_webui/routers/chats.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index d9f6e6706b..7f80ca1b6a 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -59,7 +59,7 @@ CHAT_CONFIG_KEYS = { class ChatConfigForm(BaseModel): ENABLE_CONTEXT_COMPACTION: bool CONTEXT_COMPACTION_TOKEN_THRESHOLD: int - CONTEXT_COMPACTION_TOKEN_CAP: int + CONTEXT_COMPACTION_TOKEN_CAP: int | None = None CONTEXT_COMPACTION_PROMPT_TEMPLATE: str @@ -106,7 +106,10 @@ def chat_search_snippet(chat: dict, search_text: str, max_length: int = 200) -> async def get_chat_config_values() -> dict: values = await Config.get_many(*CHAT_CONFIG_KEYS.values()) - return {field: values[storage_key] for field, storage_key in CHAT_CONFIG_KEYS.items() if storage_key in values} + config = {field: values[storage_key] for field, storage_key in CHAT_CONFIG_KEYS.items() if storage_key in values} + if config.get('CONTEXT_COMPACTION_TOKEN_CAP') is None: + config['CONTEXT_COMPACTION_TOKEN_CAP'] = config.get('CONTEXT_COMPACTION_TOKEN_THRESHOLD', 80000) + return config def chat_config_updates(data: dict) -> dict: @@ -714,7 +717,7 @@ async def get_chat_config(user=Depends(get_admin_user)): @router.post('/config', response_model=ChatConfigForm) async def set_chat_config(form_data: ChatConfigForm, user=Depends(get_admin_user)): threshold = max(1, int(form_data.CONTEXT_COMPACTION_TOKEN_THRESHOLD)) - token_cap = max(1, int(form_data.CONTEXT_COMPACTION_TOKEN_CAP)) + token_cap = max(1, int(form_data.CONTEXT_COMPACTION_TOKEN_CAP or threshold)) await Config.upsert( chat_config_updates( { From 31996a5acfe1458720fa19f1b9fb4da95749b5e6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:37:31 -0500 Subject: [PATCH 015/615] refac --- .../admin/Settings/Integrations.svelte | 1 - .../admin/Settings/Interface.svelte | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/lib/components/admin/Settings/Integrations.svelte b/src/lib/components/admin/Settings/Integrations.svelte index 81286256f4..a8d835b395 100644 --- a/src/lib/components/admin/Settings/Integrations.svelte +++ b/src/lib/components/admin/Settings/Integrations.svelte @@ -121,7 +121,6 @@ const res = await getToolServerConnections(localStorage.token); servers = res.TOOL_SERVER_CONNECTIONS as ToolServerConnection[]; - // Load terminal server connections try { const terminalRes = await getTerminalServerConnections(localStorage.token); if (terminalRes?.TERMINAL_SERVER_CONNECTIONS) { diff --git a/src/lib/components/admin/Settings/Interface.svelte b/src/lib/components/admin/Settings/Interface.svelte index c825cf3edc..0cbfd2aa04 100644 --- a/src/lib/components/admin/Settings/Interface.svelte +++ b/src/lib/components/admin/Settings/Interface.svelte @@ -40,6 +40,7 @@ let chatConfig = { ENABLE_CONTEXT_COMPACTION: false, CONTEXT_COMPACTION_TOKEN_THRESHOLD: 80000, + CONTEXT_COMPACTION_TOKEN_CAP: 80000, CONTEXT_COMPACTION_PROMPT_TEMPLATE: '' }; @@ -253,6 +254,25 @@ +
+
{$i18n.t('Token Cap')}
+ + + + +
+
{$i18n.t('Context Compaction Prompt')}
From 5e1a337d6e928e0bd38ed6b88851590c5a384a53 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:37:43 -0500 Subject: [PATCH 016/615] refac --- .../admin/Users/Groups/Permissions.svelte | 85 ++++++++++++------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/src/lib/components/admin/Users/Groups/Permissions.svelte b/src/lib/components/admin/Users/Groups/Permissions.svelte index f1d2bc5305..5a6b70e961 100644 --- a/src/lib/components/admin/Users/Groups/Permissions.svelte +++ b/src/lib/components/admin/Users/Groups/Permissions.svelte @@ -6,6 +6,7 @@ import Tooltip from '$lib/components/common/Tooltip.svelte'; import { DEFAULT_PERMISSIONS } from '$lib/constants/permissions'; + import { config } from '$lib/stores'; export let permissions = {}; export let defaultPermissions = {}; @@ -15,6 +16,24 @@ permissions = fillMissingProperties(permissions, DEFAULT_PERMISSIONS); } + $: if ( + $config?.features?.enable_plugins === false && + permissions?.workspace && + (permissions.workspace.tools || + permissions.workspace.tools_import || + permissions.workspace.tools_export) + ) { + permissions = { + ...permissions, + workspace: { + ...permissions.workspace, + tools: false, + tools_import: false, + tools_export: false + } + }; + } + function fillMissingProperties(obj: any, defaults: any) { return { ...defaults, @@ -122,43 +141,45 @@ {/if}
-
- -
- {$i18n.t('Tools Access')} -
- -
+ {#if $config?.features?.enable_plugins} +
+ +
+ {$i18n.t('Tools Access')} +
+ +
- {#if permissions.workspace.tools} -
-
-
- {$i18n.t('Import Tools')} + {#if permissions.workspace.tools} +
+
+
+ {$i18n.t('Import Tools')} +
+
- -
-
-
- {$i18n.t('Export Tools')} +
+
+ {$i18n.t('Export Tools')} +
+
-
-
- {:else if defaultPermissions?.workspace?.tools} -
-
- {$i18n.t('This is a default user permission and will remain enabled.')} + {:else if defaultPermissions?.workspace?.tools} +
+
+ {$i18n.t('This is a default user permission and will remain enabled.')} +
-
- {/if} -
+ {/if} +
+ {/if}
Date: Thu, 9 Jul 2026 17:37:47 -0500 Subject: [PATCH 017/615] refac --- src/lib/i18n/locales/en-US/translation.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index 5351f84f03..13a060bbd7 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -1457,6 +1457,7 @@ "Model can search the web for information": "", "Model Capabilities": "", "Model created successfully!": "", + "Model-specific context compaction thresholds cannot exceed this token limit.": "", "Model filesystem path detected. Model shortname is required for update, cannot continue.": "", "Model Filtering": "", "Model ID": "", @@ -2045,6 +2046,7 @@ "Set embedding model": "", "Set embedding model (e.g. {{model}})": "", "Set reranking model (e.g. {{model}})": "", + "Set a model-specific context compaction token threshold. When set, this overrides the global threshold up to the global cap.": "", "Set the default models that are automatically selected for all users when a new chat is created.": "", "Set the models that are automatically pinned to the sidebar for all users.": "", "Set the number of layers, which will be off-loaded to GPU. Increasing this value can significantly improve performance for models that are optimized for GPU acceleration but may also consume more power and GPU resources.": "", @@ -2303,6 +2305,7 @@ "Toggle status history": "", "Toggle whether current connection is active.": "", "Token": "", + "Token Cap": "", "Token counts are estimates and may not reflect actual API usage": "", "Token Threshold": "", "Tokenizer Model": "", From d3ea8eb7e762bccc27458c03027d8eb9b329223d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:38:19 -0500 Subject: [PATCH 018/615] refac --- backend/open_webui/utils/middleware.py | 5 ++--- docs/SECURITY.md | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 349ad06f1a..d72a4a63a8 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2743,7 +2743,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): # Inject builtin tools for native function calling based on enabled features and model capability. # Only inject when the request originates from the UI (identified by session_id). # API callers don't expect hidden tools; they can explicitly request tools via tool_ids. - if ENABLE_PLUGINS and use_builtin_tools: + if use_builtin_tools: # Add file context to user messages chat_id = metadata.get('chat_id') form_data['messages'] = await add_file_context(form_data.get('messages', []), chat_id, user) @@ -3920,8 +3920,7 @@ async def streaming_chat_response_handler(response, ctx): model_capabilities = model.get('info', {}).get('meta', {}).get('capabilities') or {} builtin_tools_meta = model.get('info', {}).get('meta', {}).get('builtinTools', {}) DETECT_CODE_INTERPRETER = ( - ENABLE_PLUGINS - and bool(features.get('code_interpreter')) + bool(features.get('code_interpreter')) and builtin_tools_meta.get('code_interpreter', True) and await Config.get('code_interpreter.enable') and model_capabilities.get('code_interpreter', True) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 9669b6a8e6..ebe22b47c1 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -130,7 +130,7 @@ Your remediation guidance can include, for example: > Similar to rule "Default Configuration Testing": If you believe you have found a vulnerability that affects admins and is NOT caused by admin negligence or intentionally malicious actions, > **then we absolutely want to hear about it.** This policy is intended to filter social engineering attacks on admins, malicious plugins being deployed by admins and similar malicious actions, not to discourage legitimate security research. -10. **Tools & Functions Code Execution Is Intended Behavior:** Open WebUI's Tools and Functions feature is **designed** to execute user-provided Python code on the server. This is core, intentional functionality — not a vulnerability (see also 'Threat Model Understanding'). Function creation is **restricted to administrators only**. Tool creation is controlled by the `workspace.tools` permission, which is **disabled by default** for non-admin users and should only be granted to fully trusted users who are equivalent to system administrators in terms of trust. **Granting a user the ability to create Tools is equivalent to giving them shell access to the server**. If an administrator grants this permission to untrusted users, this constitutes intentional misconfiguration and is additionally covered by 'Admin Actions Are Out of Scope'. Deployments that do not need built-in Python Tools or Functions plugin execution can set `ENABLE_PLUGINS=false`. More generally, **reports describing ANY attack chain that involves Tools or Functions — including but not limited to code execution, file access, network requests, or environment variable access — will be closed as not a vulnerability / intended behavior.** This applies to both direct code execution and frontmatter-based package installation (`pip install`). +10. **Tools & Functions Code Execution Is Intended Behavior:** Open WebUI's Tools and Functions feature is **designed** to execute user-provided Python code on the server. This is core, intentional functionality — not a vulnerability (see also 'Threat Model Understanding'). Function creation is **restricted to administrators only**. Tool creation is controlled by the `workspace.tools` permission, which is **disabled by default** for non-admin users and should only be granted to fully trusted users who are equivalent to system administrators in terms of trust. **Granting a user the ability to create Tools is equivalent to giving them shell access to the server**. If an administrator grants this permission to untrusted users, this constitutes intentional misconfiguration and is additionally covered by 'Admin Actions Are Out of Scope'. Deployments that do not need `workspace.tools` or Functions plugin execution can set `ENABLE_PLUGINS=false`. More generally, **reports describing ANY attack chain that involves Tools or Functions — including but not limited to code execution, file access, network requests, or environment variable access — will be closed as not a vulnerability / intended behavior.** This applies to both direct code execution and frontmatter-based package installation (`pip install`). > [!IMPORTANT] > **For administrators:** Treat the `workspace.tools` permission as **root-equivalent access**. Only grant it to users you would trust with direct access to your server. If you enable this permission for untrusted users, you are accepting the risk of arbitrary code execution on your host. For more details, see our [Plugin Security documentation](https://docs.openwebui.com/features/extensibility/plugin/). From 5ab012e7ae845a65f919fc25fe855388a188bc86 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:38:52 -0500 Subject: [PATCH 019/615] refac --- src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte b/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte index b2cd92bc7c..139ff84e79 100644 --- a/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte +++ b/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte @@ -150,7 +150,7 @@
Date: Thu, 9 Jul 2026 17:43:13 -0500 Subject: [PATCH 020/615] refac --- src/lib/components/chat/Chat.svelte | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 71568306b8..869ab7a880 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -2530,10 +2530,8 @@ } else { toolServerIds.push(serverId); } - } else if (toolId.startsWith('server:') || $config?.features?.enable_plugins) { - toolIds.push(toolId); } else { - continue; + toolIds.push(toolId); } } @@ -2561,10 +2559,7 @@ files: (files?.length ?? 0) > 0 ? files : undefined, - filter_ids: - $config?.features?.enable_plugins && selectedFilterIds.length > 0 - ? selectedFilterIds - : undefined, + filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined, tool_ids: toolIds.length > 0 ? toolIds : undefined, skill_ids: skillIds.length > 0 ? skillIds : undefined, terminal_id: terminalEnabled ? (activeTerminalId ?? undefined) : undefined, From 247b866330b6a93e12f84a7ad995ac5f50390abb Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:44:40 -0500 Subject: [PATCH 021/615] refac --- .../admin/Users/Groups/Permissions.svelte | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/lib/components/admin/Users/Groups/Permissions.svelte b/src/lib/components/admin/Users/Groups/Permissions.svelte index 5a6b70e961..7df7fc4f8e 100644 --- a/src/lib/components/admin/Users/Groups/Permissions.svelte +++ b/src/lib/components/admin/Users/Groups/Permissions.svelte @@ -16,24 +16,6 @@ permissions = fillMissingProperties(permissions, DEFAULT_PERMISSIONS); } - $: if ( - $config?.features?.enable_plugins === false && - permissions?.workspace && - (permissions.workspace.tools || - permissions.workspace.tools_import || - permissions.workspace.tools_export) - ) { - permissions = { - ...permissions, - workspace: { - ...permissions.workspace, - tools: false, - tools_import: false, - tools_export: false - } - }; - } - function fillMissingProperties(obj: any, defaults: any) { return { ...defaults, From f5b196c060805fd22e1aa1c9f738b60221ef0fd8 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 17:59:17 -0500 Subject: [PATCH 022/615] refac --- backend/open_webui/routers/files.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 33e7dc1220..58cbb5d155 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -201,11 +201,10 @@ async def process_uploaded_file( f'{knowledge_id}: user {user.id} lacks write access' ) else: - await Knowledges.add_file_to_knowledge_by_id( - knowledge_id=knowledge_id, - file_id=file_item.id, - user_id=user.id, - directory_id=file_metadata.get('directory_id'), + # Keep the generic file status stream open until the + # KB-specific vector write and durable link both finish. + await Files.update_file_data_by_id( + file_item.id, {'status': 'processing'}, db=db_session ) await process_file( request, @@ -213,9 +212,19 @@ async def process_uploaded_file( user=user, db=db_session, ) + knowledge_file = await Knowledges.add_file_to_knowledge_by_id( + knowledge_id=knowledge_id, + file_id=file_item.id, + user_id=user.id, + directory_id=file_metadata.get('directory_id'), + db=db_session, + ) + if not knowledge_file: + raise Exception(f'Failed to link file {file_item.id} to knowledge {knowledge_id}') log.info(f'Linked file {file_item.id} to knowledge {knowledge_id}') except Exception as e: log.warning(f'Failed to link file {file_item.id} to knowledge {knowledge_id}: {e}') + raise except Exception as e: log.error(f'Error processing file: {file_item.id}') From 5fe525b8e0cd9014b6686bdcfde8df238523b258 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 18:02:37 -0500 Subject: [PATCH 023/615] refac --- backend/open_webui/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 3f13ce973d..9e74c09b78 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1987,6 +1987,8 @@ ENABLE_CALENDAR = os.getenv('ENABLE_CALENDAR', 'True').lower() == 'true' ENABLE_AUTOMATIONS = os.getenv('ENABLE_AUTOMATIONS', 'True').lower() == 'true' +ENABLE_SUBAGENTS = os.getenv('ENABLE_SUBAGENTS', 'False').lower() == 'true' + AUTOMATION_MAX_COUNT = os.getenv('AUTOMATION_MAX_COUNT', '') AUTOMATION_MIN_INTERVAL = os.getenv('AUTOMATION_MIN_INTERVAL', '') @@ -3019,6 +3021,7 @@ DEFAULT_CONFIG = { 'channels.enable': ENABLE_CHANNELS, 'calendar.enable': ENABLE_CALENDAR, 'automations.enable': ENABLE_AUTOMATIONS, + 'subagents.enable': ENABLE_SUBAGENTS, 'automations.max_count': AUTOMATION_MAX_COUNT, 'automations.min_interval': AUTOMATION_MIN_INTERVAL, 'automations.auth_token_expires_in': AUTOMATION_AUTH_TOKEN_EXPIRES_IN, From 6e030e892b955c5cf3170bc75f243e5a819e7fd2 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 18:28:38 -0500 Subject: [PATCH 024/615] refac --- src/lib/components/chat/Chat.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 869ab7a880..4a5d1e3b96 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -612,10 +612,14 @@ if (event.chat_id === $chatId) { await tick(); + const type = event?.data?.type ?? null; + if (type === 'chat:reload') { + await loadChat(); + return; + } let message = history.messages[event.message_id]; if (message) { - const type = event?.data?.type ?? null; const data = event?.data?.data ?? null; if (type === 'status') { From b854389951ac5440f825091e63bf270a05e5f7ac Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 9 Jul 2026 18:46:40 -0500 Subject: [PATCH 025/615] refac --- src/lib/components/workspace/Models/BuiltinTools.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/components/workspace/Models/BuiltinTools.svelte b/src/lib/components/workspace/Models/BuiltinTools.svelte index fe99005688..83dab2890d 100644 --- a/src/lib/components/workspace/Models/BuiltinTools.svelte +++ b/src/lib/components/workspace/Models/BuiltinTools.svelte @@ -54,10 +54,14 @@ calendar: { label: $i18n.t('Calendar'), description: $i18n.t('List calendars, search, create, update, and delete calendar events') + }, + subagents: { + label: $i18n.t('Sub-agents'), + description: $i18n.t('Delegate focused work to parallel sub-agents') } }; - const allTools = Object.keys(toolLabels); + const allTools = Object.keys(toolLabels) as Array; export let builtinTools: Record = {}; From 42f5c3d6f78415df90027b08a2b968fe7886965f Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:28:39 +0200 Subject: [PATCH 026/615] Merge pull request #26914 from Classic298/srcdoc-embed-prompt-confirmation fix: restore prompt confirmation for sandboxed tool result embeds --- src/lib/components/chat/Chat.svelte | 13 +++++++------ .../components/common/FullHeightIframe.svelte | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 4a5d1e3b96..56bd0660cc 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -113,6 +113,7 @@ import FilesOverlay from './MessageInput/FilesOverlay.svelte'; import NotificationToast from '../NotificationToast.svelte'; import Spinner from '../common/Spinner.svelte'; + import { isEmbedWindow } from '../common/FullHeightIframe.svelte'; import Tooltip from '../common/Tooltip.svelte'; import Sidebar from '../icons/Sidebar.svelte'; import Image from '../common/Image.svelte'; @@ -795,18 +796,18 @@ const onMessageHandler = async (event: { origin: string; + source: unknown; data: { type: string; text: string }; }) => { const isSameOrigin = event.origin === window.origin; const type = event.data?.type; - // Prompt-driving message types let an embedding page control the chat - // input / submission. Cross-origin sources are only trusted when the - // user has explicitly opted in via the "iframe Sandbox Allow Same - // Origin" interface setting (the same toggle that governs whether - // rendered iframes receive `allow-same-origin`). + // Prompt-driving types are trusted only same-origin, from our own embed iframes + // (opaque srcdoc origin, submission still confirmed below) or via explicit opt-in. const promptTypes = ['input:prompt', 'input:prompt:submit', 'action:submit']; - const isTrusted = isSameOrigin || ($settings?.iframeSandboxAllowSameOrigin ?? false); + const isOwnEmbed = isEmbedWindow(event.source); + const isTrusted = + isSameOrigin || isOwnEmbed || ($settings?.iframeSandboxAllowSameOrigin ?? false); // Non-prompt message types are always restricted to same-origin only. if (!isSameOrigin && !promptTypes.includes(type)) { diff --git a/src/lib/components/common/FullHeightIframe.svelte b/src/lib/components/common/FullHeightIframe.svelte index 1bb18943a1..829fcd3b9f 100644 --- a/src/lib/components/common/FullHeightIframe.svelte +++ b/src/lib/components/common/FullHeightIframe.svelte @@ -1,3 +1,10 @@ + + From 0f8846b7fc8c210945366defbd1ed941b039a691 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:29:14 +0200 Subject: [PATCH 027/615] fix: convert SecurityHeadersMiddleware to pure ASGI (#26924) SecurityHeadersMiddleware was the last middleware in the stack still subclassing BaseHTTPMiddleware, after CommitSession, AuthToken, WebsocketUpgradeGuard and Redirect were all moved to pure ASGI in utils/asgi_middleware.py. BaseHTTPMiddleware re-buffers the response body through an anyio task group, which has known issues with streaming and Content-Length-bearing responses (e.g. the FileResponse returned by /api/v1/audio/speech). Reimplement it as a pure-ASGI middleware that stamps the configured security headers onto the http.response.start message via MutableHeaders and forwards all body chunks untouched, matching the pattern already used by its four siblings. set_security_headers() and all its helpers are unchanged. Co-authored-by: classic298 --- backend/open_webui/utils/security_headers.py | 32 +++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/utils/security_headers.py b/backend/open_webui/utils/security_headers.py index 713b33cc6d..090abeea33 100644 --- a/backend/open_webui/utils/security_headers.py +++ b/backend/open_webui/utils/security_headers.py @@ -2,15 +2,33 @@ import os import re from typing import Dict -from fastapi import Request -from starlette.middleware.base import BaseHTTPMiddleware +from starlette.datastructures import MutableHeaders +from starlette.types import ASGIApp, Message, Receive, Scope, Send -class SecurityHeadersMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request: Request, call_next): - response = await call_next(request) - response.headers.update(set_security_headers()) - return response +class SecurityHeadersMiddleware: + """Apply configured security headers to every HTTP response. + + Pure ASGI to avoid BaseHTTPMiddleware's response re-buffering. See + open_webui.utils.asgi_middleware for the rationale. + """ + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope['type'] != 'http': + await self.app(scope, receive, send) + return + + async def send_with_security_headers(message: Message) -> None: + if message['type'] == 'http.response.start': + headers = MutableHeaders(scope=message) + for key, value in set_security_headers().items(): + headers[key] = value + await send(message) + + await self.app(scope, receive, send_with_security_headers) def set_security_headers() -> Dict[str, str]: From f4a6ea9300f130dc2f755d82d935f18160b8f5d2 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:29:29 +0200 Subject: [PATCH 028/615] fix: Milvus multitenancy scalar index creation on Milvus Lite (#26911) Enabling ENABLE_MILVUS_MULTITENANCY_MODE with the default MILVUS_URI (embedded Milvus Lite at DATA_DIR/vector_db/milvus.db) fails on the first embedding write: _create_shared_collection calls collection.create_index(RESOURCE_ID_FIELD) with no index params. A Milvus server auto-selects a scalar index type in that case, but Milvus Lite rejects the call with "create_index missing required 'index_type' parameter", so shared collection creation raises and every embedding write 500s (memory add, file upload, knowledge writes). Keep the parameterless call as the first attempt so behavior on Milvus servers is unchanged, fall back to an explicit INVERTED scalar index, and if that also fails log a warning and continue. The scalar index only accelerates resource_id filters; inserts and filtered queries work without it, so a missing index must not break collection creation. Verified against embedded Milvus Lite: shared collections now create (with the warning), and memory add, file upload and memory query succeed end to end. Against a Milvus server the first attempt is identical to the current code, so nothing changes where it works today. --- .../retrieval/vector/dbs/milvus_multitenancy.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py index 6549c58c62..629acbcb64 100644 --- a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py +++ b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py @@ -146,7 +146,19 @@ class MilvusClient(VectorDBBase): index_params['params'] = {'nlist': MILVUS_IVF_FLAT_NLIST} collection.create_index('vector', index_params) - collection.create_index(RESOURCE_ID_FIELD) + try: + # A Milvus server auto-selects the scalar index type; embedded + # Milvus Lite requires an explicit one. + collection.create_index(RESOURCE_ID_FIELD) + except MilvusException: + try: + collection.create_index(RESOURCE_ID_FIELD, {'index_type': 'INVERTED'}) + except MilvusException as e: + # The index only accelerates resource_id filters; never fail + # collection creation over it. + log.warning( + f'Could not create {RESOURCE_ID_FIELD} index on {mt_collection_name}: {e}' + ) log.info(f'Created shared collection: {mt_collection_name}') return collection From 65a5fad7b97db99d490d81f4e0860282c3a4543c Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:30:18 +0200 Subject: [PATCH 029/615] fix: gate allow-same-origin on the terminal file-preview iframe to prevent same-origin XSS (#26907) The system-terminal HTML file preview (FilePreview.svelte, serveUrl branch) rendered served HTML in an iframe that hardcoded allow-same-origin. The terminal proxy serves the file root-relative (same origin as the app) and injects no CSP, and there is no default global CSP, so script in a previewed HTML file executed in the application's own origin and could read localStorage (the session token), enabling account takeover and, for admin or workspace.functions victims, server-side RCE via Functions. The sibling srcdoc branch already gates allow-same-origin behind the iframeSandboxAllowSameOrigin setting (off by default) and injects a CSP; the serveUrl branch never received that defense. Gate allow-same-origin on the serveUrl branch identically, so by default the preview runs at an opaque origin and its scripts cannot reach the parent context. Legitimate HTML preview rendering is unaffected. Co-authored-by: manus-use --- src/lib/components/chat/FileNav/FilePreview.svelte | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/components/chat/FileNav/FilePreview.svelte b/src/lib/components/chat/FileNav/FilePreview.svelte index fe5075b33e..a5d31f2c00 100644 --- a/src/lib/components/chat/FileNav/FilePreview.svelte +++ b/src/lib/components/chat/FileNav/FilePreview.svelte @@ -400,10 +400,9 @@ {/if}