Skip to content

Commit 79f0437

Browse files
authored
Merge pull request open-webui#22168 from open-webui/dev
0.8.8
2 parents 6137f7c + 10daa64 commit 79f0437

79 files changed

Lines changed: 1239 additions & 892 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.8.8] - 2026-03-02
9+
10+
### Added
11+
12+
- 📁 **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)
13+
- 📄 **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)
14+
- 🌐 **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)
15+
- ⚙️ **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)
16+
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
17+
- 🌐 Translations for Simplified Chinese, Traditional Chinese, Irish, and Catalan were enhanced and expanded.
18+
19+
### Fixed
20+
21+
- 🔧 **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)
22+
- ⚡ **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)
23+
- 🔧 **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)
24+
- 🔌 **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)
25+
- 📜 **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)
26+
- 🔧 **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)
27+
28+
### Changed
29+
830
## [0.8.7] - 2026-03-01
931

1032
### Fixed

backend/open_webui/routers/terminals.py

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
import logging
99

1010
import aiohttp
11-
from fastapi import APIRouter, Depends, Request, Response
11+
from fastapi import APIRouter, Depends, Request, Response, WebSocket
1212
from fastapi.responses import JSONResponse, StreamingResponse
1313
from starlette.background import BackgroundTask
1414

1515
from open_webui.utils.auth import get_verified_user
1616
from open_webui.utils.access_control import has_connection_access
1717
from open_webui.models.groups import Groups
18+
from open_webui.models.users import Users
1819

1920
log = logging.getLogger(__name__)
2021

@@ -149,3 +150,155 @@ async def cleanup():
149150
return JSONResponse(
150151
{"error": f"Terminal proxy error: {error}"}, status_code=502
151152
)
153+
154+
155+
# ---------------------------------------------------------------------------
156+
# WebSocket proxy for interactive terminal sessions
157+
# ---------------------------------------------------------------------------
158+
159+
160+
async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):
161+
"""Authenticate a WebSocket via first-message auth and resolve the terminal server.
162+
163+
The client must send ``{"type": "auth", "token": "<jwt>"}`` as its first
164+
message after connecting.
165+
166+
Returns ``(user, connection)`` on success, or ``None`` after closing *ws*
167+
with an appropriate error code.
168+
"""
169+
import asyncio
170+
import json
171+
from open_webui.utils.auth import decode_token
172+
173+
# First-message authentication
174+
try:
175+
raw = await asyncio.wait_for(ws.receive_text(), timeout=10.0)
176+
payload = json.loads(raw)
177+
if payload.get("type") != "auth":
178+
await ws.close(code=4001, reason="Expected auth message")
179+
return None
180+
token = payload.get("token", "")
181+
data = decode_token(token)
182+
if data is None or "id" not in data:
183+
await ws.close(code=4001, reason="Invalid token")
184+
return None
185+
user = Users.get_user_by_id(data["id"])
186+
if user is None:
187+
await ws.close(code=4001, reason="User not found")
188+
return None
189+
except (asyncio.TimeoutError, json.JSONDecodeError):
190+
await ws.close(code=4001, reason="Auth timeout or invalid payload")
191+
return None
192+
except Exception:
193+
await ws.close(code=4001, reason="Invalid token")
194+
return None
195+
196+
# Resolve terminal server
197+
connections = ws.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
198+
connection = next((c for c in connections if c.get("id") == server_id), None)
199+
200+
if connection is None:
201+
await ws.close(code=4004, reason="Terminal server not found")
202+
return None
203+
204+
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
205+
if not has_connection_access(user, connection, user_group_ids):
206+
await ws.close(code=4003, reason="Access denied")
207+
return None
208+
209+
return user, connection
210+
211+
212+
@router.websocket("/{server_id}/api/terminals/{session_id}")
213+
async def ws_terminal(
214+
ws: WebSocket,
215+
server_id: str,
216+
session_id: str,
217+
):
218+
"""Proxy an interactive WebSocket terminal session to a terminal server.
219+
220+
Uses first-message auth: the client sends ``{"type": "auth", "token": "<jwt>"}``
221+
as its first message. The proxy validates the JWT, then connects to the
222+
upstream terminal server and authenticates with the server's API key.
223+
"""
224+
await ws.accept()
225+
226+
result = await _resolve_authenticated_connection(ws, server_id)
227+
if result is None:
228+
return
229+
user, connection = result
230+
231+
base_url = (connection.get("url") or "").rstrip("/")
232+
if not base_url:
233+
await ws.close(code=4003, reason="Terminal server URL not configured")
234+
return
235+
236+
# Build upstream WebSocket URL (no token in URL)
237+
ws_base = base_url.replace("https://", "wss://").replace("http://", "ws://")
238+
239+
auth_type = connection.get("auth_type", "bearer")
240+
upstream_params = {}
241+
# For orchestrator-backed servers, pass user_id
242+
upstream_params["user_id"] = user.id
243+
244+
import urllib.parse
245+
246+
upstream_url = f"{ws_base}/api/terminals/{session_id}"
247+
if upstream_params:
248+
upstream_url += f"?{urllib.parse.urlencode(upstream_params)}"
249+
250+
session = aiohttp.ClientSession()
251+
try:
252+
async with session.ws_connect(upstream_url) as upstream:
253+
import asyncio
254+
import json as _json
255+
256+
# First-message auth to upstream terminal server
257+
auth_type = connection.get("auth_type", "bearer")
258+
if auth_type == "bearer":
259+
key = connection.get("key", "")
260+
await upstream.send_str(_json.dumps({"type": "auth", "token": key}))
261+
262+
async def _client_to_upstream():
263+
"""Forward client → upstream."""
264+
try:
265+
while True:
266+
msg = await ws.receive()
267+
if msg["type"] == "websocket.disconnect":
268+
break
269+
elif "bytes" in msg and msg["bytes"]:
270+
await upstream.send_bytes(msg["bytes"])
271+
elif "text" in msg and msg["text"]:
272+
await upstream.send_str(msg["text"])
273+
except Exception:
274+
pass
275+
276+
async def _upstream_to_client():
277+
"""Forward upstream → client."""
278+
try:
279+
async for msg in upstream:
280+
if msg.type == aiohttp.WSMsgType.BINARY:
281+
await ws.send_bytes(msg.data)
282+
elif msg.type == aiohttp.WSMsgType.TEXT:
283+
await ws.send_text(msg.data)
284+
elif msg.type in (
285+
aiohttp.WSMsgType.CLOSE,
286+
aiohttp.WSMsgType.ERROR,
287+
):
288+
break
289+
except Exception:
290+
pass
291+
292+
await asyncio.gather(
293+
_client_to_upstream(),
294+
_upstream_to_client(),
295+
return_exceptions=True,
296+
)
297+
except Exception as e:
298+
log.exception("Terminal WebSocket proxy error: %s", e)
299+
finally:
300+
await session.close()
301+
try:
302+
await ws.close()
303+
except Exception:
304+
pass

backend/open_webui/tools/builtin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1831,7 +1831,7 @@ async def query_knowledge_files(
18311831
elif item_type == "file":
18321832
# Individual file - use file-{id} as collection name
18331833
file = Files.get_file_by_id(item_id)
1834-
if file and (user_role == "admin" or file.user_id == user_id):
1834+
if file:
18351835
collection_names.append(f"file-{item_id}")
18361836

18371837
elif item_type == "note":

backend/open_webui/utils/access_control/files.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from open_webui.models.channels import Channels
88
from open_webui.models.chats import Chats
99
from open_webui.models.groups import Groups
10+
from open_webui.models.models import Models
1011
from open_webui.models.access_grants import AccessGrants
1112

1213
log = logging.getLogger(__name__)
@@ -21,6 +22,7 @@ def has_access_to_file(
2122
"""
2223
Check if a user has the specified access to a file through any of:
2324
- Knowledge bases (ownership or access grants)
25+
- Shared workspace models that attach the file directly
2426
- Channels the user is a member of
2527
- Shared chats
2628
@@ -72,4 +74,15 @@ def has_access_to_file(
7274
if chats:
7375
return True
7476

77+
# Check if the file is directly attached to a shared workspace model
78+
for model in Models.get_models_by_user_id(user.id, permission=access_type, db=db):
79+
knowledge_items = getattr(model.meta, "knowledge", None) or []
80+
for item in knowledge_items:
81+
if (
82+
isinstance(item, dict)
83+
and item.get("type") == "file"
84+
and item.get("id") == file.id
85+
):
86+
return True
87+
7588
return False

backend/open_webui/utils/middleware.py

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
get_last_user_message_item,
9292
get_last_assistant_message,
9393
get_system_message,
94+
replace_system_message_content,
9495
prepend_to_first_user_message_content,
9596
convert_logit_bias_input_to_json,
9697
get_content_from_message,
@@ -375,9 +376,9 @@ def serialize_output(output: list) -> str:
375376
result_item = tool_outputs.get(call_id)
376377
if result_item:
377378
result_text = ""
378-
for output in result_item.get("output", []):
379-
if "text" in output:
380-
output_text = output.get("text", "")
379+
for result_output in result_item.get("output", []):
380+
if "text" in result_output:
381+
output_text = result_output.get("text", "")
381382
result_text += (
382383
str(output_text)
383384
if not isinstance(output_text, str)
@@ -4090,6 +4091,22 @@ async def flush_pending_delta_data(threshold: int = 0):
40904091
all_tool_call_sources = [] # Accumulated sources across all iterations
40914092
user_message = get_last_user_message(form_data["messages"])
40924093

4094+
# Check if citations are enabled for this model
4095+
citations_enabled = (
4096+
model.get("info", {}).get("meta", {}).get("capabilities") or {}
4097+
).get("citations", True)
4098+
4099+
# Save original system message so we can restore it before
4100+
# re-applying source context (prevents duplication when
4101+
# RAG_SYSTEM_CONTEXT is enabled and the template is appended
4102+
# to the system message on each iteration).
4103+
original_system_message = get_system_message(form_data["messages"])
4104+
original_system_content = (
4105+
get_content_from_message(original_system_message)
4106+
if original_system_message
4107+
else None
4108+
)
4109+
40934110
while (
40944111
len(tool_calls) > 0
40954112
and tool_call_retries < CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES
@@ -4244,7 +4261,8 @@ async def flush_pending_delta_data(threshold: int = 0):
42444261

42454262
# Extract citation sources from tool results
42464263
if (
4247-
tool_function_name
4264+
citations_enabled
4265+
and tool_function_name
42484266
in [
42494267
"search_web",
42504268
"fetch_url",
@@ -4334,27 +4352,35 @@ async def flush_pending_delta_data(threshold: int = 0):
43344352
}
43354353
)
43364354

4337-
# Emit citation sources for UI display
4338-
for source in tool_call_sources:
4339-
await event_emitter({"type": "source", "data": source})
4340-
4341-
# Apply source context to messages for model
4342-
# Use metadata_only=True to avoid duplicating content
4343-
# that is already in the tool result message.
4344-
all_tool_call_sources.extend(tool_call_sources)
4345-
if all_tool_call_sources and user_message:
4346-
# Restore original user message before re-applying to avoid recursive nesting
4347-
set_last_user_message_content(
4348-
user_message, form_data["messages"]
4349-
)
4350-
form_data["messages"] = apply_source_context_to_messages(
4351-
request,
4352-
form_data["messages"],
4353-
all_tool_call_sources,
4354-
user_message,
4355-
include_content=False,
4356-
)
4357-
tool_call_sources.clear()
4355+
# Emit citation sources and apply source context to messages
4356+
if citations_enabled:
4357+
for source in tool_call_sources:
4358+
await event_emitter({"type": "source", "data": source})
4359+
4360+
# Apply source context to messages for model.
4361+
# Use include_content=False to avoid duplicating content
4362+
# that is already in the tool result message.
4363+
all_tool_call_sources.extend(tool_call_sources)
4364+
if all_tool_call_sources and user_message:
4365+
# Restore original messages before re-applying to
4366+
# avoid recursive nesting (user message) and
4367+
# duplication (system message with RAG_SYSTEM_CONTEXT).
4368+
set_last_user_message_content(
4369+
user_message, form_data["messages"]
4370+
)
4371+
if original_system_content is not None:
4372+
replace_system_message_content(
4373+
original_system_content,
4374+
form_data["messages"],
4375+
)
4376+
form_data["messages"] = apply_source_context_to_messages(
4377+
request,
4378+
form_data["messages"],
4379+
all_tool_call_sources,
4380+
user_message,
4381+
include_content=False,
4382+
)
4383+
tool_call_sources.clear()
43584384

43594385
await event_emitter(
43604386
{

backend/open_webui/utils/tools.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1247,7 +1247,8 @@ async def execute_tool_server(
12471247
if param_in == "path":
12481248
path_params[param_name] = params[param_name]
12491249
elif param_in == "query":
1250-
query_params[param_name] = params[param_name]
1250+
if params[param_name] is not None:
1251+
query_params[param_name] = params[param_name]
12511252

12521253
final_url = f"{url}{route_path}"
12531254
for key, value in path_params.items():

0 commit comments

Comments
 (0)