diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index ce2efa7096..1d89b4754c 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -20,6 +20,7 @@ from open_webui.models.groups import Groups from open_webui.models.users import Users from open_webui.utils.access_control import has_connection_access from open_webui.utils.auth import get_verified_user +from open_webui.utils.terminals import get_terminal_server_url from open_webui.utils.tools import bearer_auth_header, normalize_bearer_token from starlette.background import BackgroundTask @@ -100,7 +101,7 @@ async def proxy_terminal( if not await has_connection_access(user, connection, user_group_ids): return JSONResponse({'error': 'Access denied'}, status_code=403) - base_url = (connection.get('url') or '').rstrip('/') + base_url = get_terminal_server_url(connection) if not base_url: return JSONResponse({'error': 'Terminal server URL not configured'}, status_code=503) @@ -110,11 +111,6 @@ async def proxy_terminal( target_url = f'{base_url}/{safe_path}' - # Route through orchestrator policy endpoint if policy_id is set - policy_id = connection.get('policy_id') - if policy_id: - target_url = f'{base_url}/p/{policy_id}/{safe_path}' - if request.query_params: target_url += f'?{request.query_params}' @@ -272,7 +268,7 @@ async def ws_terminal( return user, connection = result - base_url = (connection.get('url') or '').rstrip('/') + base_url = get_terminal_server_url(connection) if not base_url: await ws.close(code=4003, reason='Terminal server URL not configured') return @@ -280,8 +276,6 @@ async def ws_terminal( # Build upstream WebSocket URL (no token in URL) ws_base = base_url.replace('https://', 'wss://').replace('http://', 'ws://') - # Route through orchestrator policy endpoint if policy_id is set - policy_id = connection.get('policy_id') upstream_params = {} # For orchestrator-backed servers, pass user_id upstream_params['user_id'] = user.id @@ -292,10 +286,7 @@ async def ws_terminal( # decode depth) and inject an attacker-chosen user_id ahead of the one appended below. safe_session_id = urllib.parse.quote(session_id, safe='') - if policy_id: - upstream_url = f'{ws_base}/p/{policy_id}/api/terminals/{safe_session_id}' - else: - upstream_url = f'{ws_base}/api/terminals/{safe_session_id}' + upstream_url = f'{ws_base}/api/terminals/{safe_session_id}' if upstream_params: upstream_url += f'?{urllib.parse.urlencode(upstream_params)}' diff --git a/backend/open_webui/utils/automations.py b/backend/open_webui/utils/automations.py index d4ac9c5207..3fb8f0bcad 100644 --- a/backend/open_webui/utils/automations.py +++ b/backend/open_webui/utils/automations.py @@ -36,6 +36,7 @@ from open_webui.models.users import Users from open_webui.utils.auth import create_token from open_webui.utils.misc import parse_duration from open_webui.utils.task import prompt_template +from open_webui.utils.terminals import get_terminal_server_url from starlette.datastructures import Headers log = logging.getLogger(__name__) @@ -323,16 +324,11 @@ async def _set_terminal_cwd(app, server_id: str, user, cwd: str, chat_id: str) - log.warning(f'Terminal server {server_id} not found for CWD set') return - base_url = (connection.get('url') or '').rstrip('/') + base_url = get_terminal_server_url(connection) if not base_url: return - # Build target URL — route through orchestrator policy if configured - policy_id = connection.get('policy_id') - if connection.get('server_type') == 'orchestrator' and policy_id: - target_url = f'{base_url}/p/{policy_id}/files/cwd' - else: - target_url = f'{base_url}/files/cwd' + target_url = f'{base_url}/files/cwd' headers = {'Content-Type': 'application/json', 'X-User-Id': user.id} if chat_id: diff --git a/backend/open_webui/utils/terminals.py b/backend/open_webui/utils/terminals.py new file mode 100644 index 0000000000..0599d9a880 --- /dev/null +++ b/backend/open_webui/utils/terminals.py @@ -0,0 +1,17 @@ +"""Shared routing helpers for admin-configured terminal servers.""" + +from urllib.parse import quote + + +def get_terminal_server_url(connection: dict) -> str: + """Return the upstream base URL for a terminal connection. + + Only orchestrator connections with an explicit policy use the named-policy + route. Direct Open Terminal connections and legacy unscoped orchestrator + connections keep their existing root route. + """ + base_url = str(connection.get('url') or '').rstrip('/') + policy_id = str(connection.get('policy_id') or '').strip() + if connection.get('server_type') == 'orchestrator' and policy_id: + return f'{base_url}/p/{quote(policy_id, safe="")}' + return base_url