Skip to content
Merged

Dev #268

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d7147d6
refac
tjbck Mar 2, 2026
64957db
refac
tjbck Mar 2, 2026
b338850
Merge pull request #22127 from ShirasawaSama/patch-49
ShirasawaSama Mar 2, 2026
c701ebe
refac
tjbck Mar 2, 2026
0c42cd2
enh: ot move
tjbck Mar 2, 2026
7295132
refac
tjbck Mar 2, 2026
395098c
refac
tjbck Mar 2, 2026
11487d6
refac
tjbck Mar 2, 2026
bec227d
i18n: improve Chinese translations (#22148)
ShirasawaSama Mar 2, 2026
3909b62
enh: file nav html rendering
tjbck Mar 2, 2026
933a3bb
refac
tjbck Mar 2, 2026
fe1941c
fix: add missing lang="ts" to ChatControls module script (#22131)
jannikstdl Mar 2, 2026
44349fb
refac
tjbck Mar 2, 2026
8ea35e3
i18n: Updated Irish translation (#22132)
aindriu80 Mar 2, 2026
75683e5
i18n: Update catalan translation.json (#22129)
aleixdorca Mar 2, 2026
4f6cb77
enh: open terminal
tjbck Mar 2, 2026
1a2b360
refac
tjbck Mar 2, 2026
ed9ab65
refac
tjbck Mar 2, 2026
b5c3395
refac
tjbck Mar 2, 2026
d040953
fix: omit None-valued query params in execute_tool_server (#22144)
Classic298 Mar 2, 2026
fe5c023
chore: changelog (#22152)
Classic298 Mar 2, 2026
3de14a5
chore: format
tjbck Mar 2, 2026
10baa6e
chore: format
tjbck Mar 2, 2026
65fbbf5
fix: grant file access for knowledge attached to shared workspace mod…
Classic298 Mar 2, 2026
e0d4c3e
refac
tjbck Mar 2, 2026
10daa64
chore: format
tjbck Mar 2, 2026
79f0437
Merge pull request #22168 from open-webui/dev
tjbck Mar 2, 2026
d11c138
Merge remote-tracking branch 'openwebui/main' into dev
OrenZhang Mar 3, 2026
b780171
chore(repo): merge from remote
OrenZhang Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.8] - 2026-03-02

### Added

- 📁 **Open Terminal file moving.** Users can now move files and folders between directories in the Open Terminal file browser by dragging and dropping them. [Commit](https://github.com/open-webui/open-webui/commit/0c42cd2c012f9f49816adac897e2b46573b3cb6c), [Commit](https://github.com/open-webui/open-webui/commit/72951324dfeef64e09f4776898d675bc1c44f040), [Commit](https://github.com/open-webui/open-webui/commit/395098c6f1b7499d37ad55145a5931431d3e72e9), [Commit](https://github.com/open-webui/open-webui/commit/11487d66fc1a2dfafbdaa2b7ef939a86caaf3872)
- 📄 **Open Terminal HTML file preview.** Users can now preview HTML files directly in the Open Terminal file browser, with a rendered iframe view and source toggle, enabling iterative AI editing of HTML files. [Commit](https://github.com/open-webui/open-webui/commit/3909b62ffcf49839fa57346ed8487ae759811503), [Commit](https://github.com/open-webui/open-webui/commit/933a3bbbd3f4fc3eeb0ec52c7965e9ac1c4cea39)
- 🌐 **Open Terminal WebSocket proxy.** Added a new WebSocket proxy endpoint for interactive terminal sessions, enabling real-time bidirectional terminal communication with the terminal server. [Commit](https://github.com/open-webui/open-webui/commit/4f6cb771f1afded09aad6199cdb244dd8a6c77a6)
- ⚙️ **Open Terminal feature toggle.** Administrators can now enable or disable the Interactive Terminal feature for Open Terminal via configuration on the terminal server, controlling access to terminal routes. [Commit](https://github.com/open-webui/open-webui/commit/b5c3395f79bcc7ff5bc1d82bb86a60583bb3b5bd)
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
- 🌐 Translations for Simplified Chinese, Traditional Chinese, Irish, and Catalan were enhanced and expanded.

### Fixed

- 🔧 **Middleware variable shadowing.** Fixed a variable shadowing issue in the middleware that could cause incorrect tool output processing during chat. [#22145](https://github.com/open-webui/open-webui/pull/22145)
- ⚡ **ChatControls reactivity fix.** Fixed a Svelte reactivity issue where the active tab state in the ChatControls panel was not properly saved when switching between chats. [#22127](https://github.com/open-webui/open-webui/pull/22127)
- 🔧 **ChatControls TypeScript fix.** Fixed a TypeScript syntax error in ChatControls.svelte where the module script block was missing lang="ts", causing esbuild to fail during vite dev. [#22131](https://github.com/open-webui/open-webui/pull/22131)
- 🔌 **Open Terminal tools for direct connections.** Fixed an issue where Open Terminal tools were not available to the model when the terminal was configured via direct connection settings, ensuring users can now interact with terminal files and operations through the AI. [#22137](https://github.com/open-webui/open-webui/issues/22137)
- 📜 **Chat history pagination.** Fixed an issue where older messages in long chats were not loaded when scrolling to the top. [Commit](https://github.com/open-webui/open-webui/commit/d7147d6cddfd314f0f1be77b15cec406a609ef36), [Commit](https://github.com/open-webui/open-webui/commit/c701ebe07bd152eecb42b0bf6de26071358a5c76)
- 🔧 **Terminal tool null parameter handling.** Fixed a bug where null parameters in terminal tool calls were sent as the string "None" instead of being omitted, causing 422 validation errors from the open-terminal server. [#22124](https://github.com/open-webui/open-webui/issues/22124), [#22144](https://github.com/open-webui/open-webui/pull/22144)

### Changed

## [0.8.7] - 2026-03-01

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG_EXTRA.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.8.1] - 2026.03.03

### Changed

- 合并官方 0.8.8 改动

## [0.8.7.1] - 2026.03.02

### Changed
Expand Down
155 changes: 154 additions & 1 deletion backend/open_webui/routers/terminals.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
import logging

import aiohttp
from fastapi import APIRouter, Depends, Request, Response
from fastapi import APIRouter, Depends, Request, Response, WebSocket
from fastapi.responses import JSONResponse, StreamingResponse
from starlette.background import BackgroundTask

from open_webui.utils.auth import get_verified_user
from open_webui.utils.access_control import has_connection_access
from open_webui.models.groups import Groups
from open_webui.models.users import Users

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -149,3 +150,155 @@ async def cleanup():
return JSONResponse(
{"error": f"Terminal proxy error: {error}"}, status_code=502
)


# ---------------------------------------------------------------------------
# WebSocket proxy for interactive terminal sessions
# ---------------------------------------------------------------------------


async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):
"""Authenticate a WebSocket via first-message auth and resolve the terminal server.

The client must send ``{"type": "auth", "token": "<jwt>"}`` as its first
message after connecting.

Returns ``(user, connection)`` on success, or ``None`` after closing *ws*
with an appropriate error code.
"""
import asyncio
import json
from open_webui.utils.auth import decode_token

# First-message authentication
try:
raw = await asyncio.wait_for(ws.receive_text(), timeout=10.0)
payload = json.loads(raw)
if payload.get("type") != "auth":
await ws.close(code=4001, reason="Expected auth message")
return None
token = payload.get("token", "")
data = decode_token(token)
if data is None or "id" not in data:
await ws.close(code=4001, reason="Invalid token")
return None
user = Users.get_user_by_id(data["id"])
if user is None:
await ws.close(code=4001, reason="User not found")
return None
except (asyncio.TimeoutError, json.JSONDecodeError):
await ws.close(code=4001, reason="Auth timeout or invalid payload")
return None
except Exception:
await ws.close(code=4001, reason="Invalid token")
return None

# Resolve terminal server
connections = ws.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connection = next((c for c in connections if c.get("id") == server_id), None)

if connection is None:
await ws.close(code=4004, reason="Terminal server not found")
return None

user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
if not has_connection_access(user, connection, user_group_ids):
await ws.close(code=4003, reason="Access denied")
return None

return user, connection


@router.websocket("/{server_id}/api/terminals/{session_id}")
async def ws_terminal(
ws: WebSocket,
server_id: str,
session_id: str,
):
"""Proxy an interactive WebSocket terminal session to a terminal server.

Uses first-message auth: the client sends ``{"type": "auth", "token": "<jwt>"}``
as its first message. The proxy validates the JWT, then connects to the
upstream terminal server and authenticates with the server's API key.
"""
await ws.accept()

result = await _resolve_authenticated_connection(ws, server_id)
if result is None:
return
user, connection = result

base_url = (connection.get("url") or "").rstrip("/")
if not base_url:
await ws.close(code=4003, reason="Terminal server URL not configured")
return

# Build upstream WebSocket URL (no token in URL)
ws_base = base_url.replace("https://", "wss://").replace("http://", "ws://")

auth_type = connection.get("auth_type", "bearer")
upstream_params = {}
# For orchestrator-backed servers, pass user_id
upstream_params["user_id"] = user.id

import urllib.parse

upstream_url = f"{ws_base}/api/terminals/{session_id}"
if upstream_params:
upstream_url += f"?{urllib.parse.urlencode(upstream_params)}"

session = aiohttp.ClientSession()
try:
async with session.ws_connect(upstream_url) as upstream:
import asyncio
import json as _json

# First-message auth to upstream terminal server
auth_type = connection.get("auth_type", "bearer")
if auth_type == "bearer":
key = connection.get("key", "")
await upstream.send_str(_json.dumps({"type": "auth", "token": key}))

async def _client_to_upstream():
"""Forward client → upstream."""
try:
while True:
msg = await ws.receive()
if msg["type"] == "websocket.disconnect":
break
elif "bytes" in msg and msg["bytes"]:
await upstream.send_bytes(msg["bytes"])
elif "text" in msg and msg["text"]:
await upstream.send_str(msg["text"])
except Exception:
pass

async def _upstream_to_client():
"""Forward upstream → client."""
try:
async for msg in upstream:
if msg.type == aiohttp.WSMsgType.BINARY:
await ws.send_bytes(msg.data)
elif msg.type == aiohttp.WSMsgType.TEXT:
await ws.send_text(msg.data)
elif msg.type in (
aiohttp.WSMsgType.CLOSE,
aiohttp.WSMsgType.ERROR,
):
break
except Exception:
pass

await asyncio.gather(
_client_to_upstream(),
_upstream_to_client(),
return_exceptions=True,
)
except Exception as e:
log.exception("Terminal WebSocket proxy error: %s", e)
finally:
await session.close()
try:
await ws.close()
except Exception:
pass
2 changes: 1 addition & 1 deletion backend/open_webui/tools/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1831,7 +1831,7 @@ async def query_knowledge_files(
elif item_type == "file":
# Individual file - use file-{id} as collection name
file = Files.get_file_by_id(item_id)
if file and (user_role == "admin" or file.user_id == user_id):
if file:
collection_names.append(f"file-{item_id}")

elif item_type == "note":
Expand Down
13 changes: 13 additions & 0 deletions backend/open_webui/utils/access_control/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from open_webui.models.channels import Channels
from open_webui.models.chats import Chats
from open_webui.models.groups import Groups
from open_webui.models.models import Models
from open_webui.models.access_grants import AccessGrants

log = logging.getLogger(__name__)
Expand All @@ -21,6 +22,7 @@ def has_access_to_file(
"""
Check if a user has the specified access to a file through any of:
- Knowledge bases (ownership or access grants)
- Shared workspace models that attach the file directly
- Channels the user is a member of
- Shared chats

Expand Down Expand Up @@ -72,4 +74,15 @@ def has_access_to_file(
if chats:
return True

# Check if the file is directly attached to a shared workspace model
for model in Models.get_models_by_user_id(user.id, permission=access_type, db=db):
knowledge_items = getattr(model.meta, "knowledge", None) or []
for item in knowledge_items:
if (
isinstance(item, dict)
and item.get("type") == "file"
and item.get("id") == file.id
):
return True

return False
76 changes: 51 additions & 25 deletions backend/open_webui/utils/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
get_last_user_message_item,
get_last_assistant_message,
get_system_message,
replace_system_message_content,
prepend_to_first_user_message_content,
convert_logit_bias_input_to_json,
get_content_from_message,
Expand Down Expand Up @@ -375,9 +376,9 @@ def serialize_output(output: list) -> str:
result_item = tool_outputs.get(call_id)
if result_item:
result_text = ""
for output in result_item.get("output", []):
if "text" in output:
output_text = output.get("text", "")
for result_output in result_item.get("output", []):
if "text" in result_output:
output_text = result_output.get("text", "")
result_text += (
str(output_text)
if not isinstance(output_text, str)
Expand Down Expand Up @@ -4090,6 +4091,22 @@ async def flush_pending_delta_data(threshold: int = 0):
all_tool_call_sources = [] # Accumulated sources across all iterations
user_message = get_last_user_message(form_data["messages"])

# Check if citations are enabled for this model
citations_enabled = (
model.get("info", {}).get("meta", {}).get("capabilities") or {}
).get("citations", True)

# Save original system message so we can restore it before
# re-applying source context (prevents duplication when
# RAG_SYSTEM_CONTEXT is enabled and the template is appended
# to the system message on each iteration).
original_system_message = get_system_message(form_data["messages"])
original_system_content = (
get_content_from_message(original_system_message)
if original_system_message
else None
)

while (
len(tool_calls) > 0
and tool_call_retries < CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES
Expand Down Expand Up @@ -4244,7 +4261,8 @@ async def flush_pending_delta_data(threshold: int = 0):

# Extract citation sources from tool results
if (
tool_function_name
citations_enabled
and tool_function_name
in [
"search_web",
"fetch_url",
Expand Down Expand Up @@ -4334,27 +4352,35 @@ async def flush_pending_delta_data(threshold: int = 0):
}
)

# Emit citation sources for UI display
for source in tool_call_sources:
await event_emitter({"type": "source", "data": source})

# Apply source context to messages for model
# Use metadata_only=True to avoid duplicating content
# that is already in the tool result message.
all_tool_call_sources.extend(tool_call_sources)
if all_tool_call_sources and user_message:
# Restore original user message before re-applying to avoid recursive nesting
set_last_user_message_content(
user_message, form_data["messages"]
)
form_data["messages"] = apply_source_context_to_messages(
request,
form_data["messages"],
all_tool_call_sources,
user_message,
include_content=False,
)
tool_call_sources.clear()
# Emit citation sources and apply source context to messages
if citations_enabled:
for source in tool_call_sources:
await event_emitter({"type": "source", "data": source})

# Apply source context to messages for model.
# Use include_content=False to avoid duplicating content
# that is already in the tool result message.
all_tool_call_sources.extend(tool_call_sources)
if all_tool_call_sources and user_message:
# Restore original messages before re-applying to
# avoid recursive nesting (user message) and
# duplication (system message with RAG_SYSTEM_CONTEXT).
set_last_user_message_content(
user_message, form_data["messages"]
)
if original_system_content is not None:
replace_system_message_content(
original_system_content,
form_data["messages"],
)
form_data["messages"] = apply_source_context_to_messages(
request,
form_data["messages"],
all_tool_call_sources,
user_message,
include_content=False,
)
tool_call_sources.clear()

await event_emitter(
{
Expand Down
3 changes: 2 additions & 1 deletion backend/open_webui/utils/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1247,7 +1247,8 @@ async def execute_tool_server(
if param_in == "path":
path_params[param_name] = params[param_name]
elif param_in == "query":
query_params[param_name] = params[param_name]
if params[param_name] is not None:
query_params[param_name] = params[param_name]

final_url = f"{url}{route_path}"
for key, value in path_params.items():
Expand Down
Loading
Loading