mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-05 02:41:34 +00:00
refac
Some checks are pending
Python CI / Ruff Format (3.11) (push) Waiting to run
Python CI / Ruff Format (3.12) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Frontend Build / Format & Build (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / notify-helm-charts (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
Frontend Build / Unit Tests (push) Waiting to run
Some checks are pending
Python CI / Ruff Format (3.11) (push) Waiting to run
Python CI / Ruff Format (3.12) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Frontend Build / Format & Build (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / notify-helm-charts (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
Frontend Build / Unit Tests (push) Waiting to run
This commit is contained in:
parent
e797a44806
commit
f50f9e6252
35 changed files with 408 additions and 233 deletions
|
|
@ -25,8 +25,15 @@ ENABLE_KNOWLEDGE_FILE_RETENTION=false
|
|||
# Comma-separated chunk metadata keys to expose to the model alongside retrieved content.
|
||||
RAG_SOURCE_METADATA_KEYS=''
|
||||
|
||||
# Set to false to disable workspace Tools and Functions.
|
||||
# Master switch for internal Tools/Functions and external OpenAPI/MCP/Open Terminal plugins.
|
||||
# All plugin switches require a restart; the master overrides all feature switches.
|
||||
ENABLE_PLUGINS=true
|
||||
# Set false to disable workspace Tools and their dependency installation.
|
||||
ENABLE_TOOLS=true
|
||||
# Set false to disable Functions (filters, pipes, actions, event functions) and their dependencies.
|
||||
ENABLE_FUNCTIONS=true
|
||||
# Set false to disable external tools and terminals, including personal direct connections.
|
||||
ENABLE_TOOL_SERVERS=true
|
||||
|
||||
# For production you should set this to match the proxy configuration (127.0.0.1)
|
||||
FORWARDED_ALLOW_IPS='*'
|
||||
|
|
|
|||
|
|
@ -1192,6 +1192,10 @@ VIEW_FILE_DEFAULT_MAX_CHARS = _int_env('VIEW_FILE_DEFAULT_MAX_CHARS', 10_000)
|
|||
####################################
|
||||
|
||||
ENABLE_PLUGINS = os.getenv('ENABLE_PLUGINS', 'True').lower() == 'true'
|
||||
# Deployment controls: the master switch always overrides all feature switches.
|
||||
ENABLE_TOOLS = ENABLE_PLUGINS and os.getenv('ENABLE_TOOLS', 'True').lower() == 'true'
|
||||
ENABLE_FUNCTIONS = ENABLE_PLUGINS and os.getenv('ENABLE_FUNCTIONS', 'True').lower() == 'true'
|
||||
ENABLE_TOOL_SERVERS = ENABLE_PLUGINS and os.getenv('ENABLE_TOOL_SERVERS', 'True').lower() == 'true'
|
||||
|
||||
ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = (
|
||||
os.getenv('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS', 'True').lower() == 'true'
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import uuid
|
|||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
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.env import ENABLE_FUNCTIONS, VERSION
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.retrieval.web.utils import validate_url
|
||||
from open_webui.utils.webhook import post_webhook
|
||||
|
||||
|
|
@ -1103,7 +1104,7 @@ class SocketSessionEventSink:
|
|||
async def dispatch_event_functions(
|
||||
app: Any, event: Event, request: Any | None = None, extra_function_ids: list[str] | None = None
|
||||
) -> None:
|
||||
if not ENABLE_PLUGINS:
|
||||
if not ENABLE_FUNCTIONS:
|
||||
return
|
||||
|
||||
from open_webui.models.functions import Functions
|
||||
|
|
|
|||
|
|
@ -19,7 +19,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, ENABLE_PLUGINS, GLOBAL_LOG_LEVEL
|
||||
from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, ENABLE_FUNCTIONS, 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,7 +69,7 @@ async def get_function_module_by_id(request: Request, pipe_id: str):
|
|||
|
||||
|
||||
async def get_function_models(request):
|
||||
if not ENABLE_PLUGINS:
|
||||
if not ENABLE_FUNCTIONS:
|
||||
return []
|
||||
|
||||
pipes = await Functions.get_functions_by_type('pipe', active_only=True)
|
||||
|
|
|
|||
|
|
@ -74,9 +74,7 @@ from open_webui.config import (
|
|||
seed_registered_defaults,
|
||||
)
|
||||
from open_webui.constants import ERROR_MESSAGES, TASKS
|
||||
from open_webui.utils.recurrence import RecurrenceEvaluationTimeout
|
||||
from open_webui.env import (
|
||||
USE_SLIM,
|
||||
AIOHTTP_CLIENT_SESSION_SSL,
|
||||
AUDIT_EXCLUDED_PATHS,
|
||||
AUDIT_INCLUDED_PATHS,
|
||||
|
|
@ -88,6 +86,7 @@ from open_webui.env import (
|
|||
ENABLE_COMPRESSION_MIDDLEWARE,
|
||||
ENABLE_CUSTOM_MODEL_FALLBACK,
|
||||
ENABLE_EASTER_EGGS,
|
||||
ENABLE_FUNCTIONS,
|
||||
# OAuth Back-Channel Logout
|
||||
ENABLE_OAUTH_BACKCHANNEL_LOGOUT,
|
||||
ENABLE_OTEL,
|
||||
|
|
@ -98,6 +97,8 @@ from open_webui.env import (
|
|||
ENABLE_SCIM,
|
||||
ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
|
||||
ENABLE_STAR_SESSIONS_MIDDLEWARE,
|
||||
ENABLE_TOOL_SERVERS,
|
||||
ENABLE_TOOLS,
|
||||
ENABLE_VERSION_UPDATE_CHECK,
|
||||
ENABLE_WEBSOCKET_SUPPORT,
|
||||
EXTERNAL_PWA_MANIFEST_URL,
|
||||
|
|
@ -113,6 +114,7 @@ from open_webui.env import (
|
|||
RESET_CONFIG_ON_START,
|
||||
SAFE_MODE,
|
||||
SCIM_TOKEN,
|
||||
USE_SLIM,
|
||||
VERSION,
|
||||
WEBSOCKET_HEARTBEAT_INTERVAL,
|
||||
WEBSOCKET_MANAGER,
|
||||
|
|
@ -271,6 +273,7 @@ from open_webui.utils.oauth import (
|
|||
resolve_oauth_client_info,
|
||||
)
|
||||
from open_webui.utils.plugin import install_tool_and_function_dependencies
|
||||
from open_webui.utils.recurrence import RecurrenceEvaluationTimeout
|
||||
from open_webui.utils.redis import get_redis_client
|
||||
from open_webui.utils.session_pool import cleanup_response, get_client_timeout, get_session, stream_wrapper
|
||||
from open_webui.utils.tool_approval import (
|
||||
|
|
@ -435,7 +438,9 @@ async def lifespan(app: FastAPI):
|
|||
log.warning(f'Failed to pre-fetch models at startup: {e}')
|
||||
|
||||
# Pre-fetch tool server specs so the first request doesn't pay the latency cost
|
||||
if len(await Config.get('tool_server.connections', []) or []) > 0:
|
||||
if ENABLE_TOOL_SERVERS and (
|
||||
await Config.get('tool_server.connections', []) or await Config.get('terminal_server.connections', [])
|
||||
):
|
||||
mock_request = Request(
|
||||
{
|
||||
'type': 'http',
|
||||
|
|
@ -634,7 +639,7 @@ async def initialize_runtime_config(app: FastAPI):
|
|||
migrate_access_control(connection.get('config', {}))
|
||||
await Config.upsert({'tool_server.connections': connections})
|
||||
|
||||
for tool_server_connection in connections:
|
||||
for tool_server_connection in connections if ENABLE_TOOL_SERVERS else []:
|
||||
if tool_server_connection.get('type', 'openapi') == 'mcp':
|
||||
server_id = (tool_server_connection.get('info') or {}).get('id')
|
||||
auth_type = tool_server_connection.get('auth_type', 'none')
|
||||
|
|
@ -2354,8 +2359,12 @@ 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_direct_integrations': config.get('direct.integrations.enable', False),
|
||||
'enable_direct_integrations': ENABLE_TOOL_SERVERS
|
||||
and config.get('direct.integrations.enable', False),
|
||||
'enable_plugins': ENABLE_PLUGINS,
|
||||
'enable_tools': ENABLE_TOOLS,
|
||||
'enable_functions': ENABLE_FUNCTIONS,
|
||||
'enable_tool_servers': ENABLE_TOOL_SERVERS,
|
||||
'enable_folders': config.get('folders.enable'),
|
||||
'folder_max_file_count': config.get('folders.max_file_count'),
|
||||
'enable_channels': config.get('channels.enable'),
|
||||
|
|
@ -2680,6 +2689,9 @@ except Exception as e:
|
|||
|
||||
|
||||
async def register_client(request, client_id: str) -> bool:
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
server_type, server_id = client_id.split(':', 1)
|
||||
|
||||
connection = None
|
||||
|
|
@ -2782,6 +2794,9 @@ async def oauth_client_authorize(
|
|||
user=Depends(get_verified_user),
|
||||
):
|
||||
# ensure_valid_client_registration
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
client = await oauth_client_manager.get_client(client_id)
|
||||
client_info = await oauth_client_manager.get_client_info(client_id)
|
||||
if client is None or client_info is None:
|
||||
|
|
@ -2823,6 +2838,9 @@ async def oauth_client_callback(
|
|||
request: Request,
|
||||
response: Response,
|
||||
):
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
return await oauth_client_manager.handle_callback(
|
||||
request,
|
||||
client_id=client_id,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import aiohttp
|
|||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from mcp.shared.auth import OAuthMetadata
|
||||
from open_webui.config import BannerModel
|
||||
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_TOOL_SERVERS
|
||||
from open_webui.events import EVENTS, publish_event
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.oauth_sessions import OAuthSessions
|
||||
|
|
@ -176,6 +176,9 @@ async def register_oauth_client(
|
|||
type: str | None = None,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
try:
|
||||
oauth_client_id = form_data.client_id
|
||||
if type:
|
||||
|
|
@ -264,7 +267,7 @@ async def set_tool_servers_config(
|
|||
|
||||
await set_tool_servers(request)
|
||||
|
||||
for connection in connections:
|
||||
for connection in connections if ENABLE_TOOL_SERVERS else []:
|
||||
server_type = connection.get('type', 'openapi')
|
||||
if server_type == 'mcp':
|
||||
server_id = (connection.get('info') or {}).get('id')
|
||||
|
|
@ -362,6 +365,9 @@ async def verify_terminal_server_connection(
|
|||
Tries GET {url}/api/v1/policies (orchestrator) then GET {url}/api/config
|
||||
(plain terminal). Returns ``{status: true, type: "orchestrator"|"terminal"}``.
|
||||
"""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
base_url = (form_data.url or '').rstrip('/')
|
||||
if not base_url:
|
||||
raise HTTPException(status_code=400, detail='Terminal server URL is required')
|
||||
|
|
@ -432,6 +438,9 @@ async def put_terminal_server_policy(
|
|||
request: Request, form_data: TerminalServerPolicyForm, user=Depends(get_admin_user)
|
||||
):
|
||||
"""Proxy a policy read or update to an orchestrator terminal server."""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
base_url = (form_data.url or '').rstrip('/')
|
||||
if not base_url:
|
||||
raise HTTPException(status_code=400, detail='Terminal server URL is required')
|
||||
|
|
@ -469,6 +478,9 @@ async def put_terminal_server_lifecycle(
|
|||
request: Request, form_data: TerminalServerLifecycleForm, user=Depends(get_admin_user)
|
||||
):
|
||||
"""Proxy a lifecycle read or update to an orchestrator terminal server."""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
base_url = (form_data.url or '').rstrip('/')
|
||||
if not base_url:
|
||||
raise HTTPException(status_code=400, detail='Terminal server URL is required')
|
||||
|
|
@ -508,6 +520,9 @@ async def refresh_terminal_server_terminals(
|
|||
"""
|
||||
Proxy a terminal refresh request to an orchestrator terminal server.
|
||||
"""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
base_url = (form_data.url or '').rstrip('/')
|
||||
if not base_url:
|
||||
raise HTTPException(status_code=400, detail='Terminal server URL is required')
|
||||
|
|
@ -553,6 +568,9 @@ async def verify_tool_servers_config(request: Request, form_data: ToolServerConn
|
|||
"""
|
||||
Verify the connection to the tool server.
|
||||
"""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
try:
|
||||
if form_data.type == 'mcp':
|
||||
if form_data.auth_type in ('oauth_2.1', 'oauth_2.1_static'):
|
||||
|
|
|
|||
|
|
@ -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, ENABLE_PLUGINS
|
||||
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, ENABLE_FUNCTIONS
|
||||
from open_webui.events import EVENTS, build_event, dispatch_event_functions, publish_event, schedule_webhook_dispatch
|
||||
from open_webui.internal.db import get_async_session
|
||||
from open_webui.models.functions import (
|
||||
|
|
@ -24,8 +24,8 @@ from open_webui.models.functions import (
|
|||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.plugin import (
|
||||
get_function_contents_cache,
|
||||
get_functions_cache,
|
||||
get_function_module_from_cache,
|
||||
get_functions_cache,
|
||||
load_function_module_by_id,
|
||||
replace_imports,
|
||||
resolve_valves_schema_options,
|
||||
|
|
@ -47,7 +47,7 @@ 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:
|
||||
if not ENABLE_FUNCTIONS:
|
||||
return []
|
||||
|
||||
return await Functions.get_functions(db=db)
|
||||
|
|
@ -55,7 +55,7 @@ async def get_functions(user=Depends(get_verified_user), db: AsyncSession = Depe
|
|||
|
||||
@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:
|
||||
if not ENABLE_FUNCTIONS:
|
||||
return []
|
||||
|
||||
return await Functions.get_function_list(db=db)
|
||||
|
|
@ -72,7 +72,7 @@ async def get_functions(
|
|||
user=Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
if not ENABLE_PLUGINS:
|
||||
if not ENABLE_FUNCTIONS:
|
||||
return []
|
||||
|
||||
return await Functions.get_functions(include_valves=include_valves, db=db)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import aiohttp
|
|||
from fastapi import APIRouter, Depends, Request, Response, WebSocket
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from open_webui.config import TERMINAL_PROXY_HEADERS
|
||||
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL
|
||||
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, ENABLE_TOOL_SERVERS
|
||||
from open_webui.events import EVENTS, publish_event
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.groups import Groups
|
||||
|
|
@ -87,6 +87,9 @@ def _sanitize_proxy_path(path: str) -> str | None:
|
|||
@router.get('/')
|
||||
async def list_terminal_servers(request: Request, user=Depends(get_verified_user)):
|
||||
"""Return terminal servers the authenticated user has access to."""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
return []
|
||||
|
||||
connections = await Config.get('terminal_server.connections', []) or []
|
||||
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)}
|
||||
|
||||
|
|
@ -114,6 +117,9 @@ async def proxy_terminal(
|
|||
user=Depends(get_verified_user),
|
||||
):
|
||||
"""Proxy a request to the admin terminal server identified by *server_id*."""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
return JSONResponse({'error': 'Tool servers are disabled'}, status_code=403)
|
||||
|
||||
connections = await Config.get('terminal_server.connections', []) or []
|
||||
connection = next((c for c in connections if c.get('id') == server_id), None)
|
||||
|
||||
|
|
@ -288,6 +294,10 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):
|
|||
|
||||
async def _resolve_terminal_access(ws: WebSocket, server_id: str, token: str):
|
||||
"""Resolve current access for both the handshake and an open terminal session."""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
await ws.close(code=4003, reason='Tool servers are disabled')
|
||||
return None
|
||||
|
||||
try:
|
||||
user = await get_verified_user_by_token(token, getattr(ws.app.state, 'redis', None))
|
||||
if user is None:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ 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, ENABLE_PLUGINS
|
||||
from open_webui.env import (
|
||||
AIOHTTP_CLIENT_SESSION_SSL,
|
||||
AIOHTTP_CLIENT_TIMEOUT,
|
||||
ENABLE_TOOL_SERVERS,
|
||||
ENABLE_TOOLS,
|
||||
)
|
||||
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
|
||||
|
|
@ -33,8 +38,8 @@ from open_webui.utils.access_control import (
|
|||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.plugin import (
|
||||
get_tool_contents_cache,
|
||||
get_tools_cache,
|
||||
get_tool_module_from_cache,
|
||||
get_tools_cache,
|
||||
load_tool_module_by_id,
|
||||
replace_imports,
|
||||
resolve_valves_schema_options,
|
||||
|
|
@ -78,7 +83,7 @@ async def get_tools(
|
|||
)
|
||||
|
||||
# Local Tools
|
||||
if ENABLE_PLUGINS:
|
||||
if ENABLE_TOOLS:
|
||||
tools_cache = get_tools_cache(request)
|
||||
for tool in await Tools.get_tools(
|
||||
defer_content=True,
|
||||
|
|
@ -132,7 +137,7 @@ async def get_tools(
|
|||
)
|
||||
|
||||
# MCP Tool Servers
|
||||
for server in await Config.get('tool_server.connections', []):
|
||||
for server in (await Config.get('tool_server.connections', [])) if ENABLE_TOOL_SERVERS else []:
|
||||
if server.get('type', 'openapi') == 'mcp' and (server.get('config') or {}).get('enable'):
|
||||
info = server.get('info') or {}
|
||||
server_id = info.get('id')
|
||||
|
|
@ -198,7 +203,7 @@ 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:
|
||||
if not ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
bypass_access_control = user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import sys
|
|||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
from open_webui.env import ENABLE_PLUGINS, GLOBAL_LOG_LEVEL
|
||||
from open_webui.env import ENABLE_FUNCTIONS, 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,8 +17,8 @@ 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 not ENABLE_FUNCTIONS:
|
||||
raise Exception('Functions are disabled by ENABLE_PLUGINS or ENABLE_FUNCTIONS')
|
||||
|
||||
if '.' in action_id:
|
||||
action_id, sub_action_id = action_id.split('.')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import inspect
|
||||
import logging
|
||||
|
||||
from open_webui.env import ENABLE_PLUGINS
|
||||
from open_webui.env import ENABLE_FUNCTIONS
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.utils.plugin import get_function_module_from_cache
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ def get_model_filter_ids(model, active_filters):
|
|||
|
||||
|
||||
async def resolve_filter_pipeline(request, model: dict, enabled_filter_ids: list = None):
|
||||
if not ENABLE_PLUGINS:
|
||||
if not ENABLE_FUNCTIONS:
|
||||
return [], []
|
||||
|
||||
active_filters = await get_filter_context(request).get_active_filters()
|
||||
|
|
@ -217,7 +217,7 @@ async def process_filter_functions(
|
|||
form_data,
|
||||
extra_params,
|
||||
):
|
||||
if not ENABLE_PLUGINS:
|
||||
if not ENABLE_FUNCTIONS:
|
||||
return form_data, {}
|
||||
|
||||
skip_files = None
|
||||
|
|
|
|||
|
|
@ -37,10 +37,11 @@ from open_webui.env import (
|
|||
ENABLE_API_OUTLET_FILTERS,
|
||||
ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION,
|
||||
ENABLE_CHAT_RESPONSE_STREAM_INPLACE_APPEND,
|
||||
ENABLE_PLUGINS,
|
||||
ENABLE_FUNCTIONS,
|
||||
ENABLE_QUERIES_CACHE,
|
||||
ENABLE_REALTIME_CHAT_SAVE,
|
||||
ENABLE_RESPONSES_API_STATEFUL,
|
||||
ENABLE_TOOL_SERVERS,
|
||||
GLOBAL_LOG_LEVEL,
|
||||
RAG_SYSTEM_CONTEXT,
|
||||
)
|
||||
|
|
@ -116,8 +117,8 @@ from open_webui.utils.misc import (
|
|||
get_message_list,
|
||||
get_output_text,
|
||||
get_paired_tool_call_ids,
|
||||
get_response_error_detail,
|
||||
get_reasoning_details,
|
||||
get_response_error_detail,
|
||||
get_system_message,
|
||||
is_raster_image_content_type,
|
||||
is_string_allowed,
|
||||
|
|
@ -2331,6 +2332,10 @@ async def connect_mcp_server(
|
|||
|
||||
Returns None if the server is not found or access is denied.
|
||||
"""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
log.debug('MCP resolution skipped: external plugins are disabled')
|
||||
return None
|
||||
|
||||
mcp_server_connection = None
|
||||
for server_connection in await Config.get('tool_server.connections', []):
|
||||
if server_connection.get('type', '') == 'mcp' and (server_connection.get('info') or {}).get('id') == server_id:
|
||||
|
|
@ -2649,8 +2654,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
raise e
|
||||
|
||||
filter_functions = []
|
||||
filter_context = get_filter_context(request) if ENABLE_PLUGINS else None
|
||||
if ENABLE_PLUGINS:
|
||||
filter_context = get_filter_context(request) if ENABLE_FUNCTIONS else None
|
||||
if ENABLE_FUNCTIONS:
|
||||
try:
|
||||
filter_functions = await get_filter_functions(request, model, metadata.get('filter_ids', []))
|
||||
|
||||
|
|
@ -2748,6 +2753,11 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
|
||||
tool_ids = form_data.pop('tool_ids', None)
|
||||
terminal_id = form_data.pop('terminal_id', None)
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
if terminal_id or metadata.get('tool_servers'):
|
||||
log.debug('Excluded external plugins disabled by plugin configuration')
|
||||
terminal_id = None
|
||||
metadata['tool_servers'] = None
|
||||
files = form_data.pop('files', None)
|
||||
form_data.pop('folder_id', None)
|
||||
metadata['terminal_id'] = terminal_id
|
||||
|
|
@ -2927,7 +2937,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
mcp_tools_dict = {}
|
||||
|
||||
if tool_ids:
|
||||
db_tool_ids = []
|
||||
resolved_tool_ids = []
|
||||
for tool_id in tool_ids:
|
||||
if tool_id.startswith('server:mcp:'):
|
||||
try:
|
||||
|
|
@ -2979,13 +2989,13 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
}
|
||||
)
|
||||
continue
|
||||
elif ENABLE_PLUGINS:
|
||||
db_tool_ids.append(tool_id)
|
||||
else:
|
||||
resolved_tool_ids.append(tool_id)
|
||||
|
||||
if db_tool_ids:
|
||||
if resolved_tool_ids:
|
||||
tools_dict = await get_tools(
|
||||
request,
|
||||
db_tool_ids,
|
||||
resolved_tool_ids,
|
||||
user,
|
||||
{
|
||||
**extra_params,
|
||||
|
|
@ -3223,7 +3233,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
}
|
||||
)
|
||||
|
||||
if ENABLE_PLUGINS:
|
||||
if ENABLE_FUNCTIONS:
|
||||
try:
|
||||
form_data, _ = await process_filter_functions(
|
||||
request=request,
|
||||
|
|
@ -3551,7 +3561,7 @@ async def drain_approved_tool_calls(request, form_data, user, model, metadata) -
|
|||
)
|
||||
form_data['messages'] = sanitize_tool_pairs(form_data['messages'])
|
||||
|
||||
if not paused and ENABLE_PLUGINS:
|
||||
if not paused and ENABLE_FUNCTIONS:
|
||||
filter_functions = await get_filter_functions(request, model, metadata.get('filter_ids', []))
|
||||
if filter_functions:
|
||||
filtered_form_data, _ = await process_filter_functions(
|
||||
|
|
@ -4050,7 +4060,7 @@ async def outlet_filter_handler(ctx):
|
|||
is_unsaved_chat = not is_saved_chat_id(chat_id)
|
||||
try:
|
||||
filter_functions = (
|
||||
await get_filter_functions(request, model, metadata.get('filter_ids', [])) if ENABLE_PLUGINS else []
|
||||
await get_filter_functions(request, model, metadata.get('filter_ids', [])) if ENABLE_FUNCTIONS else []
|
||||
)
|
||||
model_id = model.get('id') if isinstance(model, dict) else model
|
||||
models = request.app.state.MODELS
|
||||
|
|
@ -4432,7 +4442,7 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
}
|
||||
|
||||
filter_functions = (
|
||||
await get_filter_functions(request, model, metadata.get('filter_ids', [])) if ENABLE_PLUGINS else []
|
||||
await get_filter_functions(request, model, metadata.get('filter_ids', [])) if ENABLE_FUNCTIONS else []
|
||||
)
|
||||
|
||||
# Standard streaming response handler
|
||||
|
|
|
|||
|
|
@ -8,22 +8,22 @@ from open_webui.config import (
|
|||
BYPASS_ADMIN_ACCESS_CONTROL,
|
||||
DEFAULT_ARENA_MODEL,
|
||||
)
|
||||
from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, ENABLE_PLUGINS, GLOBAL_LOG_LEVEL, REDIS_KEY_PREFIX
|
||||
from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, ENABLE_FUNCTIONS, GLOBAL_LOG_LEVEL, REDIS_KEY_PREFIX
|
||||
from open_webui.functions import get_function_models
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.models import Models
|
||||
from open_webui.utils.chat_variables import get_chat_variables_schema
|
||||
from open_webui.models.users import UserModel
|
||||
from open_webui.routers import ollama, openai
|
||||
from open_webui.socket.utils import RedisDict
|
||||
from open_webui.utils.access_control import has_access, has_base_model_access
|
||||
from open_webui.utils.chat_variables import get_chat_variables_schema
|
||||
from open_webui.utils.json_codec import JSONCodec
|
||||
from open_webui.utils.plugin import (
|
||||
get_functions_cache,
|
||||
get_function_module_from_cache,
|
||||
get_functions_cache,
|
||||
)
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||
|
|
@ -150,7 +150,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
|||
|
||||
# One query per type: the global sets are subsets of the active sets, so
|
||||
# deriving them from the same rows halves the function-table queries.
|
||||
if ENABLE_PLUGINS:
|
||||
if ENABLE_FUNCTIONS:
|
||||
active_actions = await Functions.get_active_function_ids_by_type('action')
|
||||
global_action_ids = {function_id for function_id, is_global in active_actions if is_global}
|
||||
enabled_action_ids = {function_id for function_id, _ in active_actions}
|
||||
|
|
@ -205,7 +205,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
|||
|
||||
if 'info' in model:
|
||||
if 'meta' in model['info']:
|
||||
if ENABLE_PLUGINS:
|
||||
if ENABLE_FUNCTIONS:
|
||||
action_ids.extend(model['info']['meta'].get('actionIds', []))
|
||||
filter_ids.extend(model['info']['meta'].get('filterIds', []))
|
||||
|
||||
|
|
@ -265,10 +265,10 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
|||
if custom_model.meta:
|
||||
meta = custom_model.meta.model_dump()
|
||||
|
||||
if ENABLE_PLUGINS and 'actionIds' in meta:
|
||||
if ENABLE_FUNCTIONS and 'actionIds' in meta:
|
||||
action_ids.extend(meta['actionIds'])
|
||||
|
||||
if ENABLE_PLUGINS and 'filterIds' in meta:
|
||||
if ENABLE_FUNCTIONS and 'filterIds' in meta:
|
||||
filter_ids.extend(meta['filterIds'])
|
||||
|
||||
model['action_ids'] = action_ids
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ from open_webui.env import (
|
|||
AIOHTTP_CLIENT_SESSION_SSL,
|
||||
ENABLE_OAUTH_EMAIL_FALLBACK,
|
||||
ENABLE_OAUTH_ID_TOKEN_COOKIE,
|
||||
ENABLE_TOOL_SERVERS,
|
||||
OAUTH_CLIENT_INFO_ENCRYPTION_KEY,
|
||||
OAUTH_MAX_SESSIONS_PER_USER,
|
||||
REDIS_KEY_PREFIX,
|
||||
|
|
@ -85,8 +86,8 @@ from open_webui.models.users import Users
|
|||
from open_webui.retrieval.web.utils import get_ssrf_safe_session, validate_url
|
||||
from open_webui.utils.auth import (
|
||||
create_token,
|
||||
get_password_hash,
|
||||
get_optional_verified_user_from_request,
|
||||
get_password_hash,
|
||||
get_verified_user_by_id,
|
||||
revoke_user_tokens,
|
||||
)
|
||||
|
|
@ -896,6 +897,9 @@ class OAuthClientManager:
|
|||
Lazy-load an OAuth client from the current TOOL_SERVER_CONNECTIONS
|
||||
config if it hasn't been registered on this node yet.
|
||||
"""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
if client_id in self.clients:
|
||||
return self.clients[client_id]['client']
|
||||
|
||||
|
|
@ -1028,6 +1032,9 @@ class OAuthClientManager:
|
|||
return True
|
||||
|
||||
async def get_client(self, client_id):
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
if client_id not in self.clients:
|
||||
await self.ensure_client_from_config(client_id)
|
||||
|
||||
|
|
@ -1035,6 +1042,9 @@ class OAuthClientManager:
|
|||
return client['client'] if client else None
|
||||
|
||||
async def get_client_info(self, client_id):
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise HTTPException(status_code=403, detail='Tool servers are disabled')
|
||||
|
||||
if client_id not in self.clients:
|
||||
await self.ensure_client_from_config(client_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ from importlib import util
|
|||
from typing import Any
|
||||
|
||||
from open_webui.env import (
|
||||
ENABLE_FUNCTIONS,
|
||||
ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS,
|
||||
ENABLE_PLUGINS,
|
||||
ENABLE_TOOLS,
|
||||
OFFLINE_MODE,
|
||||
PIP_OPTIONS,
|
||||
PIP_PACKAGE_INDEX_OPTIONS,
|
||||
|
|
@ -204,8 +205,8 @@ 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 not ENABLE_TOOLS:
|
||||
raise RuntimeError('Tools are disabled by ENABLE_PLUGINS or ENABLE_TOOLS')
|
||||
|
||||
frontmatter = None
|
||||
if content is None:
|
||||
|
|
@ -257,8 +258,8 @@ 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 not ENABLE_FUNCTIONS:
|
||||
raise RuntimeError('Functions are disabled by ENABLE_PLUGINS or ENABLE_FUNCTIONS')
|
||||
|
||||
frontmatter = None
|
||||
if content is None:
|
||||
|
|
@ -338,6 +339,9 @@ def get_function_contents_cache(request) -> dict:
|
|||
|
||||
|
||||
async def get_tool_module_from_cache(request, tool_id, load_from_db=True):
|
||||
if not ENABLE_TOOLS:
|
||||
raise RuntimeError('Tools are disabled by ENABLE_PLUGINS or ENABLE_TOOLS')
|
||||
|
||||
tools_cache = get_tools_cache(request)
|
||||
tool_contents_cache = get_tool_contents_cache(request)
|
||||
content = None
|
||||
|
|
@ -375,6 +379,9 @@ async def get_tool_module_from_cache(request, tool_id, load_from_db=True):
|
|||
async def get_function_module_from_cache(
|
||||
request, function_id, function: FunctionModel | None = None, load_from_db=True
|
||||
):
|
||||
if not ENABLE_FUNCTIONS:
|
||||
raise RuntimeError('Functions are disabled by ENABLE_PLUGINS or ENABLE_FUNCTIONS')
|
||||
|
||||
functions_cache = get_functions_cache(request)
|
||||
function_contents_cache = get_function_contents_cache(request)
|
||||
content = None
|
||||
|
|
@ -458,12 +465,12 @@ 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.')
|
||||
if not ENABLE_TOOLS and not ENABLE_FUNCTIONS:
|
||||
log.info('Tools and Functions are disabled, skipping their dependencies.')
|
||||
return
|
||||
|
||||
function_list = await Functions.get_functions(active_only=True)
|
||||
tool_list = await Tools.get_tools()
|
||||
function_list = await Functions.get_functions(active_only=True) if ENABLE_FUNCTIONS else []
|
||||
tool_list = await Tools.get_tools() if ENABLE_TOOLS else []
|
||||
|
||||
all_dependencies = ''
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import ntpath
|
|||
import posixpath
|
||||
from urllib.parse import quote
|
||||
|
||||
from open_webui.env import ENABLE_TOOL_SERVERS
|
||||
from open_webui.utils.chat_id import is_saved_chat_id
|
||||
|
||||
TERMINAL_CONTEXT_HEADER = 'X-Terminal-Context-Id'
|
||||
|
|
@ -122,8 +123,10 @@ def terminal_chat_uploads(connection: dict) -> str:
|
|||
|
||||
async def get_terminal_json(request, user, metadata: dict, path: str, extra_params: dict | None = None):
|
||||
"""Read from an admin terminal on the backend or a personal terminal in its browser."""
|
||||
import aiohttp
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
return None
|
||||
|
||||
import aiohttp
|
||||
from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.groups import Groups
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ from open_webui.env import (
|
|||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER,
|
||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA,
|
||||
ENABLE_FORWARD_USER_INFO_HEADERS,
|
||||
ENABLE_PLUGINS,
|
||||
ENABLE_TOOL_SERVERS,
|
||||
ENABLE_TOOLS,
|
||||
FORWARD_SESSION_INFO_HEADER_CHAT_ID,
|
||||
FORWARD_SESSION_INFO_HEADER_MESSAGE_ID,
|
||||
REDIS_KEY_PREFIX,
|
||||
|
|
@ -266,9 +267,17 @@ 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:
|
||||
if not tool_ids:
|
||||
return {}
|
||||
|
||||
enabled_ids = [
|
||||
tool_id
|
||||
for tool_id in tool_ids
|
||||
if (ENABLE_TOOL_SERVERS if tool_id.startswith('server:') else ENABLE_TOOLS)
|
||||
]
|
||||
if len(enabled_ids) != len(tool_ids):
|
||||
log.debug('Excluded tools disabled by plugin configuration')
|
||||
tool_ids = enabled_ids
|
||||
if not tool_ids:
|
||||
return {}
|
||||
|
||||
|
|
@ -278,7 +287,8 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
|
|||
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)}
|
||||
|
||||
# Batch-fetch all DB tools in one query instead of one per tool_id
|
||||
tool_models = await Tools.get_tools_by_ids(tool_ids)
|
||||
local_tool_ids = [tool_id for tool_id in tool_ids if not tool_id.startswith('server:')]
|
||||
tool_models = await Tools.get_tools_by_ids(local_tool_ids) if local_tool_ids else {}
|
||||
|
||||
for tool_id in tool_ids:
|
||||
tool = tool_models.get(tool_id)
|
||||
|
|
@ -1169,6 +1179,9 @@ def convert_openapi_to_tool_payload(openapi_spec):
|
|||
|
||||
|
||||
async def set_tool_servers(request: Request):
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
return []
|
||||
|
||||
try:
|
||||
request.app.state.TOOL_SERVERS = await get_tool_servers_data(await Config.get('tool_server.connections', []))
|
||||
except Exception as e:
|
||||
|
|
@ -1187,6 +1200,9 @@ async def set_tool_servers(request: Request):
|
|||
|
||||
|
||||
async def get_tool_servers(request: Request):
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
return []
|
||||
|
||||
try:
|
||||
tool_servers = None
|
||||
if request.app.state.redis is not None:
|
||||
|
|
@ -1271,6 +1287,9 @@ async def get_terminal_system_prompt(
|
|||
|
||||
async def set_terminal_servers(request: Request):
|
||||
"""Load and cache OpenAPI specs from all TERMINAL_SERVER_CONNECTIONS."""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
return []
|
||||
|
||||
connections = await Config.get('terminal_server.connections', []) or []
|
||||
|
||||
# Build server configs compatible with get_tool_servers_data
|
||||
|
|
@ -1333,6 +1352,9 @@ async def set_terminal_servers(request: Request):
|
|||
|
||||
async def get_terminal_servers(request: Request):
|
||||
"""Return cached terminal server specs, loading if needed."""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
return []
|
||||
|
||||
terminal_servers = None
|
||||
if request.app.state.redis is not None:
|
||||
try:
|
||||
|
|
@ -1368,6 +1390,9 @@ async def get_terminal_tools(
|
|||
- Loads specs from cache
|
||||
- Builds callables that route through the terminal proxy
|
||||
"""
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
return {}
|
||||
|
||||
connections = await Config.get('terminal_server.connections', []) or []
|
||||
connection = next(
|
||||
(terminal_connection for terminal_connection in connections if terminal_connection.get('id') == terminal_id),
|
||||
|
|
@ -1472,6 +1497,9 @@ async def get_terminal_tools(
|
|||
|
||||
|
||||
async def get_tool_server_data(url: str, headers: dict | None) -> dict[str, Any]:
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise RuntimeError('Tool servers are disabled')
|
||||
|
||||
_headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -1520,6 +1548,9 @@ async def get_tool_server_data(url: str, headers: dict | None) -> dict[str, Any]
|
|||
async def get_tool_servers_data(servers: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
# Prepare list of enabled servers along with their original index
|
||||
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
return []
|
||||
|
||||
tasks = []
|
||||
server_entries = []
|
||||
for idx, server in enumerate(servers):
|
||||
|
|
@ -1626,6 +1657,9 @@ async def execute_tool_server(
|
|||
params: dict[str, Any],
|
||||
server_data: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], dict[str, Any | None]]:
|
||||
if not ENABLE_TOOL_SERVERS:
|
||||
raise RuntimeError('Tool servers are disabled')
|
||||
|
||||
error = None
|
||||
try:
|
||||
openapi = server_data.get('openapi', {})
|
||||
|
|
|
|||
|
|
@ -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. <ins>**Granting a user the ability to create Tools is equivalent to giving them shell access to the server**</ins>. 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`).
|
||||
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. <ins>**Granting a user the ability to create Tools is equivalent to giving them shell access to the server**</ins>. 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 internal Tools or Functions execution can set `ENABLE_TOOLS=false` and `ENABLE_FUNCTIONS=false` while keeping external plugins available. `ENABLE_PLUGINS=false` is the master switch and disables both internal and external plugins, including OpenAPI/MCP tool servers and Open Terminal. `ENABLE_TOOL_SERVERS=false` disables only external plugins. All four settings default to `true`, are environment-only, and require a restart; the master always overrides all feature switches. These controls do not disable built-in tools, the code interpreter, external knowledge, or model-provider connections, which have their own controls. 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/).
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { get } from 'svelte/store';
|
||||
import { config } from '$lib/stores';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { convertOpenApiToToolPayload, resolveSchema } from '$lib/utils';
|
||||
import { normalizeTags } from '$lib/utils/tags';
|
||||
|
|
@ -392,6 +394,7 @@ export const getTaskIdsByChatId = async (token: string, chat_id: string) => {
|
|||
};
|
||||
|
||||
export const getToolServerData = async (token: string, url: string) => {
|
||||
if (!get(config)?.features?.enable_tool_servers) throw new Error('Tool servers are disabled');
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${url}`, {
|
||||
|
|
@ -435,6 +438,7 @@ export const getToolServerData = async (token: string, url: string) => {
|
|||
};
|
||||
|
||||
export const getToolServersData = async (servers: object[]) => {
|
||||
if (!get(config)?.features?.enable_tool_servers) return [];
|
||||
return (
|
||||
await Promise.all(
|
||||
servers
|
||||
|
|
@ -546,6 +550,7 @@ export const executeToolServer = async (
|
|||
serverData: { openapi: any; info: any; specs: any },
|
||||
sessionId?: string
|
||||
) => {
|
||||
if (!get(config)?.features?.enable_tool_servers) throw new Error('Tool servers are disabled');
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { get } from 'svelte/store';
|
||||
import { config } from '$lib/stores';
|
||||
export type FileEntry = {
|
||||
name: string;
|
||||
type: 'file' | 'directory';
|
||||
|
|
@ -98,7 +100,7 @@ export const resolveTerminalConnection = (
|
|||
directServers: any[],
|
||||
token: string
|
||||
): TerminalConnection | null => {
|
||||
if (!selector) return null;
|
||||
if (!get(config)?.features?.enable_tool_servers || !selector) return null;
|
||||
if (servers.some((server) => server.id === selector)) {
|
||||
return {
|
||||
selector,
|
||||
|
|
@ -119,6 +121,7 @@ export const terminalRequest = async <T>(
|
|||
path: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> => {
|
||||
if (!get(config)?.features?.enable_tool_servers) throw new Error('Tool servers are disabled');
|
||||
const response = await fetch(`${connection.baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
|
|
@ -131,9 +134,10 @@ export const terminalRequest = async <T>(
|
|||
return response.json();
|
||||
};
|
||||
|
||||
const bearerHeaders = (apiKey: string): Record<string, string> => ({
|
||||
Authorization: `Bearer ${apiKey.trim()}`
|
||||
});
|
||||
const bearerHeaders = (apiKey: string): Record<string, string> => {
|
||||
if (!get(config)?.features?.enable_tool_servers) throw new Error('Tool servers are disabled');
|
||||
return { Authorization: `Bearer ${apiKey.trim()}` };
|
||||
};
|
||||
|
||||
export const joinTerminalPath = (base: string, child: string) => {
|
||||
if (!child) return base;
|
||||
|
|
@ -162,6 +166,7 @@ export type TerminalServer = {
|
|||
};
|
||||
|
||||
export const getTerminalServers = async (token: string): Promise<TerminalServer[]> => {
|
||||
if (!get(config)?.features?.enable_tool_servers) return [];
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/terminals/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
|
|
@ -622,6 +627,7 @@ export const getListeningPorts = async (
|
|||
};
|
||||
|
||||
export const getPortProxyUrl = (baseUrl: string, port: number, path: string = ''): string => {
|
||||
if (!get(config)?.features?.enable_tool_servers) return '';
|
||||
return `${baseUrl.replace(/\/$/, '')}/proxy/${port}/${path}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -186,155 +186,160 @@
|
|||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
{#if servers !== null && connectionsConfig !== null}
|
||||
<AdminSettingSection
|
||||
title={$i18n.t('settings.admin.integrations.sections.tools.title')}
|
||||
first
|
||||
<fieldset
|
||||
disabled={!$config?.features?.enable_tool_servers}
|
||||
class="min-w-0 disabled:opacity-50"
|
||||
>
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('settings.admin.integrations.externalToolServers.label')}
|
||||
<AdminSettingSection
|
||||
title={$i18n.t('settings.admin.integrations.sections.tools.title')}
|
||||
first
|
||||
>
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('settings.admin.integrations.externalToolServers.label')}
|
||||
</div>
|
||||
|
||||
<Tooltip content={$i18n.t('settings.admin.integrations.addConnection.label')}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
on:click={() => {
|
||||
showConnectionModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Tooltip content={$i18n.t('settings.admin.integrations.addConnection.label')}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
on:click={() => {
|
||||
showConnectionModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each servers ?? [] as server, idx}
|
||||
<Connection
|
||||
bind:connection={server}
|
||||
onSubmit={() => {
|
||||
updateHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
servers = (servers ?? []).filter((_, i) => i !== idx);
|
||||
updateHandler();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if (servers ?? []).length === 0}
|
||||
<div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('No tool server connections configured.')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
|
||||
</div>
|
||||
</div>
|
||||
</AdminSettingSection>
|
||||
|
||||
<AdminSettingSection title={$i18n.t('settings.admin.integrations.sections.terminal.title')}>
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('settings.admin.integrations.openTerminal.label')}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each servers ?? [] as server, idx}
|
||||
<Connection
|
||||
bind:connection={server}
|
||||
onSubmit={() => {
|
||||
updateHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
servers = (servers ?? []).filter((_, i) => i !== idx);
|
||||
updateHandler();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Tooltip content={$i18n.t('settings.admin.integrations.addConnection.label')}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
on:click={() => {
|
||||
editTerminalIdx = null;
|
||||
showAddTerminalModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{#if (servers ?? []).length === 0}
|
||||
<div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('No tool server connections configured.')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
|
||||
</div>
|
||||
</div>
|
||||
</AdminSettingSection>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{#each terminalConnections as connection, idx}
|
||||
<div class="flex w-full gap-2 items-center">
|
||||
<Tooltip className="w-full relative" content={''} placement="top-start">
|
||||
<div class="flex w-full">
|
||||
<div
|
||||
class="flex-1 relative flex gap-1.5 items-center {connection?.enabled ===
|
||||
false
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
>
|
||||
<Tooltip
|
||||
content={$i18n.t('settings.admin.integrations.sections.terminal.title')}
|
||||
>
|
||||
<Cloud className="size-4" strokeWidth="1.5" />
|
||||
</Tooltip>
|
||||
<AdminSettingSection title={$i18n.t('settings.admin.integrations.sections.terminal.title')}>
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('settings.admin.integrations.openTerminal.label')}
|
||||
</div>
|
||||
|
||||
<Tooltip content={$i18n.t('settings.admin.integrations.addConnection.label')}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
on:click={() => {
|
||||
editTerminalIdx = null;
|
||||
showAddTerminalModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{#each terminalConnections as connection, idx}
|
||||
<div class="flex w-full gap-2 items-center">
|
||||
<Tooltip className="w-full relative" content={''} placement="top-start">
|
||||
<div class="flex w-full">
|
||||
<div
|
||||
class="outline-hidden w-full bg-transparent text-xs text-gray-700 dark:text-gray-300"
|
||||
class="flex-1 relative flex gap-1.5 items-center {connection?.enabled ===
|
||||
false
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
>
|
||||
{connection.name || connection.url || $i18n.t('New Terminal')}
|
||||
<Tooltip
|
||||
content={$i18n.t('settings.admin.integrations.sections.terminal.title')}
|
||||
>
|
||||
<Cloud className="size-4" strokeWidth="1.5" />
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
class="outline-hidden w-full bg-transparent text-xs text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{connection.name || connection.url || $i18n.t('New Terminal')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
|
||||
<div class="flex gap-1 items-center">
|
||||
<Tooltip content={$i18n.t('Configure')}>
|
||||
<button
|
||||
class="self-center p-1 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition"
|
||||
on:click={() => {
|
||||
editTerminalIdx = idx;
|
||||
showAddTerminalModal = true;
|
||||
}}
|
||||
type="button"
|
||||
<div class="flex gap-1 items-center">
|
||||
<Tooltip content={$i18n.t('Configure')}>
|
||||
<button
|
||||
class="self-center p-1 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition"
|
||||
on:click={() => {
|
||||
editTerminalIdx = idx;
|
||||
showAddTerminalModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Cog6 />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
content={connection?.enabled !== false
|
||||
? $i18n.t('Enabled')
|
||||
: $i18n.t('Disabled')}
|
||||
>
|
||||
<Cog6 />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
content={connection?.enabled !== false
|
||||
? $i18n.t('Enabled')
|
||||
: $i18n.t('Disabled')}
|
||||
>
|
||||
<Switch
|
||||
state={connection?.enabled !== false}
|
||||
on:change={() => {
|
||||
terminalConnections = terminalConnections.map((c, i) =>
|
||||
i === idx ? { ...c, enabled: !(c?.enabled !== false) } : c
|
||||
);
|
||||
saveTerminalServers();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Switch
|
||||
state={connection?.enabled !== false}
|
||||
on:change={() => {
|
||||
terminalConnections = terminalConnections.map((c, i) =>
|
||||
i === idx ? { ...c, enabled: !(c?.enabled !== false) } : c
|
||||
);
|
||||
saveTerminalServers();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if terminalConnections.length === 0}
|
||||
<div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('No terminal connections configured.')}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t(
|
||||
'Connect to Open Terminal instances. Admins and users granted access can use file browsing and terminal tools through these servers.'
|
||||
)}
|
||||
{#if terminalConnections.length === 0}
|
||||
<div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('No terminal connections configured.')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t(
|
||||
'Connect to Open Terminal instances. Admins and users granted access can use file browsing and terminal tools through these servers.'
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
class="mt-0.5 block text-[0.6875rem] text-gray-500 underline hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
href="https://github.com/open-webui/open-terminal"
|
||||
target="_blank">{$i18n.t('Learn more about Open Terminal')} ↗</a
|
||||
>
|
||||
</div>
|
||||
<a
|
||||
class="mt-0.5 block text-[0.6875rem] text-gray-500 underline hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
href="https://github.com/open-webui/open-terminal"
|
||||
target="_blank">{$i18n.t('Learn more about Open Terminal')} ↗</a
|
||||
>
|
||||
</div>
|
||||
</AdminSettingSection>
|
||||
</AdminSettingSection>
|
||||
</fieldset>
|
||||
|
||||
<AdminSettingSection title={$i18n.t('settings.admin.integrations.sections.knowledge.title')}>
|
||||
<ExternalKnowledge />
|
||||
|
|
@ -349,6 +354,7 @@
|
|||
let:labelId
|
||||
>
|
||||
<Switch
|
||||
disabled={!$config?.features?.enable_tool_servers}
|
||||
bind:state={connectionsConfig.ENABLE_DIRECT_INTEGRATIONS}
|
||||
on:change={updateDirectIntegrations}
|
||||
ariaLabelledbyId={labelId}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $config?.features?.enable_plugins}
|
||||
{#if $config?.features?.enable_tools}
|
||||
<div class="flex flex-col w-full">
|
||||
<Tooltip
|
||||
className="flex w-full justify-between my-1"
|
||||
|
|
|
|||
|
|
@ -987,8 +987,9 @@
|
|||
/** Check whether a terminal ID references an available system or direct terminal. */
|
||||
const isTerminalAvailable = (tid: string): boolean => {
|
||||
return (
|
||||
($terminalServers ?? []).some((t) => t.id && t.id === tid) ||
|
||||
($settings?.terminalServers ?? []).some((s) => s.url === tid)
|
||||
$config?.features?.enable_tool_servers === true &&
|
||||
(($terminalServers ?? []).some((t) => t.id && t.id === tid) ||
|
||||
($settings?.terminalServers ?? []).some((s) => s.url === tid))
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -3634,7 +3635,8 @@
|
|||
const skillIds = [...selectedSkillIds];
|
||||
|
||||
// Only send terminal_id if the model has terminal capability enabled
|
||||
const terminalEnabled = model.info?.meta?.capabilities?.terminal ?? true;
|
||||
const terminalEnabled =
|
||||
$config?.features?.enable_tool_servers && (model.info?.meta?.capabilities?.terminal ?? true);
|
||||
const useChatVariablesFallback =
|
||||
!_chatId || $temporaryChatEnabled || isTemporaryChatId(_chatId);
|
||||
|
||||
|
|
@ -3652,21 +3654,30 @@
|
|||
|
||||
files: (files?.length ?? 0) > 0 ? files : undefined,
|
||||
|
||||
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
||||
tool_ids: toolIds.length > 0 ? toolIds : undefined,
|
||||
filter_ids:
|
||||
$config?.features?.enable_functions && selectedFilterIds.length > 0
|
||||
? selectedFilterIds
|
||||
: undefined,
|
||||
tool_ids: toolIds.filter((id) =>
|
||||
id.startsWith('server:')
|
||||
? $config?.features?.enable_tool_servers
|
||||
: $config?.features?.enable_tools
|
||||
),
|
||||
skill_ids: skillIds.length > 0 ? skillIds : undefined,
|
||||
terminal_id: terminalEnabled && $selectedTerminalId ? $selectedTerminalId : undefined,
|
||||
tool_servers: [
|
||||
...($toolServers ?? []).filter(
|
||||
(server, idx) => toolServerIds.includes(idx) || toolServerIds.includes(server?.id)
|
||||
),
|
||||
// Direct terminal servers — always included when enabled (not routed through selectedToolIds)
|
||||
...(terminalEnabled
|
||||
? ($terminalServers ?? [])
|
||||
.filter((server) => !server.id)
|
||||
.map((server) => ({ ...server, is_terminal: true }))
|
||||
: [])
|
||||
],
|
||||
tool_servers: $config?.features?.enable_tool_servers
|
||||
? [
|
||||
...($toolServers ?? []).filter(
|
||||
(server, idx) => toolServerIds.includes(idx) || toolServerIds.includes(server?.id)
|
||||
),
|
||||
// Direct terminal servers — always included when enabled (not routed through selectedToolIds)
|
||||
...(terminalEnabled
|
||||
? ($terminalServers ?? [])
|
||||
.filter((server) => !server.id)
|
||||
.map((server) => ({ ...server, is_terminal: true }))
|
||||
: [])
|
||||
]
|
||||
: [],
|
||||
features: getFeatures(),
|
||||
variables: {
|
||||
...getPromptVariables(
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@
|
|||
chatContextAvailable(selectedSystemTerminal) &&
|
||||
!(chatContextNeedsSavedChat(selectedSystemTerminal) && !isSavedChatId(chatId));
|
||||
$: terminalFilesAvailable = !!(
|
||||
$config?.features?.enable_tool_servers &&
|
||||
$selectedTerminalId &&
|
||||
(selectedSystemTerminalAvailable ||
|
||||
(!selectedSystemTerminal &&
|
||||
|
|
|
|||
|
|
@ -803,6 +803,7 @@
|
|||
$: hasDirectToolServerAccess =
|
||||
$_user?.role === 'admin' || ($_user?.permissions?.features?.direct_tool_servers ?? true);
|
||||
$: showTerminalSelector =
|
||||
$config?.features?.enable_tool_servers &&
|
||||
terminalCapableModels.length > 0 &&
|
||||
(($terminalServers ?? []).some((t) => t.id) ||
|
||||
(hasDirectToolServerAccess &&
|
||||
|
|
|
|||
|
|
@ -360,6 +360,9 @@ type Config = {
|
|||
enable_community_sharing: boolean;
|
||||
enable_memories: boolean;
|
||||
enable_plugins?: boolean;
|
||||
enable_tools?: boolean;
|
||||
enable_functions?: boolean;
|
||||
enable_tool_servers?: boolean;
|
||||
enable_autocomplete_generation: boolean;
|
||||
enable_direct_connections: boolean;
|
||||
enable_direct_integrations?: boolean;
|
||||
|
|
|
|||
|
|
@ -90,6 +90,11 @@
|
|||
};
|
||||
|
||||
const setToolServers = async () => {
|
||||
if (!$config?.features?.enable_tool_servers) {
|
||||
toolServers.set([]);
|
||||
terminalServers.set([]);
|
||||
return;
|
||||
}
|
||||
let toolServersData = await getToolServersData($settings?.toolServers ?? []);
|
||||
toolServersData = toolServersData.filter((data) => {
|
||||
if (!data || data.error) {
|
||||
|
|
@ -234,7 +239,9 @@
|
|||
return;
|
||||
}
|
||||
|
||||
selectedTerminalId.set(localStorage.selectedTerminalId ?? null);
|
||||
selectedTerminalId.set(
|
||||
$config?.features?.enable_tool_servers ? (localStorage.selectedTerminalId ?? null) : null
|
||||
);
|
||||
|
||||
const loadToolServers = setToolServers().catch((e) => {
|
||||
console.error('Failed to load tool servers:', e);
|
||||
|
|
@ -389,6 +396,7 @@
|
|||
|
||||
// Persist selectedTerminalId across page loads
|
||||
selectedTerminalId.subscribe((value) => {
|
||||
if (!$config?.features?.enable_tool_servers) return;
|
||||
if (value === null) {
|
||||
delete localStorage.selectedTerminalId;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
if ($user?.role !== 'admin') {
|
||||
await goto('/', { replaceState: true });
|
||||
} else if (
|
||||
!$config?.features?.enable_plugins &&
|
||||
!$config?.features?.enable_functions &&
|
||||
$page.url.pathname.includes('/admin/functions')
|
||||
) {
|
||||
await goto('/admin', { replaceState: true });
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
href="/admin/evaluations">{$i18n.t('Evaluations')}</a
|
||||
>
|
||||
|
||||
{#if $config?.features?.enable_plugins}
|
||||
{#if $config?.features?.enable_functions}
|
||||
<a
|
||||
draggable="false"
|
||||
class="min-w-fit px-1 text-sm {$page.url.pathname.includes('/admin/functions')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import Functions from '$lib/components/admin/Functions.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
if (!$config?.features?.enable_plugins) {
|
||||
if (!$config?.features?.enable_functions) {
|
||||
await goto('/admin', { replaceState: true });
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
};
|
||||
|
||||
onMount(() => {
|
||||
if (!$config?.features?.enable_plugins) {
|
||||
if (!$config?.features?.enable_functions) {
|
||||
goto('/admin', { replaceState: true });
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
};
|
||||
|
||||
onMount(async () => {
|
||||
if (!$config?.features?.enable_plugins) {
|
||||
if (!$config?.features?.enable_functions) {
|
||||
goto('/admin', { replaceState: true });
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
const canViewPrompts = $user?.role === 'admin' || $user?.permissions?.workspace?.prompts;
|
||||
const canViewSkills = $user?.role === 'admin' || $user?.permissions?.workspace?.skills;
|
||||
const canViewTools =
|
||||
$config?.features?.enable_plugins &&
|
||||
$config?.features?.enable_tools &&
|
||||
($user?.role === 'admin' || $user?.permissions?.workspace?.tools);
|
||||
|
||||
const [modelRes, knowledgeRes, promptRes, skillRes, toolRes] = await Promise.all([
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
goto('/', { replaceState: true });
|
||||
} else if (
|
||||
$page.url.pathname.includes('/tools') &&
|
||||
(!$config?.features?.enable_plugins || !$user?.permissions?.workspace?.tools)
|
||||
(!$config?.features?.enable_tools || !$user?.permissions?.workspace?.tools)
|
||||
) {
|
||||
goto('/', { replaceState: true });
|
||||
} else if ($page.url.pathname.includes('/skills') && !$user?.permissions?.workspace?.skills) {
|
||||
|
|
@ -216,7 +216,7 @@
|
|||
</a>
|
||||
{/if}
|
||||
|
||||
{#if $config?.features?.enable_plugins && ($user?.role === 'admin' || $user?.permissions?.workspace?.tools)}
|
||||
{#if $config?.features?.enable_tools && ($user?.role === 'admin' || $user?.permissions?.workspace?.tools)}
|
||||
<a
|
||||
draggable="false"
|
||||
aria-current={activeWorkspaceSection === 'tools' ? 'page' : null}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
goto('/workspace/knowledge', { replaceState: true });
|
||||
} else if ($user?.permissions?.workspace?.prompts) {
|
||||
goto('/workspace/prompts', { replaceState: true });
|
||||
} else if ($config?.features?.enable_plugins && $user?.permissions?.workspace?.tools) {
|
||||
} else if ($config?.features?.enable_tools && $user?.permissions?.workspace?.tools) {
|
||||
goto('/workspace/tools', { replaceState: true });
|
||||
} else if ($user?.permissions?.workspace?.skills) {
|
||||
goto('/workspace/skills', { replaceState: true });
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
import Tools from '$lib/components/workspace/Tools.svelte';
|
||||
|
||||
onMount(() => {
|
||||
if (!$config?.features?.enable_plugins) {
|
||||
if (!$config?.features?.enable_tools) {
|
||||
goto('/workspace', { replaceState: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $config?.features?.enable_plugins}
|
||||
{#if $config?.features?.enable_tools}
|
||||
<Tools />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -501,6 +501,10 @@
|
|||
};
|
||||
|
||||
const executeTool = async (data, cb, chatId) => {
|
||||
if (!$config?.features?.enable_tool_servers) {
|
||||
cb?.({ error: 'Tool servers are disabled' });
|
||||
return;
|
||||
}
|
||||
const { toolServer, toolServerData, token } = resolveToolServer(data.server?.url);
|
||||
const defaultInline =
|
||||
data?.name === 'display_file' &&
|
||||
|
|
@ -568,10 +572,12 @@
|
|||
event.data.data?.session_id === $socket?.id
|
||||
) {
|
||||
cb?.({
|
||||
connected: [...$connectedUserTerminals.values()].some(
|
||||
(shell) =>
|
||||
shell.terminalId === event.data.data?.terminal_id && shell.chatId === event.chat_id
|
||||
)
|
||||
connected:
|
||||
$config?.features?.enable_tool_servers &&
|
||||
[...$connectedUserTerminals.values()].some(
|
||||
(shell) =>
|
||||
shell.terminalId === event.data.data?.terminal_id && shell.chatId === event.chat_id
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -648,6 +654,7 @@
|
|||
return;
|
||||
} else if (type === 'request:terminal') {
|
||||
try {
|
||||
if (!$config?.features?.enable_tool_servers) throw new Error('Tool servers are disabled');
|
||||
const connection = resolveTerminalConnection(
|
||||
data.terminal_id,
|
||||
[],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue