diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 611f8e03e7..fea641e7fe 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -3062,6 +3062,17 @@ async def process_chat_payload(request, form_data, user, metadata, model): 'server': tool_server, } + if terminal_id and terminal_capability: + from open_webui.utils.terminals import get_terminal_agents_md + + agents_md = await get_terminal_agents_md(request, user, metadata, extra_params) + if agents_md: + form_data['messages'] = add_or_update_system_message( + agents_md, + form_data['messages'], + append=True, + ) + if mcp_clients: metadata['mcp_clients'] = mcp_clients diff --git a/backend/open_webui/utils/terminals.py b/backend/open_webui/utils/terminals.py index 9054c1f042..8c014d7e96 100644 --- a/backend/open_webui/utils/terminals.py +++ b/backend/open_webui/utils/terminals.py @@ -1,5 +1,8 @@ """Shared routing helpers for admin-configured terminal servers.""" +import asyncio +import logging +import posixpath from urllib.parse import quote from open_webui.utils.chat_id import is_saved_chat_id @@ -9,6 +12,9 @@ TERMINAL_CONTEXT_DEFAULT = 'default' TERMINAL_CONTEXT_TYPES = {'chat', 'automation'} TERMINAL_CONTEXT_ID_SOURCES = {'chat': 'chat_id', 'automation': 'automation_id'} TERMINAL_CHAT_UPLOAD_MODES = {'default', 'filesystem'} +MAX_AGENTS_MD_BYTES = 32 * 1024 + +log = logging.getLogger(__name__) def is_terminal_orchestrator(connection: dict) -> bool: @@ -131,7 +137,14 @@ async def get_terminal_request_info(request, user, metadata: dict, extra_params: connection = next((item for item in connections if item.get('id') == terminal_id), None) if connection: - if not connection.get('enabled', True): + terminal_context = 'automation' if metadata.get('automation_id') else 'chat' + context_id = terminal_context_id(connection, metadata, terminal_context) + config = terminal_context_config(connection, terminal_context) + if ( + not connection.get('enabled', True) + or not terminal_context_available(connection, terminal_context) + or (config.get('context_id') in {'chat_id', 'automation_id'} and not context_id) + ): return None user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user_model.id)} if not await has_connection_access(user_model, connection, user_group_ids): @@ -148,15 +161,17 @@ async def get_terminal_request_info(request, user, metadata: dict, extra_params: headers['X-User-Id'] = user_model.id if metadata.get('chat_id'): headers['X-Session-Id'] = metadata['chat_id'] - terminal_context = 'automation' if metadata.get('automation_id') else 'chat' - context_id = terminal_context_id(connection, metadata, terminal_context) if context_id: headers[TERMINAL_CONTEXT_HEADER] = context_id return get_terminal_server_url(connection), headers, cookies selector = str(terminal_id).rstrip('/') direct_terminal = next( - (server for server in metadata.get('tool_servers') or [] if str(server.get('url') or '').rstrip('/') == selector), + ( + server + for server in metadata.get('tool_servers') or [] + if str(server.get('url') or '').rstrip('/') == selector + ), None, ) if not direct_terminal: @@ -171,10 +186,62 @@ async def get_terminal_request_info(request, user, metadata: dict, extra_params: return selector, headers, {} -async def get_terminal_skill(request, user, metadata: dict, skill_name: str, extra_params: dict | None = None) -> dict | None: +async def get_terminal_agents_md(request, user, metadata: dict, extra_params: dict | None = None) -> str | None: + """Load the selected terminal user's home AGENTS.md afresh for this turn.""" import aiohttp + from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL + + try: + async with asyncio.timeout(5): + terminal_request = await get_terminal_request_info(request, user, metadata, extra_params) + if not terminal_request: + return None + base_url, headers, cookies = terminal_request + async with aiohttp.ClientSession( + headers=headers, cookies=cookies, timeout=aiohttp.ClientTimeout(total=5), trust_env=True + ) as session: + async with session.get( + f'{base_url.rstrip("/")}/files/cwd', + ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, + allow_redirects=False, + ) as response: + if response.status != 200: + log.debug('Skipping terminal AGENTS.md: home lookup returned HTTP %s', response.status) + return None + data = await response.json() + home = data.get('home') if isinstance(data, dict) else None + if not isinstance(home, str) or not posixpath.isabs(home): + return None + path = posixpath.join(home, 'AGENTS.md') + async with session.get( + f'{base_url.rstrip("/")}/files/read', + params={'path': path}, + ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, + allow_redirects=False, + ) as response: + if response.status == 404: + return None + response.raise_for_status() + data = await response.json() + + content = data.get('content') if isinstance(data, dict) else None + if not isinstance(content, str) or not content.strip(): + return None + if len(content.encode('utf-8')) > MAX_AGENTS_MD_BYTES: + log.warning('Skipping terminal AGENTS.md: exceeds %s bytes', MAX_AGENTS_MD_BYTES) + return None + return f'# AGENTS.md instructions for {home}\n\n\n{content}\n' + except Exception as e: + log.debug('Failed to load terminal AGENTS.md (%s)', type(e).__name__) + return None + + +async def get_terminal_skill( + request, user, metadata: dict, skill_name: str, extra_params: dict | None = None +) -> dict | None: from urllib.parse import quote + import aiohttp from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA terminal_request = await get_terminal_request_info(request, user, metadata, extra_params)