Commit Graph
100 Commits
Author SHA1 Message Date
Classic298 076a84e3f0 fix: enforce automation limits in the builtin automation tools (#27523)
The `create_automation` and `update_automation` builtin tools wrote straight to `Automations.insert` / `Automations.update_by_id`, skipping the limit checks that `/api/v1/automations/create` and `/api/v1/automations/{id}/update` run through `check_automation_limits`. A non-admin user could therefore ask the model to create automations indefinitely, ignoring `AUTOMATION_MAX_COUNT`, and could schedule them below `AUTOMATION_MIN_INTERVAL`, on both create and update.

Both tools now call the same `check_automation_limits` helper the routers use, so the limits and the admin bypass cannot drift between the chat path and the HTTP path. A rejection is returned to the model as a plain error message instead of raising. `update_automation` also gained the missing user lookup guard, since the helper needs the user's role.

The `automations.enable` toggle and the `features.automations` user permission were already enforced when the tool set is assembled, so they are unaffected.

Fixes #27121
2026-07-26 17:55:56 -04:00
Classic298 3ce734c6c6 fix: bump uvicorn to 0.51.0 to move off the legacy websocket implementation (#27553)
Uvicorn's `--ws auto` selected its `websockets_impl` protocol on 0.41.0, which is built on `websockets.legacy`. That module raises `AssertionError` in `_drain_helper` during keepalive pings and kills the websocket connection. Each crash runs the Socket.IO `disconnect` handler and drops the session from `SESSION_POOL`, so every subsequent server-to-browser call fails. The most visible symptom is the Pyodide code execution tool, which reaches the browser through `sio.call('events', ...)` and returns `{"stderr": "Client session disconnected."}` on every run.

Uvicorn 0.50.0 changed `--ws auto` to select the sans-io implementation whenever websockets is installed, and deprecated the legacy one. Bumping the pin therefore fixes this on every launch path at once, without adding a `--ws` flag to the startup scripts. Doing nothing is not stable either: websockets is unpinned apart from uvicorn's own `>=13.0` floor, and `websockets.legacy` is removed outright in websockets 17, which turns the current AssertionError into an ImportError on a fresh install.

Bumping to 0.51.0 rather than the minimum 0.50.0 also picks up the sans-io keepalive pings added in 0.44.0, so raw websocket endpoints keep the idle-timeout behaviour they have today behind a reverse proxy. Uvicorn 0.51.0 drops colorama from its `standard` extra and raises the httptools floor to 0.8.0, which the lockfile already satisfies.

Verified on the bumped pin: the backend boots, `/health` returns 200, `--ws auto` resolves to `WebSocketsSansIOProtocol`, a Socket.IO client completes a websocket handshake against the running app, and a bidirectional `sio.call` round trip succeeds. The unit test suite reports an identical 2273 passed / 7 failed on 0.41.0 and 0.51.0, with the 7 failures unrelated to uvicorn.

Fixes #27550
2026-07-26 17:55:44 -04:00
Classic298 1e0ab84717 fix: unshadow the time module so the web loader rate limiter can sleep (#27528)
`from datetime import datetime, time, timedelta` shadows the `time` module, so `RateLimitMixin._sync_wait_for_rate_limit` calls `datetime.time.sleep` and raises `AttributeError: type object 'datetime.time' has no attribute 'sleep'` whenever it actually has to wait.

Every synchronous loader path that paces requests hits this. `SafeFireCrawlLoader.lazy_load` calls the limiter directly, and Tavily, Microsoft Web IQ and Playwright reach it through `_safe_process_url_sync`. The exception is raised inside their per-URL `try`, so with `continue_on_failure=True` (the default) the URL is logged as a per-URL failure and dropped instead of being scraped. This is live by default: `WEB_LOADER_CONCURRENT_REQUESTS` is passed as `requests_per_second` and defaults to 10, so any URL whose predecessor finished within 100ms takes the sleep branch and is lost. Tavily and Microsoft Web IQ report it as "SSL verification failed", which points at the wrong cause.

`_wait_for_rate_limit` uses `asyncio.sleep` and is unaffected, but `SafeMicrosoftWebIQLoader.alazy_load` runs `lazy_load` in a threadpool, so its async entry point is affected too.

`datetime.time` is not used anywhere in the file, so importing the `time` module instead is enough.

The per-URL `continue` half of #26079 landed in 6f8221df5, which also added the `_sync_wait_for_rate_limit()` call to the Firecrawl loop. This makes that call work rather than throw.

Fixes #26079
2026-07-26 17:55:28 -04:00
Classic298andmanus-use c882222f68 fix: verify chat ownership on /api/chat/completed and /api/chat/actions (#27486)
Both routes read `chat_id` from the request body and passed it into `get_event_emitter` without checking the caller owns that chat. The emitter persists through `upsert_message_to_chat_by_id_and_message_id`, which resolves by primary key and takes no owner argument, so an invoked filter or action wrote into whichever chat the caller named. `/api/chat/completions` already performs this check; these two routes did not.

Adds `verify_chat_ownership`, called at the top of both handlers. It runs before the existing try block because the `except Exception` there catches HTTPException and would rewrite the 404 into a 400. Admins are exempt, matching the completions path, so deliberate cross-user operations keep working.

`local:` chat ids are allowed through: they are per-socket, the emitter suppresses database writes for them, and the socket emit targets the caller's own room. `channel:` chat ids are rejected instead. They reach the channel emitter, whose write only checks that the message belongs to the channel and never that the caller may write it, and the membership and write-access gate for channels exists solely on `/api/chat/completions`. No caller sends a `channel:` id to these two routes: the only frontend callers are in the regular chat UI, and the backend channel path dispatches through the completions handler.

Co-authored-by: manus-use <213290975+manus-use@users.noreply.github.com>
2026-07-26 17:39:11 -04:00
Classic298 a0ee66c145 fix: name the group permission switches (WCAG 4.1.2) (#27513)
`admin/Users/Groups/Permissions.svelte` contains **64** `<Switch>` instances and not one of them passes `ariaLabel`, `ariaLabelledbyId` or `id`. bits-ui renders the switch as a `<button role="switch">` whose subtree is a text free thumb, so all 64 have **no accessible name**. The visible label is a sibling `<div>` with no association to the control.

This is the worst remaining case in the admin area: 64 toggles in one dialog, many with near identical adjacent labels (Import Models / Export Models / Import Prompts / Export Prompts / Import Tools / Export Tools). A screen reader user hears 64 consecutive "switch, on" and "switch, off" with no way to tell which permission is which.

Breaks WCAG 4.1.2 Name, Role, Value (Level A).

Fix: pass the row's own label to each switch. The `ariaLabel` expression is the **same `$i18n.t()` key** as the visible text two lines above it, so the accessible name equals the visible label in every locale, which also satisfies 2.5.3 Label in Name and keeps voice control working.

`ariaLabel` rather than `ariaLabelledbyId`, which is what `chat/Settings/Interface.svelte` uses for the same row shape. The difference is that `Interface.svelte` is a singleton, whereas this component is rendered from `EditGroupModal`, which is instantiated in three places including once per group in `GroupItem.svelte`. Only one can be visible today, but nothing enforces that, and 64 hardcoded ids would fail silently the day two coexist, since `aria-labelledby` resolves to the first matching id. `aria-label` has no such failure mode and needs half the edits.

All 64 mappings were checked individually rather than assumed. The nearest preceding label is the correct one in every case, including the three rows wrapped in a `<Tooltip>` (whose `content` attribute precedes the label in source order) and the ~60 `{#if}` / `{:else if}` explanatory strings (which always follow their switch). All 64 resulting labels are distinct.

The nested sub toggles are unambiguous on their own because upstream already labelled them fully ("Import Models" rather than "Import"), so no extra scoping is needed.

Two known follow ups, deliberately not bundled:

- The warning tooltips on Tools Access, Skills Access and Automations ("Warning: Enabling this will allow users to upload arbitrary code on the server.") are attached to a non focusable wrapper `<div>`, so keyboard and screen reader users never receive them. That needs a change in `common/Tooltip.svelte` or an `ariaDescribedbyId` on `Switch`, not a naming change.
- This file is a ~14 line block repeated 64 times where only the label and permission key vary, and it wants a shared `PermissionRow` component. Extracting it here would bundle a large structural refactor into an accessibility fix and make the diff unreviewable against the claim, so it is left alone.

The diff is +164/−65 rather than 64 changed lines, because 33 of the switches exceed the 100 column print width and Prettier reflows them to the multi line form. The file is Prettier clean and compiles with no new warnings.

Severity: Serious.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-07-26 17:38:35 -04:00
Classic298 ba4c92c4f0 fix: make sidebar section headers keyboard operable (WCAG 2.1.1, 4.1.2) (#27489)
On latest `dev`, each sidebar section header in `Sidebar/Section.svelte` is a real `<button>` carrying `aria-expanded` and `aria-controls`, but it has **no activation handler**. The toggle comes only from `on:pointerup` on the wrapper inside `common/Collapsible.svelte`.

Keyboard activation dispatches a synthetic `click`, never `pointerup`, and that wrapper's own `on:click` handler calls `stopPropagation()`. So pressing Enter or Space on the header does nothing at all, while `aria-expanded` tells assistive technology this is a working disclosure control.

This affects every section in the sidebar: Models, Notes, Channels, Folders and Chats. Section state is persisted to `localStorage`, so a user whose section was collapsed on a previous visit has no keyboard way to open it again, and the content stays unreachable.

Breaks WCAG 2.1.1 Keyboard (Level A), and 4.1.2 Name, Role, Value (Level A), because the exposed expanded state belongs to a control that cannot be operated.

Fix: handle activation on the header button itself, where focus actually lands, and stop the now duplicate pointer path so a mouse click does not toggle twice. The existing inline `onChange` body is extracted to `setOpen` so the `change` dispatch and the `localStorage` write stay in one place and fire exactly once per toggle in both input modes. The adjacent "+" (`onAdd`) button already stops both `pointerup` and `click`, so it still does not toggle the section.

`Collapsible`'s wrapper cannot simply become a `<button>` instead, because its slot receives buttons from this component and others, so the fix belongs here.

`common/Folder.svelte` and `Sidebar/RecursiveFolder.svelte` have the same latent defect and are not touched by this PR.

Severity: Critical. Sidebar navigation cannot be expanded without a mouse.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-07-26 17:38:06 -04:00
Classic298 15f724b0f2 fix: give SensitiveInput a unique default id (WCAG 1.3.1, 4.1.2) (#27488)
On latest `dev`, `SensitiveInput` defaults to `export let id = 'password-input'`. The id is used both for the input itself and as the `for` target of the screen reader label rendered just above it.

There are 80 `<SensitiveInput>` usages in `src/` and only 4 pass an explicit id, so the remaining 76 all render `id="password-input"` together with `<label for="password-input">`. These collide on the same page in completely ordinary configurations: `admin/Settings/Audio.svelte` renders 4 at once with `STT_ENGINE === 'openai'` and 4 more with `TTS_ENGINE === 'openai'`, `admin/Settings/Documents.svelte` has 11, and `admin/Settings/WebSearch.svelte` has 33.

`for` resolves to the first matching element, so every label after the first points at the wrong input. In practice a screen reader user tabbing to the OpenAI TTS API key field hears the label belonging to the STT key field from a different section, and every one of those fields announces the same name. Browser password managers and any `getElementById` lookup collapse onto the first element the same way.

Breaks WCAG 1.3.1 Info and Relationships (Level A), because the programmatic label/field relationship is wrong, and 4.1.2 Name, Role, Value (Level A), because the fields do not expose their correct accessible name.

Fix: default the id to a per instance unique value. A Svelte prop default is evaluated per component instance, so each `SensitiveInput` gets its own stable id, and the 4 call sites that pass an explicit id are unaffected. `uuid` is already a direct dependency and `import { v4 as uuidv4 } from 'uuid'` is the existing pattern in the codebase, including `common/Collapsible.svelte`, which already generates a DOM id this way.

Note for self hosted setups: a `#password-input` selector in `static/custom.css` would stop matching. That selector already matched up to 8 elements at once on the Audio settings page, so it was never a reliable hook.

Severity: Serious. Every API key field in Admin Settings is mislabelled for assistive technology.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-07-26 17:37:45 -04:00
Classic298 d3802f7660 fix: make reasoning and details disclosures keyboard operable (WCAG 2.1.1, 4.1.2) (#27490)
On latest `dev`, the `title !== null` branch of `Collapsible` renders its header as a bare `<div>` whose only handler is `on:pointerup`, with the two Svelte a11y warnings suppressed above it.

`pointerup` is never dispatched by keyboard activation, and the `<div>` has no `role`, no `tabindex` and no `aria-expanded`. The header is therefore not focusable, not activatable and not announced as a control. This is the header for "Thinking..." / "Thought for N seconds", "Analyzing..." / "Analyzed", and every `<details>` block rendered from model output, via `Messages/Markdown/MarkdownTokens.svelte`, `Messages/StructuredOutputRenderer.svelte` and `chat/Controls/Controls.svelte`.

In practice a keyboard or screen reader user cannot expand any model reasoning trace, tool call detail or code interpreter block, and a screen reader reads the header as static text with no hint that anything is collapsed behind it.

Breaks WCAG 2.1.1 Keyboard (Level A), since the disclosure has no keyboard operation at all, and 4.1.2 Name, Role, Value (Level A), since it exposes neither a button role nor its expanded state.

Fix: render that header as a real `<button type="button">` with `aria-expanded` and the native `disabled` attribute, and toggle on `click`, which fires for both pointer and keyboard activation. This branch contains no `<slot />` and no interactive descendants, so a button is valid here. `block text-start` keeps the previous box and alignment behaviour, since a `<button>` otherwise defaults to `inline-block` and centred text. `disabled:cursor-default` replaces the old `{disabled ? '' : 'cursor-pointer'}` ternary, which became a no-op once this was a button, because `src/tailwind.css` applies `cursor-pointer` to every `button`. Verified in a browser that display, text alignment and rendered height match the previous `<div>`, and that a disabled header no longer shows a pointer cursor.

Switching from `pointerup` to `click` also means the header no longer toggles on right click, or when a drag starts outside it and ends inside.

The `{:else}` branch is deliberately left alone. Its `<slot />` receives buttons from `Sidebar/Section.svelte`, `common/Folder.svelte` and `Sidebar/RecursiveFolder.svelte`, so it cannot legally become a `<button>` and needs a different fix.

Severity: Critical. Model reasoning output is entirely unreachable without a mouse.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-07-26 17:37:34 -04:00
Classic298 65473b6ffa fix: stop Enter on Cancel from confirming ConfirmDialog (WCAG 3.2.2) (#27491)
On latest `dev`, `ConfirmDialog` registers `handleKeyDown` on `window` and treats **every** Enter keypress as Confirm, calling `event.preventDefault()` first so the focused control never gets its native activation.

The dialog also activates a focus trap with no `initialFocus`, so focus-trap falls back to the first tabbable node, which is the **Cancel** button. So the dialog opens with Cancel focused, and pressing Enter runs Confirm.

This is the confirm surface for Delete chat, Delete folder, Delete model, Delete knowledge base and ~40 other call sites. A keyboard user who tabs to Cancel and presses Enter deletes the thing they were trying to keep. Screen reader users are hit hardest, since they cannot see which button they are on and the control that means "back out safely" performs the irreversible action instead.

Two related paths have the same cause: Enter in the `input=true` textarea submits instead of inserting a newline, and a markdown link inside `message` (reachable via `eventConfirmationMessage` from tool `__event_call__` payloads, and via `web_search_confirmation_content`) becomes the first tabbable node, so Enter on that link confirms instead of following it.

Breaks WCAG 3.2.2 On Input (Level A): changing the focused control changes what the Enter key does, and activating a control performs a different action than the one it is labelled with. Also 2.1.1 Keyboard (Level A), since Cancel has no working keyboard activation.

Fix: let the focused control act on Enter itself, and only fall back to Confirm otherwise. Uses the same `target instanceof Element && target.closest(...)` guard already used in `Functions.svelte`, `Knowledge.svelte`, `Models.svelte`, `Prompts.svelte`, `Skills.svelte` and `Tools.svelte`. `select` is deliberately not in the list, because a native `select` does not act on Enter and excluding it would silently break confirm for the `inputType === 'select'` variant. Two stray `console.log` calls in the same function are removed.

Behaviour after this change: Enter on Cancel cancels, Enter on Confirm confirms, Enter in the textarea inserts a newline, Enter on a link follows it, and Enter anywhere else still confirms as before.

Severity: Critical. Silent, unrecoverable data loss triggered by the most ordinary keyboard interaction there is.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-07-26 17:37:21 -04:00
Classic298 2725ae6d6c fix: expose Checkbox as a checkbox with a name and state (WCAG 4.1.2) (#27494)
On latest `dev`, `common/Checkbox.svelte` renders a `<button type="button">` containing only `aria-hidden="true"` SVGs. It has no `role`, no `aria-checked` and no accessible name, and the component has no `$$restProps` spread, so a caller cannot supply a name either.

Assistive technology announces every one of these as an unnamed "button". A screen reader user cannot tell that the control is a checkbox, cannot tell whether it is on or off, and cannot tell what it toggles. The visible label is always an unassociated sibling element, for example `Capabilities.svelte` puts it in a preceding `<div>` with no `id`, and `Groups/Users.svelte` puts it in a different table cell from the checkbox.

Breaks WCAG 4.1.2 Name, Role, Value (Level A) on all three counts at once.

Fix: expose `role="checkbox"` and `aria-checked` on the control, add an `ariaLabel` prop, and pass the label text that is already in scope at each call site. `aria-checked` mirrors the component's existing icon logic exactly, so the indeterminate dash reports `mixed` rather than `false`. The `ariaLabel={ariaLabel || undefined}` shape matches the sibling `common/Switch.svelte`. Every label expression is the same one that renders the visible text next to the checkbox, so the accessible name always matches what is on screen.

Three call sites are deliberately left out of this PR, because they nest `Checkbox` inside another `<button>`, which is invalid HTML and independently broken:

- `workspace/Knowledge/KnowledgeBase.svelte` — the Checkbox's `on:change` sets `includeContent = true` and then the same click bubbles to the outer button, which flips it back with `includeContent = !includeContent`. Clicking the checkbox square is a no-op today, only the text label works. Giving it a confident name would advertise a control that does nothing.
- `workspace/common/MemberSelector.svelte` (two instances) — the inner Checkbox has no `on:change` at all and only works because its click bubbles to the row button. Naming it would create two focusable controls per row with the same name.

Both need the nesting resolved first, so that the row button carries the checkbox semantics. That is a behavioural fix and belongs in its own PR.

Severity: Serious. Affects model capabilities, default features, builtin tools, tool/filter/skill/action selectors and group membership.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-07-26 17:37:06 -04:00
Classic298 a7a2c7605b fix: give the rich text editor an accessible name (WCAG 4.1.2, 3.3.2) (#27503)
On latest `dev`, `RichTextInput` passes only `attributes: { id }` to tiptap, so the rendered contenteditable has an implicit `textbox` role and **no accessible name at all**.

The only label is the tiptap placeholder, which renders as CSS generated content in `src/app.css` via `content: attr(data-placeholder)`. Generated content never becomes an element's accessible name, so assistive technology announces the field as "edit text, blank".

This is the chat composer, the channel and thread composers, and the note editor, so it is the most used control in the product.

Breaks WCAG 4.1.2 Name, Role, Value (Level A), and 3.3.2 Labels or Instructions (Level A), since the only instruction is invisible to assistive technology.

Fix: expose the placeholder as `aria-label` on the editor element.

`attributes` is passed as a **function** rather than an object literal. The object form is evaluated once when the `Editor` is constructed and never rebuilt, but `placeholder` is deliberately runtime mutable: `channel/MessageInput.svelte` and `channel/Thread.svelte` swap it between "You do not have permission to send messages in this thread." and "Reply to thread..." once `channel` resolves, and it also changes when the interface language changes. With the object form the field would have been permanently named with whatever string happened to be set at mount, which for a channel the user *can* write to is the no-permission message. That would be worse than no name at all. ProseMirror supports the function form and re-evaluates it on every state update, and the component's existing `setPlaceholder` already dispatches an empty transaction, so the label now tracks the visible placeholder. It binds to `_placeholder`, the same value that feeds the visible text, so the two cannot diverge.

`aria-multiline` is deliberately not set. It is only valid on an explicit `textbox`/`searchbox` role, and adding `role="textbox"` would flatten the editor's inner structure so headings, lists and links inside rich text stop being exposed.

Severity: Critical. The application's primary input announces as an unnamed edit field.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-07-26 17:36:36 -04:00
Classic298 7effaa05d1 fix: name the switches in Admin Settings rows (WCAG 4.1.2, 1.3.1) (#27510)
`common/Switch.svelte` already accepts `id`, `ariaLabel` and `ariaLabelledbyId`, but **not one of the 148 `<Switch>` instances under `src/lib/components/admin/` passes any of them**.

`admin/Settings/AdminSettingRow.svelte` renders the row label as a plain `<div>` and the control in a **sibling** slot, so there is nothing tying them together. bits-ui renders the switch as a `<button role="switch">` whose subtree is a text free thumb, so it has no accessible name from any source.

A screen reader user working through Admin Settings hears a long run of "switch, on" and "switch, off" with no indication of what any of them controls.

Breaks WCAG 4.1.2 Name, Role, Value (Level A) and 1.3.1 Info and Relationships (Level A).

Fix: `AdminSettingRow` mints a per instance id, puts it on the label element, and hands it to the default slot, so each row's switch can point at the label that is already rendered next to it. This is the pattern `chat/Settings/Interface.svelte` already uses by hand in 45 places, hoisted into the shared row component so call sites stop hand authoring ids.

`aria-labelledby` rather than a wrapping `<label>`: per HTML-AAM a `<button>` takes its name from `aria-labelledby`, then `aria-label`, then its own subtree, never from an associated `<label>`. `chat/Settings/Subagents.svelte` already wraps two switches in a `<label>` and they are still unnamed, which is the same trap. Using the existing label element also guarantees the accessible name is byte identical to the visible text, which keeps voice control working.

The `description` paragraph deliberately sits outside the referenced element, so verbose help text is not pulled into the name.

Scope: this covers the **72** switches that live inside an `AdminSettingRow`, which is every switch that flows through the shared row component. There are no rows containing more than one switch, so nothing is silently skipped.

The remaining 76 admin switches are not in this component and are not touched. 64 of them are in `admin/Users/Groups/Permissions.svelte`, which hand rolls its own row markup, and the other 12 are per entity toggles in lists and dropdowns where the label is a dynamic row name. `Permissions.svelte` is the worst remaining case, 64 toggles with near identical adjacent labels, and it needs either its own labelling pass or a conversion to `AdminSettingRow` that changes its visual styling. Either way that is not an accessibility only diff and belongs in its own PR.

All 12 touched files compile with the Svelte compiler with no new warnings and are Prettier clean.

Severity: Serious. Admin Settings is unusable with a screen reader.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-07-26 17:35:34 -04:00
Classic298 94b1b7e6b6 fix: close Playwright pages and browser on failure in SafePlaywrightURLLoader (#27526)
`SafePlaywrightURLLoader` opened a new Playwright page for every URL and never closed it, and it only closed the browser after the URL loop finished normally. Pages therefore piled up for the whole batch, and any early exit (a raised error with `continue_on_failure=False`, or the caller abandoning/cancelling the generator mid-search) skipped `browser.close()` entirely.

With `PLAYWRIGHT_WS_URL` pointing at a remote Playwright server this leaks sessions on that server: navigation and route timeouts on slow or bot-protected pages leave pages and browser connections open until the server is restarted, which degrades every later web search.

Both `lazy_load()` and `alazy_load()` now scope the page to the per-URL loop body and the browser to the whole loop using their context managers, so each page is closed as soon as its URL is done and the browser is closed on success, on failure, and on cancellation. Closing a page also disposes the context implicitly created by `new_page()`. Exception handling is unchanged: a close error raised while `continue_on_failure` is set is still caught, logged, and the loop continues.

Fixes #25880
2026-07-26 17:35:13 -04:00
Classic298 225e238856 fix: only route PDFs and images to the PaddleOCR-VL loader (#27529)
When `RAG_DOCUMENT_LOADER_ENGINE` is set to `paddleocr_vl`, the dispatch branch in `Loader._get_loader` checked only the engine name and a non-empty token, so every uploaded file was handed to the PaddleOCR-VL loader regardless of its type. Text based uploads such as `.md`, `.txt` and `.csv` were base64 encoded and posted to the `/layout-parsing` endpoint tagged as PDFs, and the API rejected them with `422 Unprocessable Entity` ("PDFium: Data format error"), so those files never indexed at all.

The loader already knows which extensions it can handle: it tags images with `fileType: 1` and treats everything else as a PDF. That list is now a module level constant, and the dispatch branch gates on `['pdf'] + images`, the same way `mistral_ocr`, `datalab_marker`, `document_intelligence` and `mineru` already limit themselves. Deriving the gate from the loader's own list keeps the two in sync, so a file can never be admitted by the gate and then mislabelled as a PDF on the wire. Everything outside that set falls through to the default loader chain, so `.md` and `.txt` load as text, `.csv` through `CSVLoader`, `.docx` through `Docx2txtLoader`, and so on.

The branch also never checked `PADDLEOCR_VL_BASE_URL`. With the URL cleared, `PaddleOCRVLLoader` raised `ValueError` from its constructor and the upload failed outright instead of falling back. Both settings are now required for the branch to be taken, matching how the other engines guard their own configuration.

Fixes #24988
Fixes #26759
2026-07-26 17:34:59 -04:00
Classic298 f517cc7172 fix: apply the verified-user role gate to WebSocket authentication (#27537)
The Socket.IO handshake and the terminal WebSocket route each reimplement JWT authentication instead of going through the HTTP dependency chain. Both verified that the token decoded, that it had not been revoked, and that the user row existed, but neither applied the role check that `get_verified_user` enforces on every HTTP route, so any role outside `user` and `admin` was accepted.

That splits authorization across two planes. Deactivating an account by setting its role to `pending` takes effect immediately over HTTP, which returns 401, while the same JWT still opens a WebSocket. Changing a role disconnects the account's live sockets but does not revoke its token, so the client simply reconnects and gets a fresh session. Until the token expires, four weeks by default, a deactivated account keeps its channel rooms and can still read and write any note it holds an access grant on through the collaborative document handlers.

Resolve the user once, in `get_verified_user_by_token`, and route both WebSocket entry points through it. The role set moves into `VERIFIED_USER_ROLES` so the HTTP and WebSocket gates cannot drift apart, which is the underlying cause rather than either call site on its own. This also replaces five copies of the decode, revocation check and user lookup sequence.

`user-join` now resolves the user instead of reusing the identity cached in `SESSION_POOL`, which costs one extra query per handshake. Gating on the cached role would make the authorization decision depend on every future role-mutation path remembering to tear down the session pool, and that is precisely the invariant that failed here.
2026-07-26 17:27:54 -04:00
Classic298 d1aa812d80 i18n: complete de-DE translations (#27448)
* i18n: complete de-DE translations

Fill in all 544 untranslated (empty) strings in the German locale and add
the two keys that were missing entirely ("Response Auto-Scroll" and
"Follow assistant responses as they are generated.").

Wording follows the conventions already used in the file: formal "Sie"
address for user-facing sentences, infinitive phrasing for labels and
buttons, third-person descriptive phrasing for setting descriptions, and
the established terminology (Kontextverdichtung, Erinnerungen,
Wissensspeicher, Werkzeuge, Chunk, Embedding, Skills, Pipelines).
Ambiguous strings were resolved against their usage in the Svelte
components, e.g. "at"/"Through" (schedule and heatmap tooltips),
"Runs"/"runs" (automation runs vs. tool invocations), "Current"
(active chat) and "Selected" (model filter).

* i18n: fix de-DE wording and two pre-existing plural bugs

Review pass over the German locale:

- "Claim" and "DN" are masculine: "Claim, der ..." instead of "Claim,
  das ...", "Passwort für den Bind-DN", "Base DN, der ...".
- "hinzufügen" governs the dative, matching the existing string
  "... fügen Sie sie zuerst dem Arbeitsbereich "Wissen" hinzu."
- "Beschränkt oder schließt Domains ... aus" was a zeugma; the separable
  prefix only belongs to "schließt".
- Sub-agent settings render as label + input + unit suffix on one line,
  so the label and suffix no longer repeat each other.
- The built-in tool descriptions are infinitive, so the notification one
  is too.
- Align wording with terms already used in the file: Assistentennachrichten,
  Benutzernachrichten, Vervollständigungen, Tool-Server, Wissensspeicher,
  lexikalisch. Normalize the few German typographic quotes to the ASCII
  quotes used everywhere else.
- The username setting claimed the chat shows "Sie", but "You" is
  translated as "Du".

Also fixes bugs that predate these translations: "Starting in {{count}}
minutes" had the raw "minutes_one"/"minutes_other" suffix in its value,
and the singular and plural of "Ran {{COUNT}} analysis/analyses" were
swapped.
2026-07-24 17:59:54 -05:00
Classic298 e32c6743ba docs: align security policy framing with project ownership (#27431)
The security policy described Open WebUI as "a small volunteer team" and "a volunteer- and community-driven project", and explained response times as a shortage of capacity. Read by enterprise evaluators, security researchers and third parties trying to impose disclosure timelines, that wording makes the project look informal, under-resourced and externally steerable, which is the opposite of the position the policy is meant to hold.

Open WebUI is led and maintained by a small core team with clear ownership of the security process. This updates the wording to say that, and reframes response times as prioritisation across the project rather than a capacity shortfall. No rule, scope, commitment or timeline changes: the reporting channel, the disclosure schedule, the credit rules and the expected timeframe all stay exactly as they were.

Also removes the implicit first-come-first-served promise in the follow-up paragraph, which contradicted the severity-based prioritisation stated two paragraphs later, and bumps the last-updated date.
2026-07-24 12:37:23 -04:00
Classic298andClaude fe4b319428 fix: deny chained access to unregistered base models for non-admins (#26905)
A workspace model shared publicly could be used by any user even when its
base model was private. Unregistered base models (no row in the model
table) are admin-only for direct use — get_filtered_models hides them from
non-admins and check_model_access rejects them — but has_base_model_access
treated a missing row as "no ACL" and allowed the chained request through.

has_base_model_access now takes the caller's role and only allows an
unregistered base model hop for admins, so a shared preset can no longer
reach a base model the caller could not use directly. Registered base
models keep their existing grant-based enforcement.


Claude-Session: https://claude.ai/code/session_018toPfJW1hMXAhokGaL43Ep

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-24 01:32:32 -05:00
Classic298 f7e7f32102 fix: honor Admin UI web loader settings in get_web_loader (#26749)
Since the config refactor, get_web_loader dispatched on the WEB_LOADER_ENGINE module constant, which is read from the environment once at import time. The engine selected in the Admin UI is stored under web.loader.engine in the config table but was never consulted, so UI-configured loader engines (external, playwright, firecrawl, tavily, microsoft_web_iq) were silently ignored and the built-in SafeWebBaseLoader always fetched pages directly. The same applied to the per-engine settings such as the external web loader URL and API key. This breaks egress-restricted deployments that rely on an external web loader: pages are fetched directly from the container and fail with errors like "Network is unreachable" even though an external loader is configured.

Pass the DB-backed loader settings into get_web_loader from both call sites, web search in process_web_search and web fetch via get_loader, and resolve every engine setting from them, keeping the module-level env constants as the fallback for keys that were never saved. Also initialise WebLoaderClass so an unknown engine raises the intended ValueError instead of an UnboundLocalError.

Fixes #26747
2026-07-24 01:30:47 -05:00
Classic298 18719fef9c fix: malformed WEB_FETCH_FILTER_LIST entry blocking all web fetches (#26910)
Docker compose list-form environment syntax passes quotes through verbatim, so WEB_FETCH_FILTER_LIST="" reaches the backend as two literal quote characters rather than an empty string. Config parsing turned that into the filter entry '""', which has no "!" prefix and therefore landed in the allow list. A non-empty allow list requires every host to match one of its entries, and a quotes-only pattern can never match a hostname, so every fetch_url and web loader request was rejected with "URL blocked by filter list" and surfaced to the user as "The URL you provided is invalid".

get_allow_block_lists now strips surrounding quote characters from each entry and drops entries that are empty after normalisation. Quoted but otherwise valid entries such as "example.com" or !"example.com" now behave as their unquoted forms, and garbage entries no longer convert the default blocklist into a match-nothing allowlist that blocks everything.

Fixes #26908
2026-07-24 01:27:24 -05:00
Classic298 f89b501985 fix: access-check note entries in get_accessible_folder_files (#26739)
get_accessible_folder_files is the server-side filter that reduces a folder's attached-knowledge list (and, once #26723 lands, a direct model's) to the entries the caller may read, before that list is handed to the builtin knowledge tools as `__model_knowledge__`. It validated `file` and `collection` entries but passed `note` entries through unchecked (they fell into the `else` keep-as-is branch), even though notes are a first-class attached-knowledge type that flows through this list.

No current caller is exploitable, because every note consumer (`query_knowledge_files`, `view_note`, and the legacy retrieval path) independently re-checks note access before returning content. But relying on each consumer to remember that check is exactly the fragility this helper exists to remove, and the same `_has_read_access_to_file` membership short-circuit that makes an unvalidated `file` entry dangerous would turn any future note path that trusts list membership into an IDOR. Validate notes here so the filter enforces its own contract instead of leaning on downstream re-checks.

A note entry is now kept only when the caller owns it or holds a read grant. Notes are private by default and carry no self-grant, so ownership is checked explicitly alongside the grant lookup. Admins still bypass all checks and genuinely unknown types are still kept as-is.

Related: #26723
2026-07-24 01:18:43 -05:00
Classic298 585b704597 fix: clear token cookie on 401 auth redirect to stop login flash loop (#26751)
Since v0.10.0 a global fetch interceptor redirects to /auth and clears
localStorage.token whenever an authenticated backend request returns 401.
The OAuth callback cookie ("token", set with httponly=False so the
frontend can read it) is left behind. The auth page's oauthCallbackHandler
then immediately signs the user back in from that cookie and navigates to
"/", where the next 401 triggers the redirect again. The result is an
endless /auth and / ping-pong that renders as uncontrollable screen
flashing, and as a PWA stuck on a flashing splash screen when SvelteKit's
update check turns each navigation into a full page reload. Affected
users could only recover by clearing cookies, which matches the reports.

Clear the token cookie together with localStorage when redirecting, so
/auth stays on the login form and the user can sign in again normally.

Fixes #26731
2026-07-24 00:59:43 -05:00
Classic298andClaude Opus 4.8 e398ba3506 fix: don't seed non-persistent config keys (oauth.* with flag off) (#26928)
seed_defaults inserted a row for every key in DEFAULT_CONFIG regardless
of whether the DB is authoritative for it. With ENABLE_OAUTH_PERSISTENT_CONFIG
off, the oauth.* keys were seeded from the then-current (often empty) env
values. Enabling the flag later made those stale rows override live env vars
(e.g. ENABLE_OAUTH_SIGNUP=true stopped taking effect) and further env changes
were never picked up.

Skip keys where persistent_enabled_for() is false, matching the masking the
read paths (get/get_many/get_namespace/get_all) already apply.


Claude-Session: https://claude.ai/code/session_01Vr2RCYUTXCtgtV4WMUCK86

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 00:58:30 -05:00
Classic298 b6acd3cc45 fix: web speech STT repeating previous transcriptions and inserting text on cancel (#26793)
The VoiceRecording component stays mounted (hidden) between recordings, and the web speech engine accumulated every session's transcript into the never-reset transcription variable. Each new recording therefore confirmed all previous utterances again, so the inserted text repeated once per session and previously deleted text reappeared in the input.

Additionally, cancelling a recording (X button, Escape or a recognition error) called stopRecording(), which stops the SpeechRecognition instance and fires its onend handler, which unconditionally confirms the transcription. Cancelled recordings therefore still inserted the accumulated transcript.

Reset the transcription at the start of each web speech session and detach the onend handler on cancel so cancelled recordings no longer confirm.

Fixes #26784
2026-07-24 00:40:02 -05:00
Classic298 a35b37adcd fix: keep chats shared with an admin readable when ENABLE_ADMIN_CHAT_ACCESS is off (#27127)
get_chat_by_id sent admins down a branch that returned the chat only when
ENABLE_ADMIN_CHAT_ACCESS was on, or the chat was internal, and never fell
through to the access-grant and shared-folder checks. With the setting off,
an admin was therefore denied a chat that had been deliberately shared with
them, either directly or through a shared folder, while any non-admin holding
the same grant could open it. The admin role removed access the user had been
given rather than only closing the admin-only path.

Try the admin path first, then let everyone fall through to the grant and
folder checks. ENABLE_ADMIN_CHAT_ACCESS=false still closes the admin-only
route to other users' chats, and internal chats stay reachable.
2026-07-24 00:02:12 -05:00
Classic298andlegobattman 9b635d8f3d fix: calendar attendee RSVP correctness — server-derived status and hide declined invites (#27007)
* fix: let only the attendee set their own calendar RSVP status

set_attendees took each attendee's status from the caller-supplied value, so an
event organiser could set another user's RSVP (for example to 'accepted') on
create or update. RSVP is meant to be self-service: the /events/{id}/rsvp
endpoint already scopes status changes to the calling user.

Derive attendee status server-side instead of from the request. An existing
attendee keeps the status they set via RSVP and a newly added attendee starts
'pending'; any caller-supplied status is ignored. Event edits no longer reset
attendees' existing responses.

Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>

* fix: hide declined calendar invites from the attendee view

`get_events_by_range` surfaced every event where the user is an attendee regardless of their RSVP status, so declining an invite left it in the calendar with no way to remove it. Exclude `declined` attendee rows from the attendee branch, so a decline now removes the event from the user's own view while pending, accepted and tentative invitations still surface.

Co-Authored-By: legobattman <302282032+legobattman@users.noreply.github.com>

---------

Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>
2026-07-24 00:01:16 -05:00
Classic298andClaude 48f78ca58d fix: prevent startup crash when function/tool has null user_id (#26850)
The Function and Tool database columns declare user_id as a nullable
String column, but their Pydantic read-models required a non-null
string. A record with user_id NULL therefore raised a
pydantic ValidationError inside get_functions()/get_tools(), which run
during install_tool_and_function_dependencies() at app startup —
crashing the whole application and blocking all chat completions.

Make user_id Optional in the read/response models so such records
validate gracefully (user is already rendered as None downstream when
the id has no matching user) instead of taking down startup.


Claude-Session: https://claude.ai/code/session_01Y4RRUNq7ZUFkRWbWPkDw3m

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-24 00:00:37 -05:00
Classic298 ec18ce2ca0 fix: persist access_grants.allow_groups in default permissions (#27124)
AccessGrantsPermissions only declared allow_users, so allow_groups was
missing from the model backing the default user permissions endpoints.
Pydantic ignores undeclared fields, so POST /users/default/permissions
dropped allow_groups before model_dump(), it never reached the persisted
user.permissions config, and fill_missing_permissions restored it to the
default on the next read. Turning "Allow Sharing With Groups" off in
Admin Settings silently reverted to on, while the same toggle worked when
set per group, since group permissions are stored as a plain dict.

GET /users/default/permissions and /users/default/permissions/defaults
dropped it from their responses for the same reason.

Declare allow_groups on the model so it round-trips, matching the
access_grants block in DEFAULT_USER_PERMISSIONS. It defaults to True, so
existing payloads that omit it are unaffected.
2026-07-23 23:53:59 -04:00
Classic298 c609ec4115 fix: require message authorship for standard-channel message edit and delete (#27197)
The channel message update and delete handlers enforced authorship only on group and dm channels. On standard channels the else branch accepted any caller holding write access on the channel, so a member who could post could also edit or delete messages authored by other members. Because the update form binds content, data and meta, and the model layer never touches message.user_id, an edited message kept the original author's attribution, so another member's message could be rewritten under their name.

Write access on a channel is the capability to post, not a moderation capability, and the frontend gates the edit and delete controls on authorship (message.user_id === user.id, or admin) for every channel type. The group and dm branch already encodes this with an explicit authorship check. Apply the same rule to the standard branch: the caller must hold write access on the channel and be the message author, unless they are an admin. Pinning is unchanged, since it is exposed to every member by design.
2026-07-23 23:40:03 -04:00
Classic298 c895490aa8 fix: blank chat messages on Safari caused by content-visibility virtualization (#26805)
Since v0.10.0 chat messages are virtualized with content-visibility: auto to skip rendering of off-screen messages. Safari's implementation of content-visibility has known paint bugs (WebKit bugs 277573, 281570 and 283846) that can leave the contents of a message unpainted even when it is on screen. On iOS this makes assistant responses render as empty, both in Safari and as a PWA, while the same chats render fine in Chromium and Firefox. This matches the regression window reported in #26712, which appeared when upgrading from 0.9.6 to 0.10.2.

Detect Safari (including all iOS browsers, which use WebKit) with the same user agent check already used in MessageInput and ShareChatModal, and skip the virtualization class there. Safari falls back to rendering all messages like before v0.10.0, while other engines keep the optimization. Verified with a spoofed Safari user agent that messages render without the virtualization class and with content-visibility resolving to visible, while Chromium keeps content-visibility: auto.

Fixes #26712
2026-07-23 23:38:18 -04:00
Classic298 b940cd529b fix: matplotlib SyntaxError in sandboxed Pyodide code execution (#26800)
The sandboxed Pyodide host (used when ENABLE_PYODIDE_FILE_PERSISTENCE is
disabled, the default) embeds its script in a String.raw template. The
matplotlib show() override was written with '\\t' escapes as if in a normal
string context, but String.raw preserves them verbatim, so the iframe's
script parser turns them into literal backslash-t characters in the
generated Python source. Pyodide then fails to compile any code that
triggers the matplotlib patch with "SyntaxError: unexpected character
after line continuation character", which is why matplotlib only worked
with the file persistence worker path enabled.

Use single '\t' escapes instead: String.raw keeps them as-is in the
script text and the sandbox's JS parser produces real tab indentation,
matching the working implementation in pyodide.worker.ts.

Fixes #26660
2026-07-23 23:34:26 -04:00
Classic298 0f8d12201c fix: empty assistant message content in action function body (#26798)
Assistant responses are now stored as structured output items on message.output, with message.content left empty. The action payload built in chatActionHandler still sent message.content only, so action functions received assistant messages with an empty content property. Derive the content from the structured output via getOutputText, falling back to message.content, matching how the rest of Chat.svelte resolves assistant text.

Fixes #26672
2026-07-23 23:34:14 -04:00
Classic298 7ef0530b24 fix: handle urllib3-future 4-element socket options in SSRF-safe web loader (#26796)
_ssrf_safe_new_conn unpacks each entry of self.socket_options straight into socket.setsockopt(), which accepts exactly 3 positional arguments. urllib3-future, a drop-in fork that shadows the urllib3 package whenever it is installed (for example as a dependency of niquests pulled in through a tool or function's requirements), declares its default socket options with a per-protocol 4th element: [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1, "tcp")]. Its own _set_socket_options() strips that element before calling setsockopt(), but our override does not, so with urllib3-future present every synchronous web fetch (fetch_url, web search loading) fails on connect with "TypeError: setsockopt() takes exactly 3 arguments (4 given)" and returns empty content.

Mirror urllib3-future's handling in the override: for 4-element options whose last element is a protocol string, apply "tcp" options truncated to the first 3 elements and skip "udp" options (all sockets created here are SOCK_STREAM). Plain 3-element options, and any other shapes stock urllib3 would accept, are passed through unchanged, so behavior with stock urllib3 (which only ever uses 3-element tuples) is identical.

Verified locally: with urllib3-future installed the loader previously raised the TypeError on every URL and now fetches successfully; with stock urllib3 2.3.0 and 2.7.0 fetches behave the same before and after.

Note: #26015 reported this same crash but attributed it to stock urllib3 2.x, which only uses 3-element tuples; the 4-element form comes from urllib3-future shadowing urllib3.

Fixes #26791
2026-07-23 23:33:55 -04:00
Classic298 504e724fde fix: detect bare pipe alternation as regex in grep_knowledge_files (#26795)
is_regex_pattern only recognized the BRE-escaped form \| and not a bare |,
so a pattern like "Jornak|Silverlake|Orissa" was treated as one literal
string (including the pipe characters) and silently returned no matches.
This contradicted the tool docstring, which explicitly advertises
"error|warn" as an auto-detected regex example, and misled models into
concluding the searched terms were absent from the file.

Checking for a bare | also covers the escaped form, since \| contains |,
and normalize_regex already converts escaped pipes before compilation.
Literal patterns without regex metacharacters are unaffected.

Fixes #26781
2026-07-23 23:33:35 -04:00
Classic298 acf586c006 fix: resolve the web loader parser per URL instead of locking in the first one (#27367)
SafeWebBaseLoader._unpack_fetch_results assigned the resolved parser to the parser parameter itself, so the None check only ran for the first URL. In a mixed batch every later document was parsed with whatever the first URL happened to select: an .xml feed first meant all following HTML pages went through the xml parser (broken text extraction), and an HTML page first meant .xml URLs were parsed as HTML. Web search regularly fetches mixed batches, so this silently degraded extraction quality depending on result order.

The parser is now resolved per URL; an explicitly passed parser still applies to the whole batch as before. Verified with mixed xml/html batches in both orders and with an explicit parser override.
2026-07-23 21:35:27 -04:00
Classic298andClaude 656a848043 perf: halve Redis round trips on model resolution and socket pools (#27225)
When WEBSOCKET_MANAGER=redis, app.state.MODELS and the socket session/
usage pools are Redis-backed dicts, so every membership test and
getitem is a network round trip:

- generate_chat_completion checked `model_id not in models` (HEXISTS)
  and then read `models[model_id]` (HGET) on every chat completion.
  A single .get() now serves both, with the same not-found error.
- The direct-connection branch spread the pool with `{**MODELS, ...}`,
  which iterates keys() then fetches each value — HKEYS plus one HGET
  per model. dict(MODELS.items()) issues a single HGETALL instead.
- get_user_ids_from_room called SESSION_POOL.get(sid) twice per
  session (once to filter, once for the value); the usage handler
  checked membership then fetched the same key. Both now do one
  lookup.

In non-Redis mode these are plain dicts and behavior is identical.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 21:32:01 -04:00
Classic298 484fb61743 perf: make streamed content accumulation genuinely linear (#27359)
The streaming handler's content accumulator is a closure cell (declared nonlocal in stream_body_handler), and CPython's in-place string append optimization only applies to plain local variables (STORE_FAST), never to cell variables (STORE_DEREF). The content += value form introduced in #27231 therefore still allocates and copies the full accumulated string on every delta, exactly like the f-string it replaced; whether that copy is cheap or expensive is up to the allocator, and measurements swing accordingly (6 to 83 ms of pure copying for a 400 KB response on Python 3.12, against 0.3 ms for this patch).

Accumulate the deltas in a list instead and join once at the single read site (publish_chat_finished_event at stream end). List append is amortized O(1) with no dependence on reference counts, bytecode specialization or allocator behaviour, so accumulation is O(n) by construction. The non-str fallback keeps the previous f-string coercion semantics.

Verified end to end against a mock SSE upstream: a streamed chat with a think-tag block plus 40 content deltas produces output items, message text, reasoning text and usage identical to current dev, with no errors in the server log.
2026-07-23 18:17:41 -05:00
Classic298 d0f759ce40 fix: capture uncompressed response bodies in audit logs (#27369)
CompressMiddleware was registered before AuditLoggingMiddleware. Starlette prepends on add_middleware, so the audit layer ended up outside compression and, at the REQUEST_RESPONSE level, recorded the zstd/brotli/gzip bytes of every response, decoded with errors='replace'. Any client that sent Accept-Encoding (i.e. every browser) therefore produced audit entries whose response_object was unreadable mojibake.

Registering the audit middleware before the compression middleware places it inside compression, so it observes the response body exactly as the route produced it while the client still receives the compressed stream.

Verified with a stacked ASGI harness: in the old order the captured body is not parseable; in the new order the captured body round-trips as the original JSON and the client response stays compressed.
2026-07-23 18:09:40 -05:00
Classic298 3d45947053 fix: report sub-second timings in the X-Process-Time header (#27368)
The header value was truncated with int(), so every request faster than one second reported X-Process-Time: 0 and the header carried no information for exactly the requests it is meant to describe. Emit fractional seconds with microsecond precision instead, matching the pre-ASGI-refactor behavior where the raw float was sent.
2026-07-23 18:09:23 -05:00
Classic298 e0918ddb40 perf: stop the audit middleware from re-running the whole auth pipeline (#27373)
With audit logging enabled, every audited request authenticated twice. The route dependency resolved the user once, and then _log_audit_entry called get_current_user again in the request's finally block: a second JWT decode, two more Redis revocation lookups, a second user row fetch with pydantic validation and, crucially, a second fire-and-forget last-active write transaction per request.

get_current_user now stashes the resolved user on the scope-backed request state (the same mechanism the auth middleware already uses for request.state.token), and the audit middleware reuses it, falling back to the old resolution only when no user was stashed (e.g. routes without an auth dependency). While in the file, the audit path patterns are compiled once in the constructor instead of per request, and the always-log endpoint set is a class attribute instead of a per-call literal; both are fixed for the process lifetime.

Benchmark:

| metric | before | after |
| --- | --- | --- |
| audit auth resolution, CPU floor (JWT decode + user validate only) | 16.7 us | 0.24 us |
| extra work per audited request | 2 Redis GETs + 1 user SELECT + 1 last-active write | none |

The before column understates the saving: it excludes the Redis and DB round trips listed in the second row, which dominate in real deployments.

Functionally verified with a stacked ASGI harness: when the route resolves a user the audit entry carries that user and the auth pipeline is not invoked again; without a stashed user the fallback path still resolves and logs correctly; the skip matrix (exclusions, whitelist mode, always-log auth endpoints, unauthenticated and non-audited methods) is unchanged.
2026-07-23 18:09:04 -05:00
Classic298 699d512e2f perf: drop redundant session.refresh calls after commit across the model layer (#27381)
Both session factories run with expire_on_commit=False, so ORM objects keep their attribute values after commit. Every session.refresh issued right after a commit therefore re-SELECTed a row whose values the session already held, including full chat JSON blobs and user settings, purely to overwrite identical data. Fifty such calls existed across the model layer, covering nearly every write path in the app (chat inserts, title updates, pin/archive toggles, user role and settings updates, tool, prompt, function, model, file, tag, feedback, memory, automation and grant writes).

All fifty are removed. The only refreshes with an actual job were the two update-then-reload paths in tools and skills, where a Core UPDATE statement bypasses the identity map; those now use session.get(..., populate_existing=True), which guarantees a fresh row in one SELECT whether or not the row was already present in the session (the previous code issued get plus refresh, two SELECTs, on the default configuration).

Benchmark (real SQLite DB, per write):

| write path | before | after |
| --- | --- | --- |
| chat title update, ~600 KB chat blob | 2.08 ms | 1.24 ms |
| user role update, small row | 1.21 ms | 0.68 ms |

On Postgres each removed refresh is additionally a network round trip. The chat-blob case also skips re-parsing the entire JSON document per write.

Functionally verified against a fresh database: user insert, role and settings updates, chat insert (including the server-default meta column, which is always provided client-side), title update and pin toggle, tool insert and the Core-update reload path, tag insert and the prompt insert flow that pins version_id after history creation all return correct values and persist correctly.
2026-07-23 18:08:00 -05:00
Classic298 6b655689cc perf: cut repeated per-model work out of model list assembly
get_all_models runs on every models refresh and, without the base-models cache (off by default), on every /api/models request. Several of its costs multiplied by the model count for no reason:

- The active action and filter id sets were derived from get_functions_by_type, which loads full function rows including plugin source and validates them, only for the ids and is_global flags. A generalized column-only query now returns (id, is_global) tuples; the existing filter-specific helper delegates to it.
- Action priorities were computed inside the per-model sort key, constructing a pydantic Valves object per action per model; with global actions in every model's list that was models x actions constructions per refresh. Priorities are now memoized per action.
- Global action and filter item dicts were rebuilt per model from the same modules. The item lists are now built once per function and shallow-copied per model, keeping per-model dicts independent exactly as before (nested values were already shared).
- Deactivated base-model overrides were dropped with models.remove, a linear scan and shift per removal; removals are now collected and filtered out in one identity-based pass, preserving list.remove's exact object semantics.
- RedisDict.set fingerprinted the payload by serializing the already-serialized mapping a second time plus a sha256; a direct dict comparison against the last written mapping has the same skip semantics without re-serializing anything.
- /api/models did tag normalization and profile-image stripping for every model before access filtering discarded the invisible ones, and always evaluated a json.dumps debug f-string; the work now runs only on visible models and the debug line is gated on the log level. The duplicate-id dedup keeps its position before filtering so the effective-model semantics are unchanged.

Benchmark:

| metric | before | after |
| --- | --- | --- |
| model-cache fingerprint, 200 models | 45 us | 1.4 us |
| action priority Valves builds, 200 models x 4 global actions | 0.37 ms (800 builds) | 0.002 ms (4 builds) |
| function-table payload for id sets | full rows incl. source | (id, is_global) tuples |

Functionally verified: the column-only id query matches the full-row query for actions and filters including inactive exclusion, and the fingerprint skip logic writes on first set, skips identical payloads, updates plus deletes stale keys on change and clears on empty, against a scripted fake Redis.
2026-07-23 19:05:32 -04:00
Classic298 310ae91302 perf: make tools defer_content real and batch tool and knowledge access filters (#27387)
Tools.get_tools(defer_content=True) contained the literal dead statement "stmt = stmt": the deferral was a no-op, so every tools listing loaded the full Python source of every tool (five caller sites pass defer_content=True expecting the optimization: the tools list endpoints and the user and group permission overviews). The listing now selects every column except content, and ToolModel.content becomes optional to represent deferred rows; router projections are content-less response models, so nothing downstream reads the source on these paths.

On top of that, get_tools_by_user_id issued one grant query per non-owned tool and Knowledges.get_knowledge_bases_by_user_id did the same per knowledge base (the latter also sits inside per-file access checks). Both now resolve grants for all non-owned rows in a single get_accessible_resource_ids call, the same batch helper the model listing already uses.

Benchmark (real SQLite DB):

| metric | before | after |
| --- | --- | --- |
| tools listing, 33 tools x ~200 KB source | 5.13 ms | 3.18 ms |
| grant queries per accessible-tools call, N non-owned tools | N | 1 |
| grant queries per accessible-KBs call, N non-owned KBs | N | 1 |

The listing row scales with source size; on Postgres the deferral additionally avoids shipping every tool's source over the wire per listing, and each removed grant query was a real round trip.

Functionally verified: deferred listings match full listings field for field with content None, grants included and router projections working; access filtering returns exactly owned plus granted tools and knowledge bases and nothing for strangers; full (non-deferred) reads still carry the source.
2026-07-23 17:51:07 -05:00
Classic298 5b518cbe43 perf: list knowledge base file metadata without loading extracted file contents (#27386)
Knowledges.get_file_metadatas_by_id fetched full File rows, whose data column carries the entire extracted text of each document, validated each into a FileModel and then threw everything except id, hash, meta and the timestamps away. The function backs every knowledge base detail view and runs again after every file add or remove (eight call sites in the knowledge router), so rendering a filename list for a 50-file knowledge base parsed tens of megabytes of JSON per request.

The listing now selects exactly the five columns the response needs, joined through KnowledgeFile, mirroring the column-only helper that already existed in the files model for id-based lookups.

Benchmark (real SQLite DB, 50 files with ~200 KB extracted text each):

| metric | before | after |
| --- | --- | --- |
| KB file metadata listing | 14.2 ms | 1.20 ms |

The gap widens linearly with file size and count since extracted contents no longer get read, parsed or validated at all.

Functionally verified: output matches the old implementation field for field on all 50 files and an unknown knowledge base still returns an empty list.
2026-07-23 17:50:55 -05:00
Classic298 d67bc4ffcd perf: batch the file access check queries (#27383)
has_access_to_file runs for every non-owner file GET, per RAG file check and per shared-chat or model-attached file. Its final step called Models.get_models_by_user_id, which issued one grant query per non-owned workspace model, so a single file check on an instance with M workspace models cost M grant queries plus a group query, with the deny path always paying full price. Its collection_name step listed every knowledge base the user can access (itself one grant query per knowledge base) just to scan the list for one id. And get_accessible_folder_files repeated the whole pipeline per folder entry, refetching the caller's group memberships every time.

Three changes, all using parameters and helpers that already exist:
- Models.get_models_by_user_id resolves grants for all non-owned models in one get_accessible_resource_ids call and accepts prefetched user_group_ids.
- The collection_name check fetches the one referenced knowledge base and performs a single owner-or-grant check with the already-resolved group ids, preserving the write-requires-owner guard exactly (including its short-circuit before any grant query).
- get_accessible_folder_files resolves group ids once and threads them through every per-entry check.

Benchmark:

| metric | before | after |
| --- | --- | --- |
| filter loop CPU, 300 workspace models (queries stubbed) | 47 us | 19 us |
| grant queries per file-access check, M workspace models | M | 1 |
| group membership queries per folder listing, F files | F | 1 |

The stubbed CPU row understates the win: each removed query in the other two rows was a real database round trip.

Functionally verified with stubbed accessors: owned plus granted models are returned with owned ids excluded from the batch query; model-attached file access resolves through the batched path; the collection_name path does one KB fetch and one grant check with no full listing; a missing KB falls through; write access via a KB still requires the KB owner to own the file and short-circuits before the grant query; folder listings fetch groups exactly once.
2026-07-23 17:50:08 -05:00
Classic298 d5f099a5d4 perf: cut per-request database session overhead (#27385)
Two independent sources of fixed per-request cost:

The async SQLite engine was created with pool_pre_ping=True. A pre-ping guards against server connections dropped by timeouts or restarts, which cannot happen to a local SQLite file; each ping still costs a hop into the aiosqlite worker thread plus a SELECT 1 on every connection checkout, and with session sharing off a single request checks out a connection for every model-layer call it makes. The Postgres engines keep their pre-ping, where it is actually protective.

CommitSessionMiddleware unconditionally ran ScopedSession.commit() plus remove() after every HTTP request. The scoped registry instantiates a session on first access, so on the vast majority of requests (which never touch the sync session, per the middleware's own docstring) this built a Session, opened and committed an empty transaction and tore everything down for nothing. The middleware now checks ScopedSession.registry.has() first: requests that used the sync session are committed and removed exactly as before, on success and on the rollback path alike, and idle requests skip the machinery entirely.

Benchmark (real SQLite database):

| metric | before | after |
| --- | --- | --- |
| user row fetch incl. session + connection checkout | 681 us | 514 us |
| idle-request sync session work (create + empty commit + teardown) | 12.3 us | 0.26 us |

The first row saves per model-layer call, not per request: a request making five DB calls saves the checkout ping five times.

Functionally verified: normal reads and writes work with pre-ping off; an idle request through the middleware leaves no sync session behind; a request that uses the sync session still gets committed and removed.
2026-07-23 17:49:48 -05:00
Classic298 dd514ee20b fix: persist upstream streaming error lines by awaiting the message upsert (#27365)
The branch that normalizes plain JSON error lines from streaming upstreams (lines without the SSE data: prefix) called Chats.upsert_message_to_chat_by_id_and_message_id without await. The coroutine was never executed, so the error was never written to the chat and Python emitted a "coroutine was never awaited" RuntimeWarning instead. The frontend still received the error event, but after a reload the message showed no trace of the failure.

The parallel error-persist branch further down the same handler already awaits the call; this aligns the two.
2026-07-23 17:49:18 -05:00
Classic298andClaude e64acf1c0a perf: batch and deduplicate per-request DB reads in the chat middleware (#27223)
Several spots in the chat pipeline issued sequential single-key config
SELECTs, or fetched the same key twice back-to-back, on every request:

- chat_completion_tools_handler: task model default/external and the
  tools prompt template were four sequential Config round trips (the
  template was fetched twice). One batched Config.get_many now serves
  all of them.
- chat_completion_files_handler: the six RAG settings (top_k,
  top_k_reranker, relevance_threshold, hybrid_bm25_weight,
  enable_hybrid_search, full_context) were six sequential round trips
  inside the retrieval call. Batched into one get_many.
- Voice and code-interpreter prompt templates were each fetched twice
  within one conditional; fetch once and reuse. The code-interpreter
  engine was likewise fetched twice per execution.
- Skill resolution fetched the accessible-skills list, kept only the
  ids, then re-fetched each mentioned skill by id (N+1). Reuse the
  rows from the access query.

Value semantics are identical: get_many applies the same defaults as
the individual gets, and the pre-existing truthiness/empty-string
checks on templates are preserved exactly.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 13:21:29 -05:00
Classic298 dc4b828852 fix: correct async import hook that disables the code-interpreter module blocklist (#27245)
The middleware code-interpreter path defines `restricted_import` as `async def` and assigns it to `builtins.__import__`, which Python's import machinery calls synchronously. Calling an async function returns a coroutine without running its body, so the blocklist check never executes and `_real_import` is never called. When `CODE_INTERPRETER_BLOCKED_MODULES` is set, blocked modules are therefore not blocked, and every subsequent import inside the interpreter binds a dangling coroutine instead of the module, breaking legitimate imports as well.

Define the hook as a regular `def`, matching the working implementation in `tools/builtin.py`. A blocked top-level import now raises `ImportError`, and all other imports pass through to the real importer.
2026-07-23 12:33:44 -05:00
Classic298andClaude da7097565c perf: deduplicate repeated config fetches in Ollama request handlers (#27226)
The per-request Ollama handlers (chat, generate, embed, embeddings,
and the OpenAI-compat completions/chat-completions/messages/responses
endpoints) fetched 'ollama.api_configs' up to three times and
'ollama.base_urls' separately within a single request — the .get()
default-argument pattern made the second api_configs fetch
unconditional, and get_api_key() triggered a third. Up to four
sequential SELECTs per request collapse to one.

A new get_ollama_connection_config() helper fetches base_urls and
api_configs together in one batched Config.get_many where both are
needed; handlers that only need api_configs fetch it once into a
local. Admin operations (pull/push/copy/delete) and the TTL-cached
model-list path are deliberately left untouched.

Resolution semantics (str(idx) key first, url-key legacy fallback,
same defaults) are unchanged.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:48:55 -04:00
Classic298andClaude 8ace4f0a8a perf: stop recomputing static work in per-response ASGI middlewares (#27229)
SecurityHeadersMiddleware called set_security_headers() on every
response — 14 os.environ.get lookups plus a regex validation per
configured header, for values that are static for the process
lifetime. Compute the header list once at construction; when no
security env vars are set, skip wrapping send entirely.

RedirectMiddleware decoded and parse_qs'd the query string of every
GET, though it only acts on /watch?v= and ?shared= URLs. Add a cheap
path/substring precheck first; a false positive just falls through to
the previous full parse, so no redirect behavior changes.

Verified byte-identical responses (status, Location, header values)
against the previous implementations across redirect, passthrough,
and no-env cases.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:29:01 -04:00
Classic298andClaude 62491debfa perf: linear content accumulation in the streaming response handler (#27231)
The streaming handler rebuilt the full accumulated response with
`content = f'{content}{value}'` on every content delta — a complete
string copy per chunk, making accumulation O(n^2) over the response
length. Use in-place `content += value` for the (universal) str case,
which CPython extends in place, keeping accumulation O(n); the
f-string fallback is preserved for non-str values so coercion
behavior is unchanged.

In the ENABLE_REALTIME_CHAT_SAVE branch, full_output() — which
concatenates the entire accumulated output — was called twice per
chunk (once for the DB upsert, once for the emitted delta). Compute
it once and reuse.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:24:29 -04:00
Classic298 8becf9443e fix: only admins may switch the global Automatic1111 checkpoint from image generation (#27244)
The Automatic1111 branch of the image generation route called set_image_model whenever the request carried a model field. On this backend set_image_model is not request-scoped: it persists image_generation.model to the global configuration and posts the new sd_model_checkpoint to the shared server, because Automatic1111 holds a single checkpoint instance-wide. A non-admin holding features.image_generation could therefore change the instance-wide image model and the shared backend checkpoint for every user by sending a model on an ordinary generation request, even though the setting is otherwise managed only through the admin-only image configuration route and the frontend never sends this field.

Gate the switch on an admin caller. A non-admin now generates on the currently configured checkpoint and the model field no longer mutates global state; admins retain per-request model switching here and through the image configuration route. Image editing is unaffected, as it selects its model per request without writing global configuration.
2026-07-23 12:23:24 -04:00
Classic298andkomyunghan 9a6d168499 fix: enforce action availability and model access on the chat action route (#27243)
The chat action route loaded a Function by its raw action_id and executed its action callable after only checking that the id and the requested model existed. The model list that the client renders actions from resolves each model's actions to the active action-type Functions that are global or assigned to that model, and the action route did not mirror that resolution, so a disabled, unassigned, or wrong-type Function, or an action on a model the caller cannot access, could be reached by calling the route directly.

Gate the route on the same rules the model resolution applies: the Function must be an active action, and for server-resolved models the caller must have model access and the action must be one the model actually surfaces (matched by function id, the prefix of each model actions entry, so single and sub-actions both resolve). Direct connections carry a client-supplied model the caller already owns, so the model-bound checks are scoped to non-direct calls; the active-action check always applies. Executing admin-authored Function code remains intended behaviour — this only keeps the route consistent with which actions each model exposes.

Co-authored-by: komyunghan <komyunghan@users.noreply.github.com>
2026-07-23 12:23:05 -04:00
Classic298andClaude 9a54bc4bbb perf: halve function-table queries in get_all_models (#27230)
get_all_models ran four function-table queries: global actions, active
actions, global filters, active filters. Global functions are by
definition (type, is_active=True, is_global=True) — a subset of the
active set — so the global id sets can be derived from the active-rows
queries' is_global flag. Four queries become two, and each dropped
query returned full rows including every plugin's source code.

Also folds the mid-function 'models.default_metadata' read into the
Config.get_many already issued at the top of the function (one fewer
round trip; the existing `or {}` default handling is preserved).


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:11:51 -04:00
Classic298andClaude Fable 5 32242a6788 perf: 40% LESS CPU usage: cut per-instance CPU cost of shared socket.io Redis pub/sub channel (#27282)
* perf: cut per-instance CPU cost of shared socket.io Redis pub/sub channel

Profiling a multi-instance deployment (py-spy --gil) showed ~44% of worker
CPU in the socket.io pub/sub listener. Two causes, two fixes:

- Add hiredis so redis-py parses the RESP protocol in C instead of pure
  Python (redis/_parsers/resp3.py alone accounted for ~28% of GIL samples;
  redis-py auto-selects the hiredis parser when importable).

- Subclass AsyncRedisManager to drop emits whose target room has no local
  participants before upstream _handle_emit re-encodes the full packet.
  Every instance receives every emit published on the shared channel, so
  with N instances all but the hosting one were paying full packet
  re-serialization per message just to deliver it to nobody. Broadcasts
  (room=None) are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9CQ9qnp3sZGYQQwztsCJT

* fix: restrict pub/sub emit early-out to string rooms

Adversarial review against python-socketio 5.16.2 found one divergence
from upstream: for a degenerate empty-sequence room (emit to room=[]) on
an instance whose namespace has no local clients, the filter's
get_participants probe raises IndexError from room[0] where upstream
returns silently at the namespace guard and still publishes to Redis.
Open WebUI only ever emits to scalar string rooms or room=None, so the
case is unreachable today; guard on isinstance(room, str) anyway so any
non-string room shape passes through to upstream behavior unchanged.

Every open-webui emit uses a string room, so the fast path still covers
all real traffic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9CQ9qnp3sZGYQQwztsCJT

* Update requirements.txt

* Update pyproject.toml

* Update requirements-min.txt

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:11:38 -04:00
Classic298andClaude 073cd65afe Log upstream provider rejections at warn/error level (#27238)
When an upstream provider rejects a request (e.g. a 400 for a
max_tokens value above the model's ceiling), the actionable error
message was only published to event sinks, which are invisible unless
an event function or webhook is configured. Admins had to query the
provider's API directly to diagnose failures (open-webui#27237).

Add a single log line in publish_model_provider_request_failed — the
chokepoint every upstream failure path (OpenAI-compatible chat,
embeddings, responses, token counting, and Ollama) already routes
through — recording status, provider, url, model, error code, and the
upstream message truncated to 1000 chars. 4xx logs at WARNING, 5xx at
ERROR. Client-facing responses are unchanged, so no additional error
detail is exposed in the chat.


Claude-Session: https://claude.ai/code/session_018VecyiPejru1EVF5yfe2sU

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 03:42:48 -04:00
Classic298 3cf3f8e189 fix: add missing folders field to SharingPermissions so the permission persists (#27296)
`sharing.folders` is present in `DEFAULT_USER_PERMISSIONS` but absent from the `SharingPermissions` response/update schema, so the admin default and group permission API silently drops it on every round-trip and the setting is never saved. Add `folders: bool = False`, matching the config default (`USER_PERMISSIONS_FOLDERS_ALLOW_SHARING`), restoring parity with `DEFAULT_USER_PERMISSIONS`.

Fixes #27120.
2026-07-23 03:24:51 -04:00
Classic298andClaude f8c0d2fdd6 fix: don't mutate caller's payload when resolving a pipe base model (#26906)
generate_function_chat_completion rewrote form_data['model'] to the base
model id in place. Because that dict is the same object process_chat_response
later re-submits for the post-tool-call continuation, the continuation was
access-checked against the base model instead of the user-facing preset.
For a public preset over a private/unregistered pipe base, a non-admin's
first message succeeded but the continuation after a native tool call was
denied and silently swallowed, aborting the response (admins skip the
user-only check, so they were unaffected).

Operate on a shallow copy of form_data, mirroring the openai/ollama routers
which already substitute the base model on a copy. The continuation now
re-checks the preset the user actually has access to.


Claude-Session: https://claude.ai/code/session_018toPfJW1hMXAhokGaL43Ep

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 13:33:24 -05:00
Classic298 274729aa47 fix: socket reconnect recovery never runs for chats started from the home page (#26913)
When a chat is started from the home page, the URL is switched to /c/{id} with
history.replaceState, so the Chat component is never remounted and chatIdProp
stays empty for the lifetime of that view. Both websocket recovery paths,
handleSocketConnect and the chat:active fallback, are gated on chatIdProp and
therefore never run for these chats. After any websocket drop during a response
(mobile backgrounding, VPN or IP change, wake from sleep) the completed response
is never fetched and the chat stays stuck in a loading state until a manual page
refresh. Chats opened directly via /c/{id} recover fine, which is why the bug
only reproduces reliably on freshly started chats.

Gate both recovery paths on the chatId store instead, which is set for every
persisted chat, and let loadChat fall back to it so the recovery reload also
works when chatIdProp is empty. Chats opened via /c/{id} behave exactly as
before and temporary chats stay excluded.

The same gate likely explains the remaining reports in #26315.

Fixes #26844
2026-07-10 13:30:55 -05:00
Classic298andmanus-use 65a5fad7b9 fix: gate allow-same-origin on the terminal file-preview iframe to prevent same-origin XSS (#26907)
The system-terminal HTML file preview (FilePreview.svelte, serveUrl branch) rendered served HTML in an iframe that hardcoded allow-same-origin. The terminal proxy serves the file root-relative (same origin as the app) and injects no CSP, and there is no default global CSP, so script in a previewed HTML file executed in the application's own origin and could read localStorage (the session token), enabling account takeover and, for admin or workspace.functions victims, server-side RCE via Functions. The sibling srcdoc branch already gates allow-same-origin behind the iframeSandboxAllowSameOrigin setting (off by default) and injects a CSP; the serveUrl branch never received that defense. Gate allow-same-origin on the serveUrl branch identically, so by default the preview runs at an opaque origin and its scripts cannot reach the parent context. Legitimate HTML preview rendering is unaffected.

Co-authored-by: manus-use <manus-use@users.noreply.github.com>
2026-07-10 13:30:18 -05:00
Classic298 f4a6ea9300 fix: Milvus multitenancy scalar index creation on Milvus Lite (#26911)
Enabling ENABLE_MILVUS_MULTITENANCY_MODE with the default MILVUS_URI (embedded Milvus Lite at DATA_DIR/vector_db/milvus.db) fails on the first embedding write: _create_shared_collection calls collection.create_index(RESOURCE_ID_FIELD) with no index params. A Milvus server auto-selects a scalar index type in that case, but Milvus Lite rejects the call with "create_index missing required 'index_type' parameter", so shared collection creation raises and every embedding write 500s (memory add, file upload, knowledge writes).

Keep the parameterless call as the first attempt so behavior on Milvus servers is unchanged, fall back to an explicit INVERTED scalar index, and if that also fails log a warning and continue. The scalar index only accelerates resource_id filters; inserts and filtered queries work without it, so a missing index must not break collection creation.

Verified against embedded Milvus Lite: shared collections now create (with the warning), and memory add, file upload and memory query succeed end to end. Against a Milvus server the first attempt is identical to the current code, so nothing changes where it works today.
2026-07-10 13:29:29 -05:00
Classic298andclassic298 0f8846b7fc fix: convert SecurityHeadersMiddleware to pure ASGI (#26924)
SecurityHeadersMiddleware was the last middleware in the stack still
subclassing BaseHTTPMiddleware, after CommitSession, AuthToken,
WebsocketUpgradeGuard and Redirect were all moved to pure ASGI in
utils/asgi_middleware.py. BaseHTTPMiddleware re-buffers the response
body through an anyio task group, which has known issues with
streaming and Content-Length-bearing responses (e.g. the FileResponse
returned by /api/v1/audio/speech).

Reimplement it as a pure-ASGI middleware that stamps the configured
security headers onto the http.response.start message via
MutableHeaders and forwards all body chunks untouched, matching the
pattern already used by its four siblings. set_security_headers() and
all its helpers are unchanged.

Co-authored-by: classic298 <classic298@users.noreply.github.com>
2026-07-10 13:29:14 -05:00
Classic298 42f5c3d6f7 Merge pull request #26914 from Classic298/srcdoc-embed-prompt-confirmation
fix: restore prompt confirmation for sandboxed tool result embeds
2026-07-10 13:28:39 -05:00
Classic298andTim Baek 4b55e69640 Update CHANGELOG.md (#26641)
* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-07-01 03:38:33 -05:00
Classic298 2d7fc04bfb fix: use updated_at for sidebar chat timestamp (#26454)
The sidebar time-ago indicator rendered `created_at`, so the relative time stayed pinned to the chat's creation age and never reflected new activity. After sending a message the chat would jump to the top of the list (which sorts by `updated_at`) while still showing a stale label such as "3w", which is confusing.

The indicator was originally added using `updated_at` and was inadvertently switched to `created_at` during a later refactor. Restore `updated_at` (falling back to `created_at` when absent) so the timestamp matches the list ordering and updates whenever a chat is modified.

Fixes #26451
2026-07-01 02:08:56 -05:00
Classic298 6e3020b942 Update translation.json (#26386) 2026-06-29 13:40:25 -05:00
Classic298 09a1c3a948 chore: CHANGELOG (#26016)
* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2026-06-29 13:37:36 -05:00
Classic298 27c97bfe96 i18n: complete German (de-DE) translations (#26383)
Translate all remaining untranslated strings in the German locale: 228 empty
values plus the genuinely translatable members of the value-equals-key set (240
entries total). Covers webhooks/events, external knowledge sources, context
compaction, terminal servers, OAuth/OIDC claims, skills, and directory
management.

Translations follow the existing locale conventions: formal "Sie" address,
established German terms (Websuche, Wissen, Werkzeuge, Wissensspeicher), and
brand/proper nouns, format names, and identical-in-German words left untouched.
2026-06-29 11:47:56 -05:00
Classic298 75df948f34 feat: forward client User-Agent to model backends via {{USER_AGENT}} placeholder (#26333)
Adds a {{USER_AGENT}} custom-header placeholder that relays the inbound
client's User-Agent to upstream model backends, so providers see the real
client instead of Open WebUI's internal aiohttp UA. This makes upstream
usage/cost attribution and backend telemetry possible, and is opt-in
per-connection (no global flag): admins add {{USER_AGENT}} to a connection's
custom headers in Admin > Settings > Connections.

The placeholder is sourced from the live inbound request (with a metadata
fallback for detached RAG/tool calls), so it resolves on every prompt-sending
path, not just chat completions:

- OpenAI completions, Responses API, and proxy — all route through
  get_headers_and_cookies, which now passes the request into get_custom_headers.
- Anthropic Messages API (/api/v1/messages) — already covered, it delegates
  to the chat completion handler.
- Ollama (/api/chat, /v1/completions, /v1/chat/completions, /v1/messages,
  /v1/responses) — previously had no custom-header support at all; send_request
  now applies per-connection custom headers (with templating) for every
  Ollama prompt endpoint.

Custom headers are applied after the built-in user-info headers so explicit
admin-configured headers take precedence. The other existing placeholders
({{CHAT_ID}}, {{USER_ID}}, ...) now also work on the newly covered paths.

Frontend: the connection editor's Headers field is now shown for Ollama
connections too (previously gated to non-Ollama), so the placeholder can be
configured there.

Ref: open-webui/open-webui#26159
2026-06-29 05:58:19 -05:00
Classic298andClaude 4c05abbe59 fix: respect model visibility in workspace base model selector (#25668)
The base model selector in the model creation workspace listed every
model from the global store, ignoring the per-model visibility (hidden)
flag. Models marked as not visible were correctly hidden from the chat
model selector but still appeared in the workspace 'Base Model (From)'
dropdown.

Filter out hidden models (info.meta.hidden) to match the chat selector
behavior, while still keeping a model visible if it is the currently
selected base, so editing a preset built on a now-hidden base does not
show a blank dropdown.

Fixes #25665

https://claude.ai/code/session_01CUvzYN9DjvwTpNCT5QL33B

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-29 03:51:26 -05:00
Classic298 0fc630b34b fix: per-user model cache used static key= (cross-user model exposure) (#25783)
routers/openai.py and routers/ollama.py decorated get_all_models with
`@cached(key=lambda _, user: ...)`. In aiocache 0.12, `key=` is a STATIC
cache key: get_cache_key returns `self.key` verbatim (the lambda object)
without calling it, so every caller collides to ONE shared entry within
the TTL. The intended per-user namespacing never happened — one user's
permission-filtered model list could be served to another user (or an
anonymous caller) during the cache window.

The per-call hook is `key_builder=` (called as key_builder(func, *args,
**kwargs)). Switch both sites to key_builder with a (func, request,
user=None) signature so the key is built per call. user=None mirrors
ollama's optional-user signature and stays correct whether user is passed
positionally, as a kwarg, or omitted.

Verified offline: old form returns the same key object for distinct users
(collision); new form yields distinct openai_all_models_<id> /
ollama_all_models_<id> keys, and the unauthenticated base key when no user.
These two were the only @cached(key=lambda ...) sites in the backend.
2026-06-29 03:51:02 -05:00
Classic298andClaude Opus 4.8 a66477b710 fix: bind channel thread parent/reply to the URL channel (#25766)
GET /api/v1/channels/{id}/messages/{message_id}/thread authorized only the URL channel, but get_messages_by_parent_id() appended the thread parent (loaded by id) without checking it belonged to that channel, so a caller could read a message from a channel they cannot access by passing its id as the thread root. Require the parent to be in the requested channel before returning it, and reject a posted parent_id/reply_to_id that does not belong to the channel.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:46:50 -05:00
Classic298andClaude Opus 4.8 58f9170319 fix: trim terminal bearer token so whitespace can't break the WebSocket (#25686)
A trailing space in the Open Terminal bearer token broke only the
interactive terminal: HTTP calls put the token in the Authorization
header, where the spec strips trailing whitespace, so the connection
test, file listing and tool calls all worked. The terminal WebSocket
can't set headers from the browser, so it sends the token inside a JSON
auth message that preserves the space verbatim, failing auth with
[Connection closed]. Normalize the key on save so whitespace never
enters storage, and trim it in the WebSocket auth message so existing
saved configs work without re-saving.


Fixes #25613

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:35:19 -05:00
Classic298 e98730b20d fix: pass web search results to model when embedding & retrieval enabled (#25600)
Web search results stored as a vector collection were silently dropped
before retrieval when BYPASS_RETRIEVAL_ACCESS_CONTROL is False (the
default). The server-generated web_search file item carries a
'collection_name' but its 'web_search' type is not matched by any
explicit dispatch branch in get_sources_from_items, so it fell through
to the untrusted client-supplied collection_name branch and was ignored.

Add an explicit branch for type == 'web_search' items so the collection
is queried again. Access control is preserved: the collection still
passes through filter_accessible_collections, which already allowlists
web-search-* and bypasses only for admins.

Regression introduced when the retrieval access-control hardening gated
the bare collection_name fallback behind BYPASS_RETRIEVAL_ACCESS_CONTROL.
2026-06-29 03:34:55 -05:00
Classic298andClaude Opus 4.8 5796d44363 fix: defer missing-local-embedding error so it can't crash boot (#25683)
get_embedding_function runs at import time (main.py), so the empty-engine
+ no-model guard added in 55ca719b raised ValueError during startup and
bricked the instance: a blank embedding model set via the UI made the app
unbootable, with no way back into settings to undo it. Move the check into
the returned coroutine so construction always succeeds and the error only
fires when something actually embeds, surfacing as a clear request error
instead of the old cryptic 'NoneType has no attribute encode'.


Fixes #25634
Fixes #25165

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:21:09 -05:00
Classic298andClaude Opus 4.8 534206095f fix: route Account Pending sign out through backend signout flow (#25681)
The Sign Out button on the Account Pending overlay only cleared the
local token and redirected to /auth, skipping the backend signout. This
left the OAuth/OIDC session alive at the IdP, so re-login bounced the
user straight back into pending and never re-fetched updated role/group
claims. Call userSignOut() and honor the returned redirect_url, matching
the sidebar logout flow.


Fixes #25644

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:54:19 -05:00
Classic298andClaude Opus 4.8 36d08fa2a7 fix: request file list/search without extracted content by default (#25774)
The file picker and Files modal only render filenames and metadata, but
searchFiles()/getFiles() never passed content=false, so the backend
serialized data.content (full extracted text) of every file into each
response. On instances with many large documents this turns a metadata
list into tens of MB per request, re-issued on every picker keystroke.

The backend already supports content=false on GET /files/ and
GET /files/search; this just makes the list/search callers opt out of the
payload by default. The param stays overridable for callers that need it.


Fixes #25741

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:43:56 -05:00
Classic298andClaude Opus 4.8 2414dfca70 fix: mark in-place created chats as read on new chat (#25782)
A chat created in place (new chat -> first message) keeps chatIdProp
undefined because the URL is swapped via history.replaceState without a
remount, so none of the chatIdProp-gated updateLastReadAt paths fire and
the chat is never persisted with a last_read_at. Starting another new
chat via initNewChat reset $chatId without marking the outgoing chat
read, so on refresh updated_at > last_read_at (NULL) and the chat shows
as unread in the sidebar.

Mark the outgoing chat read in initNewChat, mirroring navigateHandler.


Fixes #25108

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:32:00 -05:00
Classic298andClaude Opus 4.8 386ac95814 fix: scope Socket.IO event-caller to the requesting user's own session (#25763)
get_event_call() routed execute:python / execute:tool events to a client-supplied session_id after only checking the session was connected, not that it belonged to the requester. Verify the target session is owned by the requesting user (metadata user_id) before delivering, so a client cannot route code/tool execution into another user's session.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:17:40 -05:00
914039ac81 Escape voice-derived attributes in Azure TTS SSML (#25776)
The Azure TTS handler (_tts_azure) interpolated the user-supplied voice,
and the locale derived from it, into the SSML xml:lang and <voice name>
attributes without XML-escaping, while the text body was already escaped
(2e75c6dbd). Escape both attributes too, so every user-derived value in
the SSML document is consistently encoded.

Co-authored-by: alanturing881 <alanturing881@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:16:37 -05:00
Classic298andClaude Opus 4.8 01198eaeef Close DNS-rebinding SSRF gap in get_content_from_url probe (#25775)
The web-ingest probe in get_content_from_url validated the URL at resolve
time (validate_url) but then fetched with a bare requests.get, which
re-resolves the hostname at connect time. An attacker-controlled name
server can answer with a public IP during validation and an internal IP
at connect, reaching cloud metadata / loopback / internal services (blind
always; binary content-types are read back to the caller). The
connection-layer guard (#24759) that closes this for the SafeWebBaseLoader
path was never mounted on this probe.

Route the probe through the same _SSRFSafeAdapter the loader uses, so the
resolution that feeds the TCP connect is re-validated against the
global-IP check.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:15:31 -05:00
Classic298 15d96b1f2a fix: chroma has_collection always returns False (name vs Collection) (#25780)
has_collection did `collection_name in self.client.list_collections()`,
but chromadb's list_collections() returns Collection objects (1.x), not
name strings — so the membership test is always False, even when the
collection exists. Compare against the collection names instead (with a
hasattr guard tolerating versions that yield plain names).

Found via the dependency-contract test suite (unit/deps/test_chromadb.py).
2026-06-29 02:15:05 -05:00
Classic298andClaude Opus 4.8 c31694af09 chore: Update SECURITY.md (#25773)
* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Extend the already-fixed/monitoring rule to public PRs and credit

Broaden the rule from "already fixed" to also cover issues already being fixed in
the open (e.g. an open pull request), extend the commit-monitoring pattern to PRs,
and fold in the credit consequence on provable grounds rather than an unprovable
bad-faith claim: a report of an already-public, already-fixed-or-being-fixed issue
filed strictly last is a duplicate we cannot distinguish from scraping, so it earns
no advisory. Credit belongs to whoever found or fixed it, who forfeits it by
disclosing publicly instead of reporting confidentially first — so a public fix
earns no advisory and no credit for anyone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Remove Rule 14 (One Vulnerability Per Report)

The one-CVE-per-vulnerability constraint it restated is a CVE Program counting
rule, already binding through the "Alignment with the CVE Program" section.
Dropping the standalone rule removes the duplication; bundled reports are still
split on that basis when they arise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* security policy: surface "What a Valid Report Gets You" near the top, refresh date

Move the "What a Valid Report Gets You" section up to directly under the good-faith
reporting section (it leads with what reporters receive, rather than burying it
below the rules), and update the last-updated date.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update SECURITY.md

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:11:17 -05:00
Classic298 c3394288bb fix: repair Mistral OCR async upload (aiohttp.streams.FilePayload removed) (#25779)
_upload_file_async built its multipart body with
`aiohttp.streams.FilePayload(...)`, which no longer exists in aiohttp
(payload classes live in aiohttp.payload, and there is no FilePayload).
The reference sits inside a lazy closure, so import succeeds and only the
async Mistral OCR file-upload path blows up at runtime with
AttributeError on every call.

Mirror the working sync path: open the file in a context manager and let
MultipartWriter.append(file_obj, {...}) build a streaming
BufferedReaderPayload, with the POST issued inside the open() block so
the handle stays valid for the whole upload. Preserves the streaming /
memory-efficiency intent.

Found via the dependency-contract test suite (unit/deps/test_aiohttp.py),
which pins aiohttp.streams.FilePayload as absent.
2026-06-29 02:05:54 -05:00
Classic298 8a016931f1 refac(telemetry): drop deprecated semconv SpanAttributes subclass (#25784)
constants.py subclassed opentelemetry.semconv.trace.SpanAttributes, which
is deprecated since semconv 1.25.0 (emits a DeprecationWarning; the pinned
0.63b1 has it live). Source the legacy span-attribute keys from the
non-deprecated _incubating attribute modules instead:
  http.url / http.method / http.status_code  <- _incubating http_attributes
  db.name / db.statement / db.operation      <- _incubating db_attributes
The stable http module renamed these (http.request.method, ...), so only
the incubating module preserves the original values. Custom keys
(db.instance/type/ip/port, error.*, result.*) stay literals.

Verified: emitted attribute keys are byte-identical before/after for every
key instrumentors.py reads, and importing constants no longer emits a
DeprecationWarning.
2026-06-29 02:05:34 -05:00
Classic298 5fd26b7549 docs: note pydub/audioop Python 3.13 constraint at the import (#25785)
pydub imports the stdlib `audioop`, removed in Python 3.13, so audio
preprocessing would break there. requires-python is already capped at
< 3.13; this one-line pointer flags what to handle (audioop-lts, or drop
pydub) before raising that cap.
2026-06-29 02:03:11 -05:00
Classic298andClaude Opus 4.8 b295a20b9d chore: bump Python backend dependencies, drop unused peewee (#25786)
* chore: bump Python backend dependencies, drop unused peewee

Minor/patch + reviewed major bumps across requirements.txt,
requirements-min.txt, pyproject.toml and uv.lock; playwright image bumped in
docker-compose.playwright.yaml. peewee/peewee-migrate removed (zero imports).

Security-relevant: cryptography 46->48, authlib 1.6.10->1.7.2, PyJWT 2.11->2.13,
requests 2.33.1->2.34.2, RestrictedPython 8.1->8.2, pillow 12.1.1->12.2.0.
Reviewed majors: redis 7->8, pymilvus ->2.6.14, azure-search-documents 11->12,
chardet 5->7, unstructured 0.18->0.22, pycrdt 0.12->0.13.

Testing:
- Resolution: `uv lock` resolves the full bumped set with no conflicts; uv.lock
  regenerated to match (peewee dropped, every pin including
  azure-search-documents==12.0.0 resolves).
- Per-dependency contract tests (external tests repo, unit/deps/): 105 files,
  2205 passed / 6 skipped, ruff-clean. One file per dependency pins the symbols,
  signatures and behaviour the backend actually uses, so an API removal/rename in
  a bumped version fails loudly instead of at runtime. Offline/deterministic.
- End-to-end embed->retrieve test driving transformers + sentence-transformers +
  chromadb together through Open WebUI's real RAG path (cached model, in-memory
  chroma, semantic retrieval asserted).
- Install/startup/health resolution gate added to the dep-bump workflow and the
  integration suite (uv/pip resolve + uvicorn /health + Playwright dev visibility).
- Bugs surfaced while testing each got an isolated fix branch + regression test:
  Mistral OCR aiohttp FilePayload (#25779), chroma has_collection (#25780),
  aiocache per-user model-cache key (security), otel semconv deprecation,
  pydub/audioop <3.13 note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Bump python-multipart 0.0.22 -> 0.0.27 (CVE-2026-42561, CVE-2026-40347)

0.0.22 is affected by two DoS CVEs in the multipart parser that
Starlette/FastAPI run for every multipart/form-data request, so any
authenticated user hitting an upload endpoint can trigger them:
- CVE-2026-42561: unbounded part-header count/size -> CPU exhaustion (fixed 0.0.27)
- CVE-2026-40347: large multipart preamble/epilogue DoS (fixed 0.0.26)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:02:18 -05:00
Classic298andClaude Opus 4.8 a4f7ec1fb3 Chore: Add a security-reporting link to the issue chooser (#26007)
* Add a security-reporting link to the issue chooser

With blank issues disabled, the New Issue chooser only offered Bug Report and
Feature Request, leaving security reporters no obvious route and nudging them
toward filing vulnerabilities as public issues. Add a contact_links entry that
points to the Security Policy (/security/policy), where the "Report a
vulnerability" button opens a private GitHub advisory — keeping security
reports out of public issues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update config.yml

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 01:36:44 -05:00
Classic298andClaude Opus 4.8 89629b8f03 perf: make ToolCallDisplay parseJSONString iterative (#26146)
Replace the recursive parseJSONString with an equivalent iterative unwrap.
The recursion re-parsed its own already-parsed result until JSON.parse threw;
on scalar JSON values (e.g. "5" -> 5) that recursed until a stack overflow
which was then silently caught — wasted work on every complete tool-call
payload. The loop returns the identical value in all cases (verified
byte-identical across 29 inputs incl. double/triple-encoded and partial JSON)
without the stack churn.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 01:08:59 -05:00
Classic298 d501e3d6b5 Update milvus_multitenancy.py (#25857) 2026-06-17 03:07:27 +02:00
17df026492 Confer object-derived file write only for files the object owner owns (#26032)
has_access_to_file() derives file access from the objects a file is attached to
(knowledge bases, workspace models). Those branches returned True for any access_type
whenever the user held that permission on the object, write/delete included. Since a
user can create their own KB or model and attach any file they can merely READ (KB
attach and the model meta.knowledge validator both gate on read access only), a user
with read access to a victim file could launder it into write/delete: attach it to an
object they own, then rename, overwrite or delete it via the write-gated file routes
(POST /files/{id}/rename, /data/content/update, DELETE /files/{id}). This is the
residual of GHSA-vjqm-6gcc-62cr (CVE-2026-54012) left open by the read-only attach
validator (CWE-863).

An object now confers write/delete on a file only when the object's owner owns that
file, so delegation originates from the file's own owner. Read is unchanged (RAG and
shared-object reads still work), and legitimate delegation is preserved: a write grant
on an object whose owner owns the attached file still confers write. Applied to all
three object branches: knowledge base, file home collection, and workspace model.

Co-authored-by: rexpository <30176934+rexpository@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 03:05:57 +02:00
e038bab66d Authorize POST /api/v1/images/edit (enforce ENABLE_IMAGE_EDIT + image-gen permission) (#26009)
The direct image-edit route was the only image-edit surface with no authorization: it ran
on get_verified_user alone, while POST /generations enforces ENABLE_IMAGE_GENERATION +
features.image_generation and the built-in edit_image tool enforces ENABLE_IMAGE_EDIT +
features.image_generation. A verified non-admin user could therefore reach the configured
image-edit provider (spending IMAGES_EDIT_OPENAI_API_KEY) even when the administrator had
globally disabled image editing (ENABLE_IMAGE_EDIT=False) or denied the user image
generation.

Split the route from the shared impl (mirroring generate_images/image_generations): the new
/edit wrapper enforces ENABLE_IMAGE_EDIT and the per-user image-generation permission, then
delegates to image_edits(). The internal callers (the edit_image tool and the chat
middleware) already gate themselves and call image_edits() directly, so they are unaffected.

Co-authored-by: jagstack <52110932+jagstack@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 03:05:41 +02:00
Classic298 c39be0e2d6 Update milvus.py (#25858) 2026-06-17 03:05:27 +02:00
5f3a628a8d Encode terminal ws session_id to block upstream user_id query injection (#26042)
ws_terminal() interpolated the path parameter session_id directly into the upstream
terminal WebSocket URL and then appended ?user_id=<caller>, with no encoding or
validation (the HTTP sibling proxy_terminal runs _sanitize_proxy_path; this path ran
nothing). An encoded '?'/'&' smuggled through session_id survives Open WebUI's single
decode and is re-decoded by the upstream, injecting an attacker-chosen user_id ahead
of the appended one. Query parsing binds the first occurrence, so the orchestrator
resolves the spoofed user's terminal scope, letting a normal authenticated user
present another user's identity to the upstream (CWE-116/863).

Encode session_id as an opaque path segment with urllib.parse.quote(session_id,
safe=''). This neutralises '?'/'#'/'&' at any decode depth (the upstream's single
decode reverses only the quote, leaving the original delimiters inert as path
content), while legitimate UUID session ids pass through unchanged. The appended
user_id is then the only query parameter the upstream binds.

The separate concern that the forwarded identity is a bearer claim with no integrity
binding spans Open WebUI and the upstream terminal server and is not addressed here.

Co-authored-by: rexpository <30176934+rexpository@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:15:39 +02:00
Classic298andClaude Opus 4.8 22f2fe1ffb Require auth on ydoc:awareness:update and ydoc:document:leave handlers (CWE-306) (#25946)
These were the only two ydoc Socket.IO handlers missing the SESSION_POOL guard that
every sibling (join/state/update) enforces. With always_connect=True admitting
unauthenticated sockets, a client that knew a note's document_id could broadcast
spoofed awareness (cursors/presence) and fake ydoc:user:left events into a live
editing room, attributed to any client-supplied user_id.

Both handlers now require an authenticated SESSION_POOL session; the awareness handler
additionally requires prior ydoc:document:join (room membership); and the broadcast
user_id is fixed to the authenticated identity instead of the client-supplied value,
removing the impersonation. Document content was never reachable (ydoc:document:update
already enforces write permission).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:06:13 +02:00
4c06b392da Don't let SCIM's active flag demote an admin (defense-in-depth) (#25948)
The SCIM update_user (PUT) and patch_user (PATCH) handlers mapped the SCIM active field
unconditionally onto the role column (role = 'user' if active else 'pending'), so a routine
IdP sync or a misconfigured IdP that marked a locally-provisioned admin inactive would
silently strip that admin's role and could lock an instance out of its own administration.

Gate both active->role assignments on user.role != 'admin' so SCIM provisioning can
activate/deactivate ordinary users but never demotes an existing admin; admin role changes
continue to go through the dedicated admin endpoints. SCIM already cannot promote to admin
(active only maps to user/pending), so this is symmetric.

Credit to @HOHK0923 for surfacing the admin-demotion footgun.

Co-authored-by: HOHK0923 <118590749+HOHK0923@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:59:55 +02:00
087878ce84 Match WEB_FETCH_FILTER_LIST on hostnames with label boundaries, not URL suffix (CWE-693) (#25949)
is_string_allowed does endswith() matching and was called with the full URL
(retrieval/web/utils.py) against WEB_FETCH_FILTER_LIST, so a blocklisted host with any
path (https://blocked.example/x) ended with /x, not the host, and slipped through; the
allowlist direction false-rejected legitimate URLs and admitted attacker URLs ending in
an allowed string. The same endswith caused label confusion at the hostname call site
(retrieval/web/main.py): corp.com matched evilcorp.com, 10.0.0.1 matched 110.0.0.1.

Add is_host_allowed(host, ...) matching on DNS label boundaries (host == pattern or
host.endswith('.' + pattern)), called with the parsed hostname at both web-fetch call
sites. is_string_allowed is left unchanged for the unrelated function-name filters
(utils/middleware.py, utils/tools.py).

The separate is_global guard (validate_url / _ssrf_safe_new_conn, active when
ENABLE_RAG_LOCAL_WEB_FETCH is off) already blocks RFC1918/loopback/link-local, so this
restores the admin's intended blocking of specific public hosts.

Co-authored-by: addcontent <59762500+addcontent@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:53:08 +02:00
de8ea08f5c Route user-supplied image-URL fetches through an SSRF-safe session (DNS rebinding, CWE-918) (#25960)
The connection-layer DNS-rebinding guard (_SSRFSafeResolver / _SSRFSafeAdapter, PR #24759) was
mounted only on SafeWebBaseLoader. Two user-reachable image fetches validate the URL then fetch
it through the shared get_session() pool with the default resolver, so a TTL-0 rebinding answer
that passed validate_url reaches an internal address at connect:

- get_image_base64_from_url (utils/files.py): user image_url on every chat completion.
- load_url_image (routers/images.py, POST /api/v1/images/edit): user-supplied image field.

Add get_ssrf_safe_session() (a one-off aiohttp session mounting _SSRFSafeResolver) and use it
for both fetches, so the connect-time IP is re-validated and a rebound loopback / RFC1918 /
metadata address is rejected. The shared pool is left untouched for the admin-configured
image-generation callers, which legitimately reach internal hosts.

Co-authored-by: dhyabi2 <32069256+dhyabi2@users.noreply.github.com>
Co-authored-by: geo-chen <2404584+geo-chen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:36:53 +02:00
b7626f05fb Authorize KB write access before auto-linking an uploaded file (CWE-862/863) (#26001)
process_uploaded_file auto-links an uploaded file to the knowledge base named in
client-supplied metadata.knowledge_id, but it called Knowledges.add_file_to_knowledge_by_id
directly with no authorization, while the dedicated POST /knowledge/{id}/file/add endpoint
gates the same operation on owner / admin / write grant. A read-only collaborator (or any
verified user who knows the KB id) could therefore attach arbitrary files to a knowledge base
they cannot write: the KnowledgeFile membership row was committed, and the later vector-index
write check failed closed but left the row in place.

Gate the auto-link on the same owner/admin/write check before inserting the membership row;
otherwise skip the link and log. The file still uploads as the user's own file, it just is
not linked to a KB they cannot write.

Co-authored-by: jagstack <52110932+jagstack@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:56:24 +02:00