This commit is contained in:
Timothy Jaeryang Baek
2026-06-17 02:52:35 +02:00
parent e8d55c0a8b
commit 5cdcdbaeec
53 changed files with 7829 additions and 7954 deletions
@@ -114,7 +114,7 @@ async def has_access(
Check if a user has the specified permission using an in-memory access_grants list.
Used for config-driven resources (arena models, tool servers) that store
access control as JSON in ConfigVar rather than in the access_grant DB table.
access control as JSON config rather than in the access_grant DB table.
Semantics:
- None or [] → private (owner-only, deny all)
+2 -1
View File
@@ -39,6 +39,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.security import HTTPAuthorizationCredentials
from open_webui.env import CUSTOM_API_KEY_HEADER
from open_webui.internal.db import ScopedSession
from open_webui.models.config import Config
from open_webui.utils.auth import get_http_authorization_cred
from starlette.datastructures import MutableHeaders
from starlette.requests import Request
@@ -165,7 +166,7 @@ class AuthTokenMiddleware:
token = HTTPAuthorizationCredentials(scheme='Bearer', credentials=api_key)
request.state.token = token
request.state.enable_api_keys = self._fastapi_app.state.config.ENABLE_API_KEYS
request.state.enable_api_keys = await Config.get('auth.enable_api_keys')
async def send_with_timing(message: Message) -> None:
if message['type'] == 'http.response.start':
+8 -5
View File
@@ -35,6 +35,7 @@ from open_webui.env import (
pk,
)
from open_webui.models.auths import Auths
from open_webui.models.config import Config
from open_webui.models.users import Users
from open_webui.utils.access_control import has_permission
from pytz import UTC
@@ -409,12 +410,16 @@ async def get_current_user_by_api_key(request, api_key: str):
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
user_permissions = await Config.get('user.permissions')
enable_endpoint_restrictions = await Config.get('auth.api_key.endpoint_restrictions')
allowed_endpoints = await Config.get('auth.api_key.allowed_endpoints', '')
if not request.state.enable_api_keys or (
user.role != 'admin'
and not await has_permission(
user.id,
'features.api_keys',
request.app.state.config.USER_PERMISSIONS,
user_permissions,
)
):
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED)
@@ -422,10 +427,8 @@ async def get_current_user_by_api_key(request, api_key: str):
# Enforce endpoint restrictions — checked here (not in middleware)
# so it applies regardless of how the API key was transported
# (Authorization header, cookie, x-api-key header, etc.).
if request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS:
allowed_paths = [
path.strip() for path in str(request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS).split(',') if path.strip()
]
if enable_endpoint_restrictions:
allowed_paths = [path.strip() for path in str(allowed_endpoints).split(',') if path.strip()]
request_path = request.scope['path'] # Use raw ASGI path — not spoofable via Host header (CVE-2026-48710)
is_allowed = any(request_path == allowed or request_path.startswith(allowed + '/') for allowed in allowed_paths)
if not is_allowed:
+9 -9
View File
@@ -29,6 +29,7 @@ from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_db
from open_webui.models.automations import AutomationModel, AutomationRuns, Automations
from open_webui.models.chats import ChatForm, Chats
from open_webui.models.config import Config
from open_webui.models.users import Users
from open_webui.utils.task import prompt_template
from starlette.datastructures import Headers
@@ -172,7 +173,7 @@ async def scheduler_worker_loop(app) -> None:
while True:
try:
# ── Automations ──
if getattr(app.state.config, 'ENABLE_AUTOMATIONS', False):
if await Config.get('automations.enable'):
try:
async with get_async_db() as db:
batch = await Automations.claim_due(int(time.time_ns()), limit=10, db=db)
@@ -184,7 +185,7 @@ async def scheduler_worker_loop(app) -> None:
log.exception('Scheduler: automation error')
# ── Calendar Alerts ──
if getattr(app.state.config, 'ENABLE_CALENDAR', False):
if await Config.get('calendar.enable'):
try:
await _check_calendar_alerts(app)
except Exception:
@@ -239,7 +240,7 @@ def _resolve_model_tool_ids(app, model_id: str) -> list[str]:
return list(tool_ids) if tool_ids else []
def _resolve_model_features(app, model_id: str) -> dict:
async def _resolve_model_features(app, model_id: str) -> dict:
"""Read model default features from model config.
The frontend does this in Chat.svelte (model.info.meta.defaultFeatureIds
@@ -256,14 +257,13 @@ def _resolve_model_features(app, model_id: str) -> dict:
return {}
capabilities = meta.get('capabilities', {})
config = app.state.config
features = {}
# code_interpreter is excluded: it requires the frontend event emitter
# and does not work in headless backend execution.
feature_checks = {
'web_search': getattr(config, 'ENABLE_WEB_SEARCH', False),
'image_generation': getattr(config, 'ENABLE_IMAGE_GENERATION', False),
'web_search': await Config.get('rag.web.search.enable'),
'image_generation': await Config.get('image_generation.enable'),
}
for feature_id in default_feature_ids:
@@ -364,7 +364,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
if user.role not in ('user', 'admin') or (
user.role != 'admin'
and not await has_permission(user.id, 'features.automations', app.state.config.USER_PERMISSIONS)
and not await has_permission(user.id, 'features.automations', await Config.get('user.permissions'))
):
await _record_run(automation.id, 'error', error='Owner no longer permitted to run automations')
return
@@ -436,7 +436,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
# Resolve model defaults (frontend does this, backend doesn't)
tool_ids = _resolve_model_tool_ids(app, model_id)
features = _resolve_model_features(app, model_id)
features = await _resolve_model_features(app, model_id)
filter_ids = _resolve_model_filter_ids(app, model_id)
# Resolve terminal from model config
@@ -561,7 +561,7 @@ async def _check_calendar_alerts(app) -> None:
# Send webhook notification if user has one configured
try:
webui_name = getattr(app.state, 'WEBUI_NAME', 'Open WebUI')
enable_user_webhooks = getattr(app.state.config, 'ENABLE_USER_WEBHOOKS', False)
enable_user_webhooks = await Config.get('ui.enable_user_webhooks')
if enable_user_webhooks:
user = await Users.get_user_by_id(event.user_id)
+43 -40
View File
@@ -40,6 +40,7 @@ from open_webui.env import (
RAG_SYSTEM_CONTEXT,
)
from open_webui.models.chats import Chats
from open_webui.models.config import Config
from open_webui.models.folders import Folders
from open_webui.models.functions import Functions
from open_webui.models.models import Models
@@ -980,13 +981,13 @@ async def apply_source_context_to_messages(
if RAG_SYSTEM_CONTEXT:
return add_or_update_system_message(
await rag_template(request.app.state.config.RAG_TEMPLATE, context, user_message),
await rag_template(await Config.get('rag.template'), context, user_message),
messages,
append=True,
)
else:
return add_or_update_user_message(
await rag_template(request.app.state.config.RAG_TEMPLATE, context, user_message),
await rag_template(await Config.get('rag.template'), context, user_message),
messages,
append=False,
)
@@ -1290,8 +1291,8 @@ async def chat_completion_tools_handler(
task_model_id = get_task_model_id(
body['model'],
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
await Config.get('task.model.default'),
await Config.get('task.model.external'),
models,
)
@@ -1301,8 +1302,8 @@ async def chat_completion_tools_handler(
specs = [tool['spec'] for tool in tools.values()]
tools_specs = json.dumps(specs, ensure_ascii=False)
if request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE != '':
template = request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
if await Config.get('task.tools.prompt_template') != '':
template = await Config.get('task.tools.prompt_template')
else:
template = DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
@@ -1792,7 +1793,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra
system_message_content = ''
if len(input_images) > 0 and request.app.state.config.ENABLE_IMAGE_EDIT:
if len(input_images) > 0 and await Config.get('images.edit.enable'):
# Edit image(s)
try:
images = await image_edits(
@@ -1852,7 +1853,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra
else:
# Create image(s)
if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION:
if await Config.get('image_generation.prompt.enable'):
try:
res = await generate_image_prompt(
request,
@@ -2018,17 +2019,17 @@ async def chat_completion_files_handler(
embedding_function=lambda query, prefix: request.app.state.EMBEDDING_FUNCTION(
query, prefix=prefix, user=user
),
k=request.app.state.config.TOP_K,
k=await Config.get('rag.top_k'),
reranking_function=(
(lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents, user=user))
if request.app.state.RERANKING_FUNCTION
else None
),
k_reranker=request.app.state.config.TOP_K_RERANKER,
r=request.app.state.config.RELEVANCE_THRESHOLD,
hybrid_bm25_weight=request.app.state.config.HYBRID_BM25_WEIGHT,
hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
full_context=all_full_context or request.app.state.config.RAG_FULL_CONTEXT,
k_reranker=await Config.get('rag.top_k_reranker'),
r=await Config.get('rag.relevance_threshold'),
hybrid_bm25_weight=await Config.get('rag.hybrid_bm25_weight'),
hybrid_search=await Config.get('rag.enable_hybrid_search'),
full_context=all_full_context or await Config.get('rag.full_context'),
user=user,
)
except Exception as e:
@@ -2269,7 +2270,7 @@ async def connect_mcp_server(
Returns None if the server is not found or access is denied.
"""
mcp_server_connection = None
for server_connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
for server_connection in await Config.get('tool_server.connections', []):
if server_connection.get('type', '') == 'mcp' and server_connection.get('info', {}).get('id') == server_id:
mcp_server_connection = server_connection
break
@@ -2442,8 +2443,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
task_model_id = get_task_model_id(
form_data['model'],
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
await Config.get('task.model.default'),
await Config.get('task.model.external'),
models,
)
@@ -2550,9 +2551,9 @@ async def process_chat_payload(request, form_data, user, metadata, model):
extra_params['__features__'] = features
if features:
if 'voice' in features and features['voice']:
if getattr(request.app.state.config, 'ENABLE_VOICE_MODE_PROMPT', True):
if request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE:
template = request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE
if await Config.get('task.voice.prompt.enable'):
if await Config.get('task.voice.prompt_template'):
template = await Config.get('task.voice.prompt_template')
else:
template = DEFAULT_VOICE_MODE_PROMPT_TEMPLATE
@@ -2577,14 +2578,14 @@ async def process_chat_payload(request, form_data, user, metadata, model):
form_data = await chat_image_generation_handler(request, form_data, extra_params, user)
if 'code_interpreter' in features and features['code_interpreter']:
engine = getattr(request.app.state.config, 'CODE_INTERPRETER_ENGINE', 'pyodide')
engine = await Config.get('code_interpreter.engine', 'pyodide')
# Skip XML-tag prompt injection when native FC is enabled —
# execute_code will be injected as a builtin tool instead
if metadata.get('params', {}).get('function_calling') == 'legacy':
prompt = (
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE
if request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE != ''
await Config.get('code_interpreter.prompt_template')
if await Config.get('code_interpreter.prompt_template') != ''
else DEFAULT_CODE_INTERPRETER_PROMPT
)
@@ -3534,18 +3535,19 @@ async def non_streaming_chat_response_handler(response, ctx):
)
# Send a webhook notification if the user is not active
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
if await Config.get('ui.enable_user_webhooks') and not await Users.is_user_active(user.id):
webhook_url = await Users.get_user_webhook_url_by_id(user.id)
if webhook_url:
webui_url = await Config.get('webui.url')
await post_webhook(
request.app.state.WEBUI_NAME,
webhook_url,
f'{content}\n\n{title} - {request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}',
f'{content}\n\n{title} - {webui_url}/c/{metadata["chat_id"]}',
{
'action': 'chat',
'message': content,
'title': title,
'url': f'{request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}',
'url': f'{webui_url}/c/{metadata["chat_id"]}',
},
)
@@ -3883,14 +3885,14 @@ async def streaming_chat_response_handler(response, ctx):
DETECT_CODE_INTERPRETER = (
bool(features.get('code_interpreter'))
and builtin_tools_meta.get('code_interpreter', True)
and getattr(request.app.state.config, 'ENABLE_CODE_INTERPRETER', True)
and await Config.get('code_interpreter.enable')
and model_capabilities.get('code_interpreter', True)
and (
getattr(user, 'role', None) == 'admin'
or await has_permission(
getattr(user, 'id', ''),
'features.code_interpreter',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
)
)
)
@@ -4784,7 +4786,7 @@ async def streaming_chat_response_handler(response, ctx):
source_context = source_context.strip()
if source_context:
rag_content = await rag_template(
request.app.state.config.RAG_TEMPLATE,
await Config.get('rag.template'),
source_context,
user_message,
)
@@ -4978,7 +4980,7 @@ async def streaming_chat_response_handler(response, ctx):
""")
code = blocking_code + '\n' + code
if request.app.state.config.CODE_INTERPRETER_ENGINE == 'pyodide':
if await Config.get('code_interpreter.engine') == 'pyodide':
ci_output = await event_caller(
{
'type': 'execute:python',
@@ -4990,21 +4992,21 @@ async def streaming_chat_response_handler(response, ctx):
},
}
)
elif request.app.state.config.CODE_INTERPRETER_ENGINE == 'jupyter':
elif await Config.get('code_interpreter.engine') == 'jupyter':
ci_output = await execute_code_jupyter(
request.app.state.config.CODE_INTERPRETER_JUPYTER_URL,
await Config.get('code_interpreter.jupyter.url'),
code,
(
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN
if request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == 'token'
await Config.get('code_interpreter.jupyter.auth_token')
if await Config.get('code_interpreter.jupyter.auth') == 'token'
else None
),
(
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
if request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == 'password'
await Config.get('code_interpreter.jupyter.auth_password')
if await Config.get('code_interpreter.jupyter.auth') == 'password'
else None
),
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
await Config.get('code_interpreter.jupyter.timeout'),
)
else:
ci_output = {'stdout': 'Code interpreter engine not configured.'}
@@ -5148,18 +5150,19 @@ async def streaming_chat_response_handler(response, ctx):
)
# Send a webhook notification if the user is not active
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
if await Config.get('ui.enable_user_webhooks') and not await Users.is_user_active(user.id):
webhook_url = await Users.get_user_webhook_url_by_id(user.id)
if webhook_url:
webui_url = await Config.get('webui.url')
await post_webhook(
request.app.state.WEBUI_NAME,
webhook_url,
f'{content}\n\n{title} - {request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}',
f'{content}\n\n{title} - {webui_url}/c/{metadata["chat_id"]}',
{
'action': 'chat',
'message': content,
'title': title,
'url': f'{request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}',
'url': f'{webui_url}/c/{metadata["chat_id"]}',
},
)
+15 -7
View File
@@ -12,6 +12,7 @@ from open_webui.config import (
from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, 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
from open_webui.models.functions import Functions
from open_webui.models.groups import Groups
from open_webui.models.models import Models
@@ -52,14 +53,15 @@ async def fetch_openai_models(request: Request, user: UserModel = None):
async def get_all_base_models(request: Request, user: UserModel = None):
config = await Config.get_many('openai.enable', 'ollama.enable')
openai_task = (
fetch_openai_models(request, user)
if request.app.state.config.ENABLE_OPENAI_API
if config.get('openai.enable')
else asyncio.sleep(0, result=[])
)
ollama_task = (
fetch_ollama_models(request, user)
if request.app.state.config.ENABLE_OLLAMA_API
if config.get('ollama.enable')
else asyncio.sleep(0, result=[])
)
function_task = get_function_models(request)
@@ -70,10 +72,15 @@ async def get_all_base_models(request: Request, user: UserModel = None):
async def get_all_models(request, refresh: bool = False, user: UserModel = None):
config = await Config.get_many(
'models.base_models_cache',
'evaluation.arena.enable',
'evaluation.arena.models',
)
if (
request.app.state.MODELS
and request.app.state.BASE_MODELS
and (request.app.state.config.ENABLE_BASE_MODELS_CACHE and not refresh)
and (config.get('models.base_models_cache') and not refresh)
):
base_models = request.app.state.BASE_MODELS
else:
@@ -88,9 +95,10 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
return []
# Add arena models
if request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS:
if config.get('evaluation.arena.enable'):
arena_models = []
if len(request.app.state.config.EVALUATION_ARENA_MODELS) > 0:
arena_config = config.get('evaluation.arena.models') or []
if len(arena_config) > 0:
arena_models = [
{
'id': model['id'],
@@ -103,7 +111,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
'owned_by': 'arena',
'arena': True,
}
for model in request.app.state.config.EVALUATION_ARENA_MODELS
for model in arena_config
]
else:
# Add default arena model
@@ -289,7 +297,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
# Apply global model defaults to all models
# Per-model overrides take precedence over global defaults
default_metadata = getattr(request.app.state.config, 'DEFAULT_MODEL_METADATA', None) or {}
default_metadata = await Config.get('models.default_metadata', {}) or {}
if default_metadata:
for model in models:
+140 -89
View File
@@ -1,18 +1,17 @@
import base64
import copy
import fnmatch
import hashlib
import json
import logging
import mimetypes
import re
import secrets
import sys
import time
import urllib
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from types import SimpleNamespace
from typing import Literal, Optional
import aiohttp
@@ -62,7 +61,6 @@ from open_webui.config import (
OAUTH_UPDATE_PICTURE_ON_LOGIN,
OAUTH_USERNAME_CLAIM,
WEBHOOK_URL,
AppConfig,
)
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from open_webui.env import (
@@ -78,6 +76,7 @@ from open_webui.env import (
WEBUI_NAME,
)
from open_webui.models.auths import Auths
from open_webui.models.config import Config
from open_webui.models.groups import GroupForm, GroupModel, Groups, GroupUpdateForm
from open_webui.models.oauth_sessions import OAuthSessions
from open_webui.models.users import Users
@@ -111,31 +110,73 @@ from open_webui.env import GLOBAL_LOG_LEVEL
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
auth_manager_config = AppConfig()
auth_manager_config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
auth_manager_config.ENABLE_OAUTH_SIGNUP = ENABLE_OAUTH_SIGNUP
auth_manager_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE
auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL = OAUTH_MERGE_ACCOUNTS_BY_EMAIL
auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT
auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT = ENABLE_OAUTH_GROUP_MANAGEMENT
auth_manager_config.ENABLE_OAUTH_GROUP_CREATION = ENABLE_OAUTH_GROUP_CREATION
auth_manager_config.OAUTH_GROUP_DEFAULT_SHARE = OAUTH_GROUP_DEFAULT_SHARE
auth_manager_config.OAUTH_BLOCKED_GROUPS = OAUTH_BLOCKED_GROUPS
auth_manager_config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM
auth_manager_config.OAUTH_SUB_CLAIM = OAUTH_SUB_CLAIM
auth_manager_config.OAUTH_GROUPS_CLAIM = OAUTH_GROUPS_CLAIM
auth_manager_config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
auth_manager_config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
auth_manager_config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
auth_manager_config.OAUTH_ALLOWED_ROLES = OAUTH_ALLOWED_ROLES
auth_manager_config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES
auth_manager_config.OAUTH_ALLOWED_DOMAINS = OAUTH_ALLOWED_DOMAINS
auth_manager_config.WEBHOOK_URL = WEBHOOK_URL
auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN
auth_manager_config.OAUTH_UPDATE_NAME_ON_LOGIN = OAUTH_UPDATE_NAME_ON_LOGIN
auth_manager_config.OAUTH_UPDATE_EMAIL_ON_LOGIN = OAUTH_UPDATE_EMAIL_ON_LOGIN
auth_manager_config.OAUTH_AUDIENCE = OAUTH_AUDIENCE
OAUTH_RUNTIME_CONFIG = {
'DEFAULT_USER_ROLE': ('ui.default_user_role', DEFAULT_USER_ROLE),
'ENABLE_OAUTH_SIGNUP': ('oauth.enable_signup', ENABLE_OAUTH_SIGNUP),
'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE': (
'oauth.refresh_token.include_scope',
OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE,
),
'OAUTH_MERGE_ACCOUNTS_BY_EMAIL': (
'oauth.merge_accounts_by_email',
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
),
'ENABLE_OAUTH_ROLE_MANAGEMENT': (
'oauth.enable_role_mapping',
ENABLE_OAUTH_ROLE_MANAGEMENT,
),
'ENABLE_OAUTH_GROUP_MANAGEMENT': (
'oauth.enable_group_mapping',
ENABLE_OAUTH_GROUP_MANAGEMENT,
),
'ENABLE_OAUTH_GROUP_CREATION': (
'oauth.enable_group_creation',
ENABLE_OAUTH_GROUP_CREATION,
),
'OAUTH_GROUP_DEFAULT_SHARE': (
'oauth.group_default_share',
OAUTH_GROUP_DEFAULT_SHARE,
),
'OAUTH_BLOCKED_GROUPS': ('oauth.blocked_groups', OAUTH_BLOCKED_GROUPS),
'OAUTH_ROLES_CLAIM': ('oauth.roles_claim', OAUTH_ROLES_CLAIM),
'OAUTH_SUB_CLAIM': ('oauth.sub_claim', OAUTH_SUB_CLAIM),
'OAUTH_GROUPS_CLAIM': ('oauth.group_claim', OAUTH_GROUPS_CLAIM),
'OAUTH_EMAIL_CLAIM': ('oauth.email_claim', OAUTH_EMAIL_CLAIM),
'OAUTH_PICTURE_CLAIM': ('oauth.picture_claim', OAUTH_PICTURE_CLAIM),
'OAUTH_USERNAME_CLAIM': ('oauth.username_claim', OAUTH_USERNAME_CLAIM),
'OAUTH_ALLOWED_ROLES': ('oauth.allowed_roles', OAUTH_ALLOWED_ROLES),
'OAUTH_ADMIN_ROLES': ('oauth.admin_roles', OAUTH_ADMIN_ROLES),
'OAUTH_ALLOWED_DOMAINS': ('oauth.allowed_domains', OAUTH_ALLOWED_DOMAINS),
'WEBHOOK_URL': ('webhook_url', WEBHOOK_URL),
'JWT_EXPIRES_IN': ('auth.jwt_expiry', JWT_EXPIRES_IN),
'OAUTH_UPDATE_PICTURE_ON_LOGIN': (
'oauth.update_picture_on_login',
OAUTH_UPDATE_PICTURE_ON_LOGIN,
),
'OAUTH_UPDATE_NAME_ON_LOGIN': (
'oauth.update_name_on_login',
OAUTH_UPDATE_NAME_ON_LOGIN,
),
'OAUTH_UPDATE_EMAIL_ON_LOGIN': (
'oauth.update_email_on_login',
OAUTH_UPDATE_EMAIL_ON_LOGIN,
),
'OAUTH_AUDIENCE': ('oauth.audience', OAUTH_AUDIENCE),
}
def _default_value(value):
return getattr(value, 'value', value)
async def get_oauth_runtime_config() -> SimpleNamespace:
keys = [key for key, _default in OAUTH_RUNTIME_CONFIG.values()]
stored = await Config.get_many(*keys)
values = {
name: stored.get(key, _default_value(default))
for name, (key, default) in OAUTH_RUNTIME_CONFIG.items()
}
return SimpleNamespace(**values)
# Conservative default when the provider omits both expires_in and expires_at.
@@ -423,7 +464,8 @@ async def get_oauth_client_info_with_dynamic_client_registration(
oauth_server_metadata = None
oauth_server_metadata_url = None
redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/')
webui_url = await Config.get('webui.url')
redirect_base_url = (str(webui_url or request.base_url)).rstrip('/')
oauth_client_metadata = OAuthClientMetadata(
client_name='Open WebUI',
@@ -549,7 +591,8 @@ async def get_oauth_client_info_with_static_credentials(
oauth_server_metadata = None
oauth_server_metadata_url = None
redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/')
webui_url = await Config.get('webui.url')
redirect_base_url = (str(webui_url or request.base_url)).rstrip('/')
redirect_uri = f'{redirect_base_url}/oauth/clients/{client_id}/callback'
# Discover server metadata (authorization endpoint, token endpoint, scopes, etc.)
@@ -636,7 +679,7 @@ class OAuthClientManager:
'client_secret': oauth_client_info.client_secret,
'client_kwargs': {
'follow_redirects': True,
**({'timeout': int(OAUTH_CLIENT_TIMEOUT.value)} if OAUTH_CLIENT_TIMEOUT.value else {}),
**({'timeout': int(OAUTH_CLIENT_TIMEOUT)} if OAUTH_CLIENT_TIMEOUT else {}),
**({'scope': oauth_client_info.scope} if oauth_client_info.scope else {}),
**(
{'token_endpoint_auth_method': oauth_client_info.token_endpoint_auth_method}
@@ -668,7 +711,7 @@ class OAuthClientManager:
}
return self.clients[client_id]
def ensure_client_from_config(self, client_id):
async def ensure_client_from_config(self, client_id):
"""
Lazy-load an OAuth client from the current TOOL_SERVER_CONNECTIONS
config if it hasn't been registered on this node yet.
@@ -677,7 +720,7 @@ class OAuthClientManager:
return self.clients[client_id]['client']
try:
connections = getattr(self.app.state.config, 'TOOL_SERVER_CONNECTIONS', [])
connections = await Config.get('tool_server.connections', [])
except Exception:
connections = []
@@ -783,22 +826,22 @@ class OAuthClientManager:
return True
def get_client(self, client_id):
async def get_client(self, client_id):
if client_id not in self.clients:
self.ensure_client_from_config(client_id)
await self.ensure_client_from_config(client_id)
client = self.clients.get(client_id)
return client['client'] if client else None
def get_client_info(self, client_id):
async def get_client_info(self, client_id):
if client_id not in self.clients:
self.ensure_client_from_config(client_id)
await self.ensure_client_from_config(client_id)
client = self.clients.get(client_id)
return client['client_info'] if client else None
def get_server_metadata_url(self, client_id):
client = self.get_client(client_id)
async def get_server_metadata_url(self, client_id):
client = await self.get_client(client_id)
if not client:
return None
@@ -881,6 +924,7 @@ class OAuthClientManager:
Returns:
dict: New token data, or None if refresh failed
"""
auth_config = await get_oauth_runtime_config()
client_id = session.provider
token_data = session.token
@@ -889,14 +933,14 @@ class OAuthClientManager:
return None
try:
client = self.get_client(client_id)
client = await self.get_client(client_id)
if not client:
log.error(f'No OAuth client found for provider {client_id}')
return None
token_endpoint = None
async with aiohttp.ClientSession(trust_env=True) as session_http:
async with session_http.get(self.get_server_metadata_url(client_id)) as r:
async with session_http.get(await self.get_server_metadata_url(client_id)) as r:
if r.status == 200:
openid_data = await r.json()
token_endpoint = openid_data.get('token_endpoint')
@@ -913,7 +957,7 @@ class OAuthClientManager:
'client_id': client.client_id,
}
# RFC 8707: include resource indicator so refreshed tokens retain correct audience
client_info = self.get_client_info(client_id)
client_info = await self.get_client_info(client_id)
if client_info and client_info.resource:
refresh_data['resource'] = client_info.resource
@@ -924,7 +968,7 @@ class OAuthClientManager:
if (
hasattr(client, 'client_kwargs')
and client.client_kwargs.get('scope')
and getattr(self.app.state.config, 'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', False)
and auth_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE
):
refresh_data['scope'] = client.client_kwargs['scope']
@@ -957,13 +1001,13 @@ class OAuthClientManager:
return None
async def handle_authorize(self, request, client_id: str) -> RedirectResponse:
client = self.get_client(client_id) or self.ensure_client_from_config(client_id)
client = await self.get_client(client_id)
if client is None:
raise HTTPException(404)
client_info = self.get_client_info(client_id)
client_info = await self.get_client_info(client_id)
if client_info is None:
# ensure_client_from_config registers client_info too
client_info = self.get_client_info(client_id)
# get_client registers client_info too
client_info = await self.get_client_info(client_id)
if client_info is None:
raise HTTPException(404)
@@ -976,13 +1020,13 @@ class OAuthClientManager:
return await client.authorize_redirect(request, redirect_uri_str, **kwargs)
async def handle_callback(self, request, client_id: str, user_id: str, response):
client = self.get_client(client_id) or self.ensure_client_from_config(client_id)
client = await self.get_client(client_id)
if client is None:
raise HTTPException(404)
error_message = None
try:
client_info = self.get_client_info(client_id)
client_info = await self.get_client_info(client_id)
# Note: Do NOT pass client_id/client_secret explicitly here.
# The Authlib client already has these configured during add_client().
@@ -1035,7 +1079,8 @@ class OAuthClientManager:
exc_info=True,
)
redirect_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/')
webui_url = await Config.get('webui.url')
redirect_url = (str(webui_url or request.base_url)).rstrip('/')
if error_message:
log.debug(error_message)
@@ -1202,7 +1247,7 @@ class OAuthManager:
if (
hasattr(client, 'client_kwargs')
and client.client_kwargs.get('scope')
and auth_manager_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE
and auth_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE
):
refresh_data['scope'] = client.client_kwargs['scope']
@@ -1235,6 +1280,7 @@ class OAuthManager:
return None
async def get_user_role(self, user, user_data):
auth_config = await get_oauth_runtime_config()
user_count = await Users.get_num_users()
if user and user_count == 1:
# If the user is the only user, assign the role "admin" - actually repairs role for single user on login
@@ -1246,16 +1292,16 @@ class OAuthManager:
# default role here (not 'admin') — admin promotion happens
# race-safely *after* insert via get_num_users() == 1.
log.debug('First user bootstrap: using default role (admin promotion deferred to post-insert)')
return auth_manager_config.DEFAULT_USER_ROLE
return auth_config.DEFAULT_USER_ROLE
if auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT:
if auth_config.ENABLE_OAUTH_ROLE_MANAGEMENT:
log.debug('Running OAUTH Role management')
oauth_claim = auth_manager_config.OAUTH_ROLES_CLAIM
oauth_allowed_roles = auth_manager_config.OAUTH_ALLOWED_ROLES
oauth_admin_roles = auth_manager_config.OAUTH_ADMIN_ROLES
oauth_claim = auth_config.OAUTH_ROLES_CLAIM
oauth_allowed_roles = auth_config.OAUTH_ALLOWED_ROLES
oauth_admin_roles = auth_config.OAUTH_ADMIN_ROLES
oauth_roles = []
# Default/fallback role if no matching roles are found
role = auth_manager_config.DEFAULT_USER_ROLE
role = auth_config.DEFAULT_USER_ROLE
# Next block extracts the roles from the user data, accepting nested claims of any depth
if oauth_claim and oauth_allowed_roles and oauth_admin_roles:
@@ -1313,7 +1359,7 @@ class OAuthManager:
else:
if not user:
# If role management is disabled, use the default role for new users
role = auth_manager_config.DEFAULT_USER_ROLE
role = auth_config.DEFAULT_USER_ROLE
else:
# If role management is disabled, use the existing role for existing users
role = user.role
@@ -1321,11 +1367,12 @@ class OAuthManager:
return role
async def update_user_groups(self, user, user_data, default_permissions, db=None):
auth_config = await get_oauth_runtime_config()
log.debug('Running OAUTH Group management')
oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM
oauth_claim = auth_config.OAUTH_GROUPS_CLAIM
try:
blocked_groups = json.loads(auth_manager_config.OAUTH_BLOCKED_GROUPS)
blocked_groups = json.loads(auth_config.OAUTH_BLOCKED_GROUPS)
except Exception as e:
log.exception(f'Error loading OAUTH_BLOCKED_GROUPS: {e}')
blocked_groups = []
@@ -1353,7 +1400,7 @@ class OAuthManager:
all_available_groups: list[GroupModel] = await Groups.get_all_groups(db=db)
# Create groups if they don't exist and creation is enabled
if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION:
if auth_config.ENABLE_OAUTH_GROUP_CREATION:
log.debug('Checking for missing groups to create...')
all_group_names = {g.name for g in all_available_groups}
groups_created = False
@@ -1370,7 +1417,7 @@ class OAuthManager:
name=group_name,
description=f"Group '{group_name}' created automatically via OAuth.",
permissions=default_permissions, # Use default permissions from function args
data={'config': {'share': auth_manager_config.OAUTH_GROUP_DEFAULT_SHARE}},
data={'config': {'share': auth_config.OAUTH_GROUP_DEFAULT_SHARE}},
)
# Use determined creator ID (admin or fallback to current user)
created_group = await Groups.insert_new_group(creator_id, new_group_form, db=db)
@@ -1496,6 +1543,7 @@ class OAuthManager:
return '/user.png'
async def handle_login(self, request, provider):
auth_config = await get_oauth_runtime_config()
if provider not in OAUTH_PROVIDERS:
raise HTTPException(404)
# If the provider has a custom redirect URL, use that, otherwise automatically generate one
@@ -1507,14 +1555,15 @@ class OAuthManager:
)
kwargs = {}
if auth_manager_config.OAUTH_AUDIENCE:
kwargs['audience'] = auth_manager_config.OAUTH_AUDIENCE
if auth_config.OAUTH_AUDIENCE:
kwargs['audience'] = auth_config.OAUTH_AUDIENCE
if OAUTH_AUTHORIZE_PARAMS:
kwargs.update(OAUTH_AUTHORIZE_PARAMS)
return await client.authorize_redirect(request, redirect_uri, **kwargs)
async def handle_callback(self, request, provider, response, db=None):
auth_config = await get_oauth_runtime_config()
if provider not in OAUTH_PROVIDERS:
raise HTTPException(404)
@@ -1568,8 +1617,8 @@ class OAuthManager:
id_token_claims = dict(user_data) if user_data else {}
if (
(not user_data)
or (auth_manager_config.OAUTH_EMAIL_CLAIM not in user_data)
or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data)
or (auth_config.OAUTH_EMAIL_CLAIM not in user_data)
or (auth_config.OAUTH_USERNAME_CLAIM not in user_data)
):
user_data: UserInfo = await client.userinfo(token=token)
# Merge back ID token claims that the userinfo endpoint doesn't
@@ -1585,8 +1634,8 @@ class OAuthManager:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
# Extract the "sub" claim, using custom claim if configured
if auth_manager_config.OAUTH_SUB_CLAIM:
sub = user_data.get(auth_manager_config.OAUTH_SUB_CLAIM)
if auth_config.OAUTH_SUB_CLAIM:
sub = user_data.get(auth_config.OAUTH_SUB_CLAIM)
else:
# Fallback to the default sub claim if not configured
sub = user_data.get(OAUTH_PROVIDERS[provider].get('sub_claim', 'sub'))
@@ -1600,7 +1649,7 @@ class OAuthManager:
}
# Email extraction
email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM
email_claim = auth_config.OAUTH_EMAIL_CLAIM
email = user_data.get(email_claim, '')
# We currently mandate that email addresses are provided
if not email:
@@ -1642,8 +1691,8 @@ class OAuthManager:
email = email.lower()
# If allowed domains are configured, check if the email domain is in the list
if (
'*' not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
and email.split('@')[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
'*' not in auth_config.OAUTH_ALLOWED_DOMAINS
and email.split('@')[-1] not in auth_config.OAUTH_ALLOWED_DOMAINS
):
log.warning(f'OAuth callback failed, e-mail domain is not in the list of allowed domains: {user_data}')
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
@@ -1652,7 +1701,7 @@ class OAuthManager:
user = await Users.get_user_by_oauth_sub(provider, sub, db=db)
if not user:
# If the user does not exist, check if merging is enabled
if auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
if auth_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
# Check if the user exists by email
user = await Users.get_user_by_email(email, db=db)
if user:
@@ -1667,8 +1716,8 @@ class OAuthManager:
# to avoid problems with the ENABLE_OAUTH_GROUP_MANAGEMENT check below
user.role = determined_role
if auth_manager_config.OAUTH_UPDATE_NAME_ON_LOGIN:
username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
if auth_config.OAUTH_UPDATE_NAME_ON_LOGIN:
username_claim = auth_config.OAUTH_USERNAME_CLAIM
if username_claim:
new_name = user_data.get(username_claim)
if new_name and new_name != user.name:
@@ -1676,8 +1725,8 @@ class OAuthManager:
user.name = new_name
log.debug(f'Updated name for user {user.email}')
if auth_manager_config.OAUTH_UPDATE_EMAIL_ON_LOGIN:
email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM
if auth_config.OAUTH_UPDATE_EMAIL_ON_LOGIN:
email_claim = auth_config.OAUTH_EMAIL_CLAIM
if email_claim:
new_email = user_data.get(email_claim)
if new_email and new_email.lower() != user.email.lower():
@@ -1692,8 +1741,8 @@ class OAuthManager:
log.debug(f'Updated email for user {user.id}')
# Update profile picture if enabled and different from current
if auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN:
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
if auth_config.OAUTH_UPDATE_PICTURE_ON_LOGIN:
picture_claim = auth_config.OAUTH_PICTURE_CLAIM
if picture_claim:
new_picture_url = user_data.get(
picture_claim,
@@ -1707,13 +1756,13 @@ class OAuthManager:
log.debug(f'Updated profile picture for user {user.email}')
else:
# If the user does not exist, check if signups are enabled
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
if auth_config.ENABLE_OAUTH_SIGNUP:
# Check if an existing user with the same email already exists
existing_user = await Users.get_user_by_email(email, db=db)
if existing_user:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
picture_claim = auth_config.OAUTH_PICTURE_CLAIM
if picture_claim:
picture_url = user_data.get(
picture_claim,
@@ -1722,7 +1771,7 @@ class OAuthManager:
picture_url = await self._process_picture_url(picture_url, token.get('access_token'))
else:
picture_url = '/user.png'
username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
username_claim = auth_config.OAUTH_USERNAME_CLAIM
name = user_data.get(username_claim)
if not name:
@@ -1749,10 +1798,10 @@ class OAuthManager:
await Users.update_user_role_by_id(user.id, 'admin', db=db)
user = await Users.get_user_by_id(user.id, db=db)
if auth_manager_config.WEBHOOK_URL:
if auth_config.WEBHOOK_URL:
await post_webhook(
WEBUI_NAME,
auth_manager_config.WEBHOOK_URL,
auth_config.WEBHOOK_URL,
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{
'action': 'signup',
@@ -1761,7 +1810,8 @@ class OAuthManager:
},
)
await apply_default_group_assignment(request.app.state.config.DEFAULT_GROUP_ID, user.id, db=db)
default_group_id = await Config.get('ui.default_group_id')
await apply_default_group_assignment(default_group_id, user.id, db=db)
else:
raise HTTPException(
@@ -1771,13 +1821,13 @@ class OAuthManager:
jwt_token = create_token(
data={'id': user.id},
expires_delta=parse_duration(auth_manager_config.JWT_EXPIRES_IN),
expires_delta=parse_duration(auth_config.JWT_EXPIRES_IN),
)
if auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT:
if auth_config.ENABLE_OAUTH_GROUP_MANAGEMENT:
await self.update_user_groups(
user=user,
user_data=user_data,
default_permissions=request.app.state.config.USER_PERMISSIONS,
default_permissions=await Config.get('user.permissions'),
db=db,
)
@@ -1789,7 +1839,8 @@ class OAuthManager:
else ERROR_MESSAGES.DEFAULT('Error during OAuth process')
)
redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/')
webui_url = await Config.get('webui.url')
redirect_base_url = (str(webui_url or request.base_url)).rstrip('/')
redirect_url = f'{redirect_base_url}/auth'
if error_message:
@@ -1799,7 +1850,7 @@ class OAuthManager:
response = RedirectResponse(url=redirect_url, headers=response.headers)
# Compute cookie expiry from JWT lifetime
expires_delta = parse_duration(auth_manager_config.JWT_EXPIRES_IN)
expires_delta = parse_duration(auth_config.JWT_EXPIRES_IN)
cookie_max_age = int(expires_delta.total_seconds()) if expires_delta else None
# Set the cookie token
+24 -13
View File
@@ -42,6 +42,7 @@ from open_webui.env import (
REDIS_KEY_PREFIX,
)
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.tools import Tools
from open_webui.models.users import UserModel
@@ -345,7 +346,7 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
continue
tool_server_idx = tool_server_data.get('idx', 0)
connections = request.app.state.config.TOOL_SERVER_CONNECTIONS
connections = await Config.get('tool_server.connections', [])
if tool_server_idx >= len(connections):
log.warning(
f'Tool server index {tool_server_idx} out of range '
@@ -451,6 +452,16 @@ async def get_builtin_tools(
# Helper to check user-level feature permission (admins always pass)
user = extra_params.get('__user__', {})
config = await Config.get_many(
'rag.web.search.enable',
'image_generation.enable',
'images.edit.enable',
'code_interpreter.enable',
'notes.enable',
'channels.enable',
'automations.enable',
'calendar.enable',
)
async def has_user_permission(feature_key: str) -> bool:
if user.get('role') == 'admin':
@@ -458,7 +469,7 @@ async def get_builtin_tools(
return await has_permission(
user.get('id', ''),
f'features.{feature_key}',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
)
# Time utilities - available for date calculations
@@ -533,7 +544,7 @@ async def get_builtin_tools(
# Add web search tools if builtin category enabled AND enabled globally AND model has web_search capability
if (
is_builtin_tool_enabled('web_search')
and getattr(request.app.state.config, 'ENABLE_WEB_SEARCH', False)
and config.get('rag.web.search.enable')
and get_model_capability('web_search')
and features.get('web_search')
and await has_user_permission('web_search')
@@ -543,7 +554,7 @@ async def get_builtin_tools(
# Add image generation/edit tools if builtin category enabled AND enabled globally AND model has image_generation capability
if (
is_builtin_tool_enabled('image_generation')
and getattr(request.app.state.config, 'ENABLE_IMAGE_GENERATION', False)
and config.get('image_generation.enable')
and get_model_capability('image_generation')
and features.get('image_generation')
and await has_user_permission('image_generation')
@@ -551,7 +562,7 @@ async def get_builtin_tools(
builtin_functions.append(generate_image)
if (
is_builtin_tool_enabled('image_generation')
and getattr(request.app.state.config, 'ENABLE_IMAGE_EDIT', False)
and config.get('images.edit.enable')
and get_model_capability('image_generation')
and features.get('image_generation')
and await has_user_permission('image_generation')
@@ -561,7 +572,7 @@ async def get_builtin_tools(
# Add code interpreter tool if builtin category enabled AND enabled globally AND model has code_interpreter capability
if (
is_builtin_tool_enabled('code_interpreter')
and getattr(request.app.state.config, 'ENABLE_CODE_INTERPRETER', True)
and config.get('code_interpreter.enable')
and get_model_capability('code_interpreter')
and features.get('code_interpreter')
and await has_user_permission('code_interpreter')
@@ -571,7 +582,7 @@ async def get_builtin_tools(
# Notes tools - search, view, create, and update user's notes
if (
is_builtin_tool_enabled('notes')
and getattr(request.app.state.config, 'ENABLE_NOTES', False)
and config.get('notes.enable')
and await has_user_permission('notes')
):
builtin_functions.extend([search_notes, view_note, write_note, replace_note_content])
@@ -579,7 +590,7 @@ async def get_builtin_tools(
# Channels tools - search channels and messages
if (
is_builtin_tool_enabled('channels')
and getattr(request.app.state.config, 'ENABLE_CHANNELS', False)
and config.get('channels.enable')
and await has_user_permission('channels')
):
builtin_functions.extend(
@@ -602,7 +613,7 @@ async def get_builtin_tools(
# Automation tools - create and manage scheduled automations from chat
if (
is_builtin_tool_enabled('automations')
and getattr(request.app.state.config, 'ENABLE_AUTOMATIONS', False)
and config.get('automations.enable')
and await has_user_permission('automations')
):
builtin_functions.extend(
@@ -612,7 +623,7 @@ async def get_builtin_tools(
# Calendar tools - search/create/update/delete events
if (
is_builtin_tool_enabled('calendar')
and getattr(request.app.state.config, 'ENABLE_CALENDAR', False)
and config.get('calendar.enable')
and await has_user_permission('calendar')
):
builtin_functions.extend(
@@ -958,7 +969,7 @@ def convert_openapi_to_tool_payload(openapi_spec):
async def set_tool_servers(request: Request):
try:
request.app.state.TOOL_SERVERS = await get_tool_servers_data(request.app.state.config.TOOL_SERVER_CONNECTIONS)
request.app.state.TOOL_SERVERS = await get_tool_servers_data(await Config.get('tool_server.connections', []))
except Exception as e:
log.error(f'Error fetching tool server data: {e}')
request.app.state.TOOL_SERVERS = getattr(request.app.state, 'TOOL_SERVERS', None) or []
@@ -1055,7 +1066,7 @@ async def get_terminal_system_prompt(
async def set_terminal_servers(request: Request):
"""Load and cache OpenAPI specs from all TERMINAL_SERVER_CONNECTIONS."""
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connections = await Config.get('terminal_server.connections', []) or []
# Build server configs compatible with get_tool_servers_data
# Terminal connections store id/name at top level; translate to info dict
@@ -1148,7 +1159,7 @@ async def get_terminal_tools(
- Loads specs from cache
- Builds callables that route through the terminal proxy
"""
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connections = await Config.get('terminal_server.connections', []) or []
connection = next((c for c in connections if c.get('id') == terminal_id), None)
if connection is None:
log.warning(f'Terminal server not found: {terminal_id}')