Repository navigation
feat: build the chat frontend from a forked source tree and give it one Hive navigation - #938
Conversation
git-subtree-dir: vendor/open-webui git-subtree-split: ecd48e2f718220a6400ecf49eafd4867a38feb10
…ne Hive navigation
The owner's complaint was that the left navigation carries neither the agent
workspace nor the chat interface, that the product still looks like Open WebUI,
and that the agent is reached through a button in the top right corner that
opens a separate page. All three are properties of shipping a prebuilt vendor
bundle, so this change stops doing that.
vendor/open-webui is upstream at v0.10.2, added as a squashed git subtree. A
first stage in Dockerfile.open-webui compiles its frontend exactly the way
upstream's own Dockerfile does and copies the result over /app/build in the
pinned image; the Python backend, its dependency set and every existing backend
patch are untouched, so digest bumps keep delivering backend fixes. The stage
asserts that the vendored version equals the version inside the pinned image, so
a bump without a matching subtree pull fails the build rather than a browser.
On top of that:
* packages/hive-tokens/tokens.css carries the design system as plain CSS
custom properties, outside the vendored tree so the visual identity is not
hostage to the chat engine.
* The fourteen values Open WebUI funnels its whole surface and ink scale
through are remapped to the brand's warm palette, which is what turns the
product from grey to cream and charcoal in one place rather than a hundred.
* Hanken Grotesk and Geist Mono are served from the deployment itself.
* src/lib/hive/ holds every Hive authored component: a navigation manifest as
data, a row component, and the shell stylesheet. Chats, Agents and Knowledge
are labelled destinations in both sidebar states.
* /agents renders the agent workspace inside the shell over the same origin,
so reaching the agent no longer leaves the product. The injected launcher
overlay and loader.js are deleted with it.
Surfaces the bundle rewrite layer used to remove are removed in source instead,
which is what the fork is for: the Workspace tabs with no Hive counterpart and
their routes, the Playground, the Admin Panel entry, the vendor's documentation,
releases, changelog dialogue, social badges and copyright, the Settings
Integrations tab, and the vendor links on the error screen. The accessible names
that layer used to add are attributes in source now.
The rewrite layer itself is left running against the upstream bundle and is not
retired here, deliberately: a separate change was in flight against
hive_ui_surfaces.py, and two edits to that file from different directions is how
a surface silently returns. docs/owui-fork.md records what it costs and what
retiring it involves.
Visual proofTwo images built from the same pinned Open WebUI digest, differing only in this branch's diff, captured at 1440x900 in both themes with the DOM read as well as the pixels. Method as PR #909. Everything below is reproducible with The shell as a user meets it, light
This branch. Chats, Agents and Knowledge as labelled rows, the current row carrying a coral bar, the corner link gone, cream and charcoal in place of grey: The same pair, darkThe agent, reached from the sidebar rather than from a cornerThe sidebar is still there, Agents is the current row, and the panel fills the region in the shell's own theme: The panel here is the agent workspace's own sign-in screen, because this stack has no Supabase session and that application still has a login of its own. That is today's behaviour on the live box too, and it is the third login that single sign-on removes. What the capture proves about the part this change owns: the panel is same origin, it is inside the shell, it knows it is embedded, and it follows the shell's theme rather than the desktop's. Collapsed, and focusedEvery destination survives the collapse; only the labels go. Keyboard focus shows the two ring treatment, which is what keeps a focus ring visible on a coral ground: What the DOM says, from the same runsRead from
The authenticated panel with a real task list in it is captured against the demo box once this merges and deploys. |
|
Caution Review failedAn error occurred during the review process. Please try again later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Cowork visual proof, captured in CICaptured against a stack booted from launch-liveness-01-empty-consolelaunch-liveness-02-sandbox-launchedRun log (screenshot stamps carry no URL, and the log is redacted and linted by `lint:proof-tokens`) |
…agent console The frame headers moved from a blanket refusal to a same-origin one, because the chat shell renders this app as its Agents destination over one origin, so the test that asserted DENY now asserts SAMEORIGIN plus frame-ancestors 'self' and says why. Two new cases cover the redirect: the embed and theme parameters have to survive the bounce to sign-in, or the panel loses the shell's theme exactly when a user is not signed in, and a theme value the stylesheet does not know is dropped rather than reflected into the redirect target.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Too many files changed for review (3000 files, 100 file limit). |
Two defects in the previous commit, both found by reading state rather than by trusting it. The first is the serious one. `/vendor/*` is excluded in .gitignore with one negation for the OpenHands vendor tree. `git subtree add` writes a tree directly and so ignored that exclusion, which left vendor/open-webui tracked while every NEW file under it was silently unstageable: the entire src/lib/hive/ shell, the /agents route and the three brand font files were never committed, and nothing failed, because the image build reads the working tree rather than the index. The branch built green and would have deployed a frontend with no Hive navigation in it. vendor/open-webui is now re-included explicitly. The second is the demo box's own timeout. That job recreates the stack with `--build`, and the chat image now compiles the vendored frontend first: on a first deploy with no layer cache that is measured minutes on top of everything else, against a 15 minute ceiling. Raised to 30, with the stale-lock sweep that is documented to match it moved in the same step. Also adds the panel's loading state. The agent workspace resolves fast because it is same origin, but fast is not instant on a cold container, and the design system's rule is that no state is a blank region. One line, no spinner, and it sits behind the frame rather than in front of it, so a load event that never arrives cannot leave a permanent "Opening the agent workspace" over a working panel.
A row that does not respond under the finger reads as dead. One transform, 100 milliseconds, on a control the user has already decided to press, and removed entirely under prefers-reduced-motion. Captures refreshed from the same run.
Adversarial reviewStreams that ran, and streams that did not. An absent stream is not a pass.
Findings on this diff, then findings on the product it lands in. The second set is written up in full at Found and fixed before this comment
Open on this diff, accepted with reasons
Design checklist findings, Emil formatTwo of these are ours and are fixed above; the rest are inherited chrome this change does not touch, and they are wave seven.
Security
Not mergingGitHub is degraded ( |
Security and supply-chain review, PR #938 (
|
…t, and triage the vendored advisories Two things review caught in docs/owui-fork.md. The ordering was written backwards. The bundle rewrites run against the stock bundle inside the pinned upstream image, and the Hive source build replaces that bundle afterward, so the rewrites never see our own output at all. Stated the right way round, with what that means for whoever retires the layer: what those rewrites protect today is not the bundle we ship, it is a drift check on upstream, because every surface they remove is removed again in source and that removal is the one a user meets. The advisories now carry a triage rather than a sentence saying they are ours. The critical one and most of the high cluster are devDependencies that never reach the bundle. Four are direct dependencies and each has a reachability answer read in this tree: xlsx ships and parses user uploaded spreadsheets with no fix available upstream, which is the one that needs a real decision; dompurify ships and is the sanitiser itself, fix available; undici is build time only; sharp is a native module the built output never imports. No upgrades here, deliberately, and the missing Dependabot config is named rather than implied.
Response to the security and supply-chain reviewThank you for verifying provenance against upstream directly rather than against this PR's own claims, and for checking the Dispositions, one per finding. Two produced code, four were verifications with nothing to fix, one is out of scope.
The two that produced work
Advisory triage, written down so it is not redone. Same commit. The critical advisory and most of the high cluster are
No upgrades in this PR, deliberately. A dependency bump inside the same change that introduces 4,900 files is unreviewable, and three of the four need their own verification. On Dependabot you were right that the doc asserted a scope nothing enforces. It now says plainly that no Noted as skipped
|
Cowork visual proof, captured in CICaptured against a stack booted from launch-liveness-01-empty-consolelaunch-liveness-02-sandbox-launchedRun log (screenshot stamps carry no URL, and the log is redacted and linted by `lint:proof-tokens`) |
These two are from #938 and had never once run: the sign-in readiness signal they sit behind was failing for every OWUI end-to-end run, so the first time they executed was in this branch, after that fix, and both went red immediately. Neither is a regression from this change and neither was weakened to go green. They both assumed a sidebar that starts expanded, and this fixture's starts collapsed. That single fact produced both failures. The nav renders twice, an icon-only rail whose name is on `aria-label` and an expanded row that renders the same string as text, and `.first()` resolves to the rail either way, so the text assertion read the icon and got "". The collapsed-rail test then waited sixty seconds for a "Close Sidebar" control that only exists while the sidebar is open. So each test now drives the sidebar into the state it is about, through Open WebUI's own toggle rather than by writing its store, and asserts against the row a person can actually see rather than whichever of the two comes first in the document. The assertions themselves are unchanged: visible text on the expanded row, a non-empty accessible name on the rail.
## Summary Three small, high-value fixes to the chat left navigation (owner-reported). 1. **Removed the vestigial "Chats" nav row.** Its `href` was `/`, identical to New Chat, and no list view was ever built behind it (added in #938 purely for row parity with Agents/Knowledge). `HIVE_NAV` now ships two rows: Agents, Knowledge. 2. **Investigated the "Folders/Recents never populate" report live**, against the deployed demo box (`chat-hive.scubed.co`), signed in as a dedicated, run-scoped e2e fixture identity (`owui-e2e+<runkey>@hive-e2e.invalid`, minted via `scripts/seed-owui-e2e-user.py`) — never the shared `demo@hive-demo.invalid` account and never the owner's account. A clean run confirmed both sections are correctly wired (`getFolders`/`getChatList`) and render as soon as the sidebar is expanded. No code defect in Folders/Recents. 3. **Defaulted the sidebar to expanded** for a first-time visitor. `Sidebar.svelte` reads `localStorage.sidebar === 'true'` on mount, so an unset key (first visit) reads as collapsed, hiding Folders/Recents with no visible affordance to expand. New logic (`sidebarDefaultExpanded`, extracted to its own pure, unit-tested function since `Sidebar.svelte` is too large/stateful to unit test directly) treats anything other than the literal string `'false'` as expanded, so a first-time visitor gets expanded by default while an explicit prior choice (collapsed or expanded) is preserved on every later visit. ## Why not fix Folders/Recents directly Ground truth (prior read-only investigation) said they were fully wired with no broken code, and this PR's own live investigation reconfirmed it end to end on the real deployed box: `FOLDERS_HEADING_VISIBLE=true`, `RECENTS_HEADING_VISIBLE=true` once the sidebar was toggled open, on a freshly created account with real DOM/localStorage checks (not just a visual glance). The collapsed-by-default state was the whole complaint, exactly as scoped. ## Visual proof — partial, live infra was unstable during this session I made five separate live attempts (Playwright, headless Chromium) against `chat-hive.scubed.co` to capture full before/after screenshots with Folders and Recents populated (a real folder + two real chats). Only one attempt completed cleanly end-to-end; the DOM-level assertions from that clean run are quoted above and are real, direct evidence from the live, currently-deployed (pre-fix) build. The other four attempts each failed on a distinct, unrelated infrastructure error during this specific window: - Supabase Auth `oauth/authorize` returned `504` once. - Cloudflare returned `522` (origin connection timeout) to the demo box once. - Caddy/edge returned `upstream request timeout` (crashed the SPA to SvelteKit's default error boundary) once. - A session bounce landed back on the Hive sign-in page without completing OAuth once. None of these touch the changed code (pure frontend, no auth/backend changes). I also spun up an isolated local verification stack (`docker compose -p navqw952`, same shared Supabase, separate from any other agent's stack) to get a controlled screenshot instead; `edge-api` stalled during startup on this host for several minutes with no log progress, consistent with heavy concurrent-agent load on this dev box observed throughout the session (docker builds and `docker ps` itself intermittently took 60-120s). I tore that stack down rather than keep contending for host resources. I am not fabricating a "populated" screenshot. Given the required `Web E2E (full stack)` check is already known red for unrelated reasons and blocks merge regardless, I'm opening this PR now with the code, tests, and the real evidence gathered, and flagging that a clean before/after screenshot set (collapsed vs. expanded, with a real folder and two real chats visible) should be captured and attached here once live infra is stable, before merge. ## Tests `vendor/open-webui/src/lib/hive/nav.test.ts` (8 tests, updated for the row removal) and new `vendor/open-webui/src/lib/hive/sidebar-default.test.ts` (5 tests, RED confirmed before the implementation existed, GREEN after). Both pass. Frontend production build (`npm run build`, includes this repo's own assertion step checking `data-hive-nav` and brand assets survive the bundle) also succeeds. ## Buglog entry ```json {"date":"2026-08-18","error_message":"Folders and Recents sidebar sections appear to never populate","root_cause":"Sidebar.svelte defaults localStorage.sidebar to collapsed for a first-time visitor (localStorage.sidebar === 'true' check), hiding Folders/Recents with no visible affordance to expand; both sections were already correctly wired to getFolders/getChatList","fix":"Default sidebar to expanded when no explicit prior choice exists (sidebarDefaultExpanded in vendor/open-webui/src/lib/hive/sidebar-default.ts), preserving an explicit collapse/expand choice on later visits; also removed the vestigial 'Chats' nav row whose href duplicated New Chat","tags":["owui","sidebar","nav","frontend"]} ``` (To be appended to `main`'s `.wolf/buglog.jsonl` via a separate buglog-only PR once this merges, per `.claude/rules/openwolf.md`.) ## Out of scope (explicitly) Agents row, a Projects section, and the history region restructure are all out of scope per the dispatching brief; not touched here. ## Test plan - [x] `nav.test.ts` (8 tests) and `sidebar-default.test.ts` (5 tests) pass - [x] Frontend production build succeeds, brand/nav assertions pass - [x] Live DOM confirmation: Folders/Recents headings render once sidebar expanded (see above) - [ ] Clean before/after screenshot set with populated Folders/Recents (blocked by live infra instability this session; retry before merge)
…#976) ## Summary `deploy-demo-box.yml`'s `push.paths` filter had no entry for `vendor/open-webui`, the forked chat frontend's source tree that `Dockerfile.open-webui` compiles directly from (`COPY vendor/open-webui ./`). A change under that tree merges to `main` and triggers no deploy at all. PR #971 (nav fixes, merged clean) is the live example: it touched only `vendor/open-webui`, and no deploy run followed. The fork build in PR #938 deployed only because it also touched `deploy/docker/Dockerfile.open-webui`, which the filter does cover, so past deploys were incidental rather than evidence the path worked. The workflow's own comment already documents this exact failure class for `apps/web-console` (PR #786) and `supabase/migrations`. This is the same defect recurring a second time in the same file. I audited every other Dockerfile the deploy job builds (`edge-api`, `control-plane`, `agent-console`, `web-console.prod`, `agent-engine`) plus the ones it does not (`sdk-tests-*`, `toolchain`, `desktop-linux`, non-prod `web-console`). Every COPY/ADD source across all of them falls under `apps/**`, `packages/**`, `deploy/docker/**`, or `go.work(.sum)`, all already present in the filter. `vendor/open-webui` was the only gap. ## Changes - `.github/workflows/deploy-demo-box.yml`: add `vendor/open-webui/**` to `push.paths`, with a comment pointing at the new guard below. - New `.github/ci/lint-deploy-paths-filter.mjs`: parses the deploy workflow's `push.paths` and every `deploy/docker/Dockerfile.*`'s COPY/ADD sources, fails if any source is not covered by the filter. Skips `--from=` stage copies, which read a prior build stage rather than the host filesystem. - Wired into `.github/workflows/ci.yml`'s existing `repo-policy-lints` job (already a required check, already installs `node`+`yaml`), right after the sibling `lint-workflow-check-names.mjs` step. No new CI job. Deliberately not added to `.github/branch-protection-main.json`'s required-checks list: `repo-policy-lints` is already required, so this step riding inside it already fails the PR loudly on a gap. Promoting it to its own named required check is a separate, more sensitive branch-protection change and out of scope here. ## Verification Ran the new guard locally (`npm ci --ignore-scripts && node .github/ci/lint-deploy-paths-filter.mjs`): - Passes after this fix: `Deploy path-filter coverage OK: every COPY/ADD source across 14 Dockerfiles under deploy/docker/ is covered...` - Stashed only the `deploy-demo-box.yml` change and reran: fails loud, naming `vendor/open-webui/package.json`, `vendor/open-webui/package-lock.json`, and `vendor/open-webui` itself as uncovered. Confirms the guard would have caught the original bug. - Re-ran the sibling `lint-workflow-check-names.mjs`: still passes, no regression (23 check names, 6 required contexts). - Both edited workflow YAMLs parse cleanly. ## Deploy note Merging this PR changes `.github/workflows/deploy-demo-box.yml` itself, which is already in its own filter, so the merge will trigger a real deploy to the demo box. That deploy will also carry every other merged-but-undeployed change currently sitting on `main` (including PR #971). Given the open P0 on the shared Supabase connection pool, hold this merge for explicit go-ahead rather than merging on green CI alone. ## Buglog entry Second instance of the same defect class in the same file (first: PR #786, `apps/web-console`). ```json {"error_message":"vendor/open-webui had no entry in deploy-demo-box.yml's push.paths filter; a change under that tree merged to main and triggered no deploy","root_cause":"the paths filter is a hand-maintained allowlist and the fork's frontend source tree (added when the chat frontend was forked to compile from source) was never added to it, so PR #971 (nav fixes touching only vendor/open-webui) merged clean with zero deploy run","fix":"added vendor/open-webui/** to the filter; added .github/ci/lint-deploy-paths-filter.mjs (wired into ci.yml's repo-policy-lints required check) which parses every deploy/docker/Dockerfile.*'s COPY/ADD sources against the filter and fails loud on the next missing entry instead of silently never deploying","tags":["ci","deploy","paths-filter","open-webui","recurring"]} ```
These two are from #938 and had never once run: the sign-in readiness signal they sit behind was failing for every OWUI end-to-end run, so the first time they executed was in this branch, after that fix, and both went red immediately. Neither is a regression from this change and neither was weakened to go green. They both assumed a sidebar that starts expanded, and this fixture's starts collapsed. That single fact produced both failures. The nav renders twice, an icon-only rail whose name is on `aria-label` and an expanded row that renders the same string as text, and `.first()` resolves to the rail either way, so the text assertion read the icon and got "". The collapsed-rail test then waited sixty seconds for a "Close Sidebar" control that only exists while the sidebar is open. So each test now drives the sidebar into the state it is about, through Open WebUI's own toggle rather than by writing its store, and asserts against the row a person can actually see rather than whichever of the two comes first in the document. The assertions themselves are unchanged: visible text on the expanded row, a non-empty accessible name on the rail.
These two are from #938 and had never once run: the sign-in readiness signal they sit behind was failing for every OWUI end-to-end run, so the first time they executed was in this branch, after that fix, and both went red immediately. Neither is a regression from this change and neither was weakened to go green. They both assumed a sidebar that starts expanded, and this fixture's starts collapsed. That single fact produced both failures. The nav renders twice, an icon-only rail whose name is on `aria-label` and an expanded row that renders the same string as text, and `.first()` resolves to the rail either way, so the text assertion read the icon and got "". The collapsed-rail test then waited sixty seconds for a "Close Sidebar" control that only exists while the sidebar is open. So each test now drives the sidebar into the state it is about, through Open WebUI's own toggle rather than by writing its store, and asserts against the row a person can actually see rather than whichever of the two comes first in the document. The assertions themselves are unchanged: visible text on the expanded row, a non-empty accessible name on the rail.
…op storing emails as display names (#952) Three owner requested changes to the forked chat front end. All of them change the vendored source under `vendor/open-webui/`, which is where fork work belongs now that the image builds from that tree (#938, D-036, D-040). ## 1. No intermediate sign in page With `ENABLE_LOGIN_FORM: "false"` and exactly one provider configured, `/auth` rendered a heading, a horizontal rule with nothing above it, and a single "Continue with Hive" button. That is a choice between one option, and the rule is the separator for a password form that never renders. An unauthenticated visitor now goes straight into the provider flow, and the `redirect` query parameter survives the round trip through the mechanism that already existed for it. The failure path was built first, because a broken redirect on the login path locks every user out: * The decision moved out of the page into `src/lib/hive/sso-redirect.ts`, a pure function with twelve tests, following the `nav.ts` plus `nav.test.ts` precedent already in that directory. * **Loop guard.** The bounce nobody had guarded against is a round trip that returns the browser to `/auth` with no `error` parameter and no token cookie. The old condition is true again at that point and the page redirects again, forever. The page now stamps each attempt in `sessionStorage`, and an attempt inside the last fifteen seconds renders the page with an explanation and the provider button as the manual retry instead of redirecting. The stamp is cleared once a session exists. * **Provider errors.** `handle_callback` already redirects every exception to `/auth?error=...`, and an error parameter already suppressed the redirect. It now also renders a persistent message carrying the provider's reason, rather than only a toast that disappears. * **Sign out.** Signing out landed on `/auth` while the provider's own session was usually still live, so an unconditional redirect would have signed the user straight back in and made signing out unreachable. The two user initiated sign out paths now carry `?signed_out=1`, which suppresses the redirect and says so on the page. The token expiry path deliberately does not, since a silent refresh is the wanted behaviour there. * The manual page is not removed. It is the destination for every one of these cases, plus `?form=1` as an unconditional escape hatch. * The orphaned divider is gone: it now renders only when the password or LDAP form it separates is actually on screen. The behaviour is gated on `OAUTH_AUTO_REDIRECT`, and every other precondition (one provider, no password form, no LDAP, no trusted header, no onboarding) stays a runtime check, so a second provider or a re-enabled password form turns it off by itself. **The compose line alone would have been a silent no-op.** `oauth.auto_redirect` is in Open WebUI's `DEFAULT_CONFIG`, so the demo box seeded it false on its first boot and the database has outranked the environment ever since. The key is reconciled from the environment through `owui-patches/hive_rag_env_config.py`, the same path `ENABLE_LOGIN_FORM` needed for the same reason (#722, #772). ## 2. Suggested prompts removed The stock upstream starter prompts ("a fun fact about the Roman Empire", options trading, procrastination) are not ours. Removed the renderer rather than hiding it: the `Suggestions` usage and import in `Placeholder.svelte` and `ChatPlaceholder.svelte`, the component itself, the six sample prompts hardcoded in `backend/open_webui/config.py`, and the plumbing that existed only to serve it (the `onSelect` prop chain through `Messages.svelte` and `Chat.svelte`, and the "Insert Suggestion Prompt to Input" toggle in Settings then Interface, whose only reader was that handler). Removing the renderer rather than the config value is also what makes this survive the persisted config trap above: the demo box has the six stock prompts in its own database from first boot, and no compose change could reach them. Deliberately left alone: the per model `suggestion_prompts` authoring UI in the workspace model editor and admin settings. Those are admin surfaces that D-014 already disables, and surface removal is a separate piece of work. ## 3. Display name showing an email address Five of six accounts on the box store `name` equal to the email string. It is the stored value, not a render time fallback. Root cause, checked against live sources rather than assumed: * `handle_callback` provisioning did `name = user_data.get(username_claim)` and, when absent, `name = email`. * The claim really is absent. Supabase's OAuth authorization server issues a minimal third party OIDC token, standard claims only and no user metadata, confirmed live on both the shared runner and the demo box and already recorded in `docker-compose.yml` where the same finding defeated `OAUTH_ROLES_CLAIM`. * Nothing writes a name at sign up either: `apps/web-console/app/auth/sign-up/page.tsx` calls `supabase.auth.signUp` with no `options.data`. * The provider's discovery document does advertise `name` among `claims_supported`, so preferring the claim when it is present is correct and is unchanged. No claim we do not receive is invented. The fallback now derives a display name from the local part of the address (`first.last@example.com` becomes "First Last") in a standard library only module with its own self check wired into `make test-scripts`. **The user can also fix it themselves.** Open WebUI's Settings then Account already carries an editable display name that persists through `updateUserProfile`; it is verified live in the proof below rather than assumed. That edit survives later sign ins, because the login refresh path only overwrites the name when the claim is actually present, which it never is here. **No migration.** Nothing in this PR rewrites the five existing accounts. That is real user data and the owner's call, proposed separately. ## Also Removed a `console.log` of the entire session user object, bearer token included, from the sign in path. ## Verification * `vendor/open-webui/src/lib/hive/sso-redirect.test.ts`, twelve cases including the loop guard window, the sign out case, and a future dated stamp that must not lock anyone out. Run with vitest, all passing. * `python3 scripts/test_owui_display_name.py`, covering the fallback for an underivable address, a right to left override, a very long local part, and an assertion that the provisioning path actually calls the derivation. Wired into `make test-scripts`, which is a required check. * `python3 scripts/test_owui_rag_env_config.py`, extended with three cases for the new reconciled key, including one asserting compose actually enables it. Passing. * The Open WebUI image builds from this tree. * `make test-owui-frontend`, new. The vitest suites previously ran nowhere at all, which a reviewer caught: the package's `test:frontend` script is referenced only by an upstream workflow file this repository never executes. CI now runs both Hive authored suites, 25 tests, beside `make test-scripts`. * Visual proof: captured against a stack running this branch's build and posted as a comment below. ## Buglog entry To be appended to `.wolf/buglog.jsonl` on `main` in a separate buglog only pull request once this merges, per the protocol in `.claude/rules/openwolf.md`. ```json {"id":"bug-owui-name-is-email","timestamp":"2026-08-17T00:00:00.000Z","related_bugs":[],"occurrences":1,"last_seen":"2026-08-17T00:00:00.000Z","title":"Open WebUI stored every user's email address as their display name","error_message":"Five of six accounts on the demo box have public user.name equal to their email string, so the chat greeting and every avatar initial render an email address","root_cause":"open_webui/utils/oauth.py handle_callback provisioned a new OAuth user with name = user_data.get(OAUTH_USERNAME_CLAIM) and fell back to name = email when the claim was missing. The claim is always missing on this deployment: Supabase's OAuth authorization server issues a minimal third party OIDC token with standard claims only and no user metadata, the same finding that defeated OAUTH_ROLES_CLAIM, and apps/web-console never collects a name at sign up so there is nothing for the provider to send.","fix":"Derive the display name from the email local part in open_webui/utils/hive_display_name.py (stdlib only, self checked by scripts/test_owui_display_name.py in make test-scripts) and call it from the provisioning fallback. The configured claim is still preferred when present, and a name the user sets in Settings then Account survives later sign ins because the refresh path only overwrites from a claim that is actually there. Existing accounts are deliberately not rewritten.","tags":["open-webui","oidc","supabase-oauth","provisioning","display-name","pr-owui-signin"]} ``` ```json {"id":"bug-owui-sso-page-one-choice","timestamp":"2026-08-17T00:00:00.000Z","related_bugs":[],"occurrences":1,"last_seen":"2026-08-17T00:00:00.000Z","title":"Sign in page offered a choice between one option, above an orphaned divider","error_message":"Unauthenticated visitors landed on /auth showing a heading, a horizontal rule with nothing above it, and a single Continue with Hive button","root_cause":"OAUTH_AUTO_REDIRECT was never enabled, and enabling it in compose alone would have been a silent no-op because oauth.auto_redirect is in Open WebUI's DEFAULT_CONFIG and the demo box seeded it false on first boot. The divider is the separator for the password form, and it rendered whenever any provider existed rather than when the form it separates was on screen. The existing auto redirect also had no loop guard: a provider round trip returning to /auth with no error and no token cookie satisfied the same condition again and would bounce forever.","fix":"Enable OAUTH_AUTO_REDIRECT and reconcile oauth.auto_redirect through owui-patches/hive_rag_env_config.py like ENABLE_LOGIN_FORM. Move the decision into the tested pure function src/lib/hive/sso-redirect.ts with a fifteen second sessionStorage attempt guard, render an explanation plus manual retry whenever the redirect is withheld, carry signed_out=1 out of the user initiated sign out paths so signing out is not undone, and render the divider only alongside the form it separates.","tags":["open-webui","auth","sso","redirect-loop","persistent-config","pr-owui-signin"]} ``` ## Review round Four streams read the first commit: CodeRabbit CLI (the GitHub App was rate limited and never started, so the CLI is the CodeRabbit signal here), a mandatory security review, a TypeScript and Svelte review, and a plain adversarial pass. No blocker on the code itself. Every finding is answered in its thread; the substantive ones changed the code: * the loop guard failed open when the browser refuses storage, which is exactly the case it exists for. It now verifies the write and withholds the automatic redirect when it cannot; * signing out lasted one page load while the provider session outlived it, so the next navigation signed the same person back in. The marker is stored now, and set on both sign out paths; * the provider's error text was reflected into a persistent banner on the credential page. The banner carries our own wording; * the `redirect` parameter is now reachable with no interaction, so it is validated as a path inside the application; * the front end tests ran nowhere in CI. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Automatically directs eligible visitors to the configured OAuth sign-in, with safeguards against redirect loops and unsafe destinations. - Shows clearer sign-in, retry, and provider-error messages. - Generates readable display names from email addresses when no name is provided. - **Changes** - Removed suggested prompts from chat placeholders and related settings. - Sign-out and interrupted sign-in flows now return users to a clear authentication state. - Improved OAuth session refresh support. - **Documentation** - Added evidence covering sign-in, prompt, and display-name behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- ## Rebase onto current main, 2026-08-22 Rebased onto `main` at `c30882491`. The base this branch was validated against, `1dc67ee69`, predates the migration of the database off hosted Supabase onto the self-hosted instance, the move of the public auth origin to a same-origin `/auth/v1` route on the console, and the switch to admin-provisioned-only signup. Two conflicts, both additive and both resolved by keeping each side: `.github/workflows/ci.yml`, where `main` added the "Every Caddyfile adapts" step and this branch adds `make test-owui-frontend`, and `Makefile`, where `main` added seven self-checks to `test-scripts` and this branch adds three. ### Still correct against the new baseline Checked rather than assumed, because this branch sits directly on top of what moved. The redirect targets Open WebUI's own `/oauth/{provider}/login`, and Open WebUI builds the provider URL from the `OAUTH_*` variables in compose, which `main` owns and this branch does not touch. So where the authorization server lives is orthogonal to this change; it rides on whatever `main` configures. Admin-provisioned-only signup does not turn the automatic redirect into a trap. A visitor with no account completes the provider round trip, comes back with `?error=`, and the decision function returns `provider-error`, which renders the explained page with a retry rather than bouncing them out again. That arm is one of the sixteen unit tests. `OAUTH_AUTO_REDIRECT` needs the reconciler because `oauth.auto_redirect` is in Open WebUI's `DEFAULT_CONFIG` and the demo box seeded it false on first boot. That is why the compose variable is joined to a `hive_rag_env_config.py` entry rather than set alone. Contrast `OAUTH_SCOPES`, which is a plain `os.getenv` and needs no reconciler; PR #787 records that trace in its own comment block. ### Review findings addressed Four threads, four fixes, no rebuttals. The email fallback in the display name helper returned the address unsanitized. A local part made only of characters `_sanitize` drops leaves no words to join, and that branch returned the address verbatim, so a bidirectional override in an address went straight into a stored display name rendered next to other people's names. The fallback now sanitizes the address too, and drops to a neutral literal only when nothing alphanumeric survives, because `_sanitize` leaves the `@` of any address that has one and a display name made of punctuation is not an improvement. Reading `localStorage` throws outright when browser storage is blocked, and `hasExistingSession` ran before `loaded = true`, so an affected visitor got no sign in page and no manual provider button at all. The new frontend test runner depended on host node, which breaks the Docker-only testing contract in `CLAUDE.md`. It now runs vitest inside a pinned node image with the scratch directory as its only mount and the caller's uid, so the npx cache is not left root owned. `docker run` rather than `exec docker run`, deliberately: `exec` would replace the shell and the EXIT trap would never fire, leaking the scratch directory every run. Grammar fix in the committed evidence log. ### Merge order matters, with #951 This branch adds the only CI step that runs the vendored front end's unit tests. PR #951's `agentTasks.test.ts` could not be loaded by that runner, so whichever of the two merged second would have turned this new required check red. That has been fixed on #951's head, not worked around here. Either order is now safe. ### Verification - All 31 vendored front end tests pass in the container, which also picks up the `sidebar-default` and `nav` suites that arrived with the forked tree in #938 and were themselves running nowhere. - `make test-scripts` green, including `main`'s seven new self-checks. - `npm run lint:proof-tokens` green. - Mutation checks, each made to fail on purpose: removing the signed out guard from the redirect decision fails the "lets a user actually sign out" case; restoring the raw email fallback fails the override assertion with the override visible in the message. ### Visual proof Fresh captures posted on this pull request as release assets, replacing the 2026-08-17 set, which was taken against the hosted Supabase baseline that has since been deleted. Method and limits in `docs/proof/owui-signin-no-intermediate-page-2026-08-22/README.md`. The three captures are the states where redirecting would be wrong, which is the half of this change that can lock every user out, rather than the happy path: signed out does not bounce back in, a provider error explains itself and offers a retry, and `?form=` still reaches the manual page. The bundle is the real output of `docker build --target frontend` on this branch; the backend is stubbed on a loopback origin, because the development box's `.env` still points `SUPABASE_URL` at the deleted hosted project and `oauth.auto_redirect` is still false on the deployed instance, since the variable that turns it on ships here. No password was set, reset or rotated to work around that. No credential appears in any captured URL. ### Buglog entry To be appended to `.wolf/buglog.jsonl` on `main` in a separate buglog-only pull request after this merges. ```json {"id":"owui-display-name-fallback-skipped-sanitize","date":"2026-08-22","error_message":"A local part made only of characters the sanitizer drops produced a display name containing a bidirectional override, for example U+202E followed by @example.com","root_cause":"The empty-words fallback in display_name_from_email returned the email argument verbatim, which was the one path that bypassed _sanitize entirely","fix":"The fallback sanitizes the address too and returns a neutral literal when nothing alphanumeric survives; regression cases cover both arms","tags":["owui","display-name","unicode","bidi","sanitization"]} {"id":"owui-signin-localstorage-throw-blanks-page","date":"2026-08-22","error_message":"With browser storage blocked, the sign in page rendered neither the page nor the manual provider button","root_cause":"hasExistingSession read localStorage.token unguarded, and it ran before loaded = true, so the throw left the page in its pre-render state","fix":"The cookie check is hoisted above a try/catch around the storage read, so a throw degrades to the cookie answer instead of blanking the page","tags":["owui","auth","localstorage","availability"]} {"id":"owui-frontend-test-runner-needed-host-node","date":"2026-08-22","error_message":"make test-owui-frontend required host-installed node and npm, contrary to the repository's Docker-only testing contract","root_cause":"The script called npx vitest directly on the host","fix":"Runs vitest in a pinned node image with the scratch directory as the only mount and the caller's uid; docker run rather than exec docker run, so the EXIT trap still fires and the scratch directory is not leaked","tags":["ci","docker","tests","tooling"]} ``` <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> The PR streamlines the vendored Open WebUI sign-in flow, removes stock prompt suggestions, and derives safer display names when OAuth omits the configured name claim. - Automatically starts the sole configured OAuth provider while preserving manual, error, and signed-out states. - Reconciles the persisted auto-redirect setting from deployment configuration. - Removes suggestion rendering, defaults, and obsolete settings plumbing. - Adds Dockerized frontend tests and display-name/configuration self-checks. </details> <details open><summary><h3>Confidence Score: 5/5</h3></summary> The PR appears safe to merge. No blocking failure remains, and the previously reported host-Node dependency has been replaced with a Dockerized test runner. </details> <details open><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | scripts/test-owui-hive-frontend.sh | Replaces the host-Node test invocation with a pinned Docker-based Vitest runner, resolving the previously reported portability problem. | | vendor/open-webui/src/routes/auth/+page.svelte | Integrates automatic SSO routing with explicit manual, provider-error, loop-guard, and signed-out states. | | vendor/open-webui/src/lib/hive/sso-redirect.ts | Centralizes the automatic SSO redirect decision and validates internal post-authentication destinations. | | deploy/docker/owui-patches/hive_display_name.py | Derives bounded, sanitized display names from email local parts when the identity provider supplies no name. | | deploy/docker/owui-patches/hive_rag_env_config.py | Reconciles the persisted OAuth auto-redirect setting from the deployment environment. | | vendor/open-webui/src/lib/components/chat/Chat.svelte | Removes the obsolete suggestion-selection plumbing after eliminating stock prompt suggestions. | </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant U as Unauthenticated visitor participant A as Open WebUI /auth participant P as OAuth provider U->>A: Visit protected application alt Auto-redirect conditions satisfied A->>P: Start OAuth flow P-->>A: Callback alt Session established A-->>U: Continue to validated application path else Error or no session A-->>U: Render explanation and manual retry end else Signed out, manual form, or blocked redirect A-->>U: Render manual sign-in page end ``` </details> <sub>Reviews (4): Last reviewed commit: ["docs: fresh capture of the sign in refus..."](7e9fbc8) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=54088038)</sub> <!-- /greptile_comment --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
These two are from #938 and had never once run: the sign-in readiness signal they sit behind was failing for every OWUI end-to-end run, so the first time they executed was in this branch, after that fix, and both went red immediately. Neither is a regression from this change and neither was weakened to go green. They both assumed a sidebar that starts expanded, and this fixture's starts collapsed. That single fact produced both failures. The nav renders twice, an icon-only rail whose name is on `aria-label` and an expanded row that renders the same string as text, and `.first()` resolves to the rail either way, so the text assertion read the icon and got "". The collapsed-rail test then waited sixty seconds for a "Close Sidebar" control that only exists while the sidebar is open. So each test now drives the sidebar into the state it is about, through Open WebUI's own toggle rather than by writing its store, and asserts against the row a person can actually see rather than whichever of the two comes first in the document. The assertions themselves are unchanged: visible text on the expanded row, a non-empty accessible name on the rail.
…pplication (#951) Owner directive, 2026-08-17: "stop using iframes and keep it native inside OpenWebUI." `https://chat-hive.scubed.co/agents` rendered exactly one `<iframe>`, `loading="eager"`, pointed at `/agent-workspace/tasks`, which booted `apps/agent-console` (a second whole Next.js application) inside the page. That is why the agent surface still looked exactly as it did before the shell landed, why it was slow, and why a task could never become part of the conversation. The frame is gone. Scope is bounded deliberately: visual and functional parity with what the frame showed, natively, which is the task composer and the task list. No tool call cards, no progress panel, no transcript. All three are specified in `spec-2026-08-17-agent-run-surface` and blocked on the event relay (its step S6), and a tool card with nothing to render is worse than today's status row. ## Two premises in the brief that were already stale Both checked in the tree at `1dc67ee69`, both reported before building anything. 1. **`apps/agent-console/middleware.ts` does not send `X-Frame-Options: DENY` or `frame-ancestors 'none'`.** It sends `SAMEORIGIN` and `frame-ancestors 'self'` (`middleware.ts:80-84`), changed in #938 with a comment naming the shell as the reason. Nothing was working around anything: the frame was permitted because one Caddy listener serves both applications on one origin, so `'self'` is a real origin check that matches. 2. **The frame's `src` carried no credential.** It was `embed=1` and `theme=<light|dark>`, both re-validated and rebuilt rather than reflected (`middleware.ts:25-31`, `:93-100`). What the credential path actually was: `apps/agent-console` holds its own first-party Supabase session in cookies on the shared origin, and its browser code calls `/v1/agent/*` directly with `session.access_token`. That token comes from a direct Supabase sign-in, so it carries the `tenant_id` claim the `custom_access_token_hook` mints, and `JWTMiddleware` accepts it with no fallback. The frame existed because a second application had a second, better token. ## The problem, and why the recommended fix needed one correction The chat frontend holds Open WebUI's own session token, which edge-api has never heard of, plus a Supabase access token minted through Supabase's **OAuth-server** grant, which does not run `custom_access_token_hook` and therefore carries **no `tenant_id`**. `JWTMiddleware` rejects that (`middleware.go:123-129`). That is D-023 working, pinned by `inert_token_test.go`, and it is not relaxed here. Worse for any browser-side fix: **that OAuth token is not in the browser at all.** Open WebUI keeps it server side and resolves it per request through `get_system_oauth_token`, refreshing it when expired (`utils/middleware.py:3012-3045`). Handing it to page JavaScript would be a downgrade, not a fix. So the recommendation to reuse the mechanism `hive_jwt_forward.py` plus `owui_unwrap.go` already run in production is adopted. The correction: **`__metadata.upstream_auth` cannot serve this surface**, because three of the four agent-task calls have no JSON body to hide a token in. | Call | Method | Body | |---|---|---| | `GET /v1/agent/tasks` | GET | none | | `POST /v1/agent/tasks` | POST | JSON | | `GET /v1/agent/tasks/{id}` | GET | none | | `POST /v1/agent/tasks/{id}/cancel` | POST | none | (`apps/edge-api/internal/agenttask/handler.go:38-72`.) ## The design, three layers ### 1. edge-api: a second carrier on the same boundary `X-Hive-Upstream-Auth` carries the per-user token for requests with no body to carry it in. **This is not a new auth boundary.** The trust decision is unchanged and unchanged in strength: the header is honoured only when `Authorization` is exactly the shim key, which is the identical gate the body carrier already sits behind. What changes is the carrier, not who is allowed to use it. Properties, each with a test: * Honoured only under the shim key. Under a real API key, a real JWT, or no credential at all, it is ignored. * **Stripped from the forwarded request on every branch**, including the ignored one and including when the middleware is disabled. A header a client controls must never be readable by a handler, a log or an audit sink, because a handler that can read it is one that could later be taught to trust it. * Same 8 KiB cap as the body carrier, shared through one normaliser so the two cannot drift into accepting different shapes of the same credential. * Fails closed when present but unusable, rather than forwarding with the shim key still on `Authorization`. `requiresPerUserAuth` grows the agent-task paths. It previously covered `/v1/chat/completions` only, which meant a shim-key request to `/v1/agent/tasks` with no user token would pass through and **bind to the shim's own principal**, listing and cancelling the shim account's tasks instead of the user's. Nothing sent that request; this change is what keeps the new proxy from being one bug away from sending it. It is a tightening, not a widening: a silent mis-attribution becomes a 401. One latent defect fixed while in the same function: a present-but-empty `__metadata.upstream_auth` previously returned `unwrapOK` with an empty token, which skipped the fail-closed arm and forwarded to `/v1/chat/completions` with the shim key intact. It now takes the missing-carrier arm, so it 401s and logs like every other missing token. Not touched, deliberately: the tenant fallback is still gated on `IsOWUIUnwrapped` and is not widened by one line; `Selector` is unchanged; `inert_token_test.go` passes unmodified. ### 2. The chat container: a server-side proxy Only the **frontend** of `hive-open-webui` is built from the fork; the Python backend stays upstream's pinned image on purpose (Dockerfile header, D-036). So this arrives as a copied module plus one asserted splice, the same shape as `hive_model_picker` and `hive_rag_env_config`. The frontend builds with `adapter-static`, so there is no SvelteKit server to put it in instead. `deploy/docker/owui-patches/hive_agent_proxy.py`, mounted at `/api/v1/hive/agent`: * Every route depends on `get_verified_user`, and **no route reads a user id, a tenant id, an email or a token from the request**, so a caller cannot influence which principal edge-api resolves. * Presents the shim key on `Authorization` and the user's token on the carrier header. Neither is ever returned to the browser, logged, or included in an error. * Four named operations with the task id validated as a UUID before it is interpolated. Deliberately not a general-purpose proxy, which is the shape #770 removed from this image. * The create body is **rebuilt from two named fields** rather than passed through, so nothing a caller invents (a `__metadata` block, for one) reaches edge-api. * Adds no configuration: `OPENAI_API_BASE_URL` and `OPENAI_API_KEY` are already set on this container, so the shim key gains no new home. ### 3. The frontend `src/lib/hive/agentTasks.ts` and `AgentTasks.svelte`, plus the `/agents` route. Ported from the console, including two decisions that are easy to mistake for accidents and are not: the `unknown` status sentinel (a status this build cannot name still gets a row, because dropping it made a submitted task vanish) and the two engine sentinels (a deployment with no runtime reads "Blocked" in the warning register, never "Failed" in red next to a real failure). ## What happened to `/agent-workspace`, stated plainly It still exists and it is still served. Nothing in this PR removes it, and the PR should not be read as removing it. Three separate things get confused under that name, so all three, precisely: * **The `agent-hive.scubed.co` subdomain is already gone**, and was before this PR. It is NXDOMAIN and decommissioned (D-026, verified 2026-07-28). There is no separate agent host and has not been one for some time. * **`/agent-workspace` is a path on the chat host**, proxied to the `agent-console` container by `deploy/docker/Caddyfile.owui:161-168`. That rule is untouched here, so the path still resolves and still serves `apps/agent-console`. * **Nothing in the product points at it any more.** The nav row goes to `/agents`, and `/agents` no longer loads it. A user reaches it only by typing the URL. So after this PR the old surface is unreachable through the interface but still reachable by URL. Making it actually stop being a thing is the retirement described in the next section, and it is deliberately not in this PR: the console is the only surface that has ever launched a task against the live engine, so it is the control if the native path misbehaves on the box. Recommendation is to retire it once this has run there for a day. ## On the composer: the extraction was taken, not the fallback Answering the directive's point 1 plainly. `MessageInput.svelte` cannot be consumed wholesale: 2222 lines, 29 exported props, several required and chat-specific (`history`, `selectedModels`, `createMessagePair`, `stopResponse`, `taskIds`, `messageQueue`), and its container's classes are computed from chat stores. Cloning its CSS was banned, correctly. So the **presentational shell was extracted**, which was the preferred option: * `src/lib/hive/ComposerShell.svelte` is `#message-input-container` lifted verbatim: the rounded surface, the border, the hover and focus-within treatment, the inner padding. It reads no store; the two values that used to compute its classes arrive as props. * `src/lib/hive/ComposerSendButton.svelte` is `#send-message-button` lifted verbatim, classes and glyph unchanged. * `MessageInput.svelte` now renders both. **No behavioural edit was needed**, which was the stated condition for taking this option: the diff there is two tag swaps, one component swap and one import. What is deliberately **not** shared is the row of controls inside the container. Chat's carries attach, tools, skills, web search, voice and the model picker; the agent composer's carries the pack toggle. Sharing that row would mean sharing chat state with a surface that has none of it, which is the coupling the extraction exists to avoid. Per the directive: the heading block and the helper paragraph are deleted. The pack choice is a segmented **Knowledge work** / **Coding** toggle inside the composer. The one line describing the selected mode survives as a single muted line under the composer, because it is genuinely useful and it does not turn the composer back into a form. `Enter` sends and `Shift+Enter` is a newline, which is the chat composer's own behaviour. The two packs and their semantics are unchanged, and so is the default (`coding-pack`): this is a presentation change, not a capability change. Chat's own composer is proved unchanged two ways, per the condition set: the vitest suite is green, and before and after screenshots of the **chat** composer are posted below alongside the agent one. ## Does `apps/agent-console` still need to exist? Honest assessment, not acted on here. **No route in it is reachable from the product any more.** Nothing frames it, and the only two links that ever pointed at it are gone: the nav row goes to `/agents`, and `/agents` no longer loads `/agent-workspace/tasks`. It is now a signed-out URL that a user reaches only by typing it. What retiring it would take, in order: 1. Delete the `@agentConsole` matcher and its `reverse_proxy` from `deploy/docker/Caddyfile.owui` (lines 145 to 168), which is what makes `/agent-workspace` resolve at all. 2. Remove the `agent-console` service from `deploy/docker/docker-compose.yml` and its `Dockerfile.agent-console`, plus the `NEXT_PUBLIC_EDGE_API_BASE_URL` and `EDGE_API_INTERNAL_BASE_URL` wiring that exists only for it. 3. Remove the `agent-console-unit` job from `.github/workflows/ci.yml`, which is a required check, so branch protection needs updating in the same change or the merge gate waits forever on a job that no longer runs. 4. Delete `apps/agent-console/`. Two things worth keeping first, and neither is code: its `proof/` harness, and the accessibility reasoning in `task-console.tsx` (it refuses `--color-ink-3` for body text on a measured contrast argument). The second is already carried across into `AgentTasks.svelte`'s comments and CSS. One caveat against deleting it immediately: it is the only surface that has ever launched a task against the live engine, so it is the control if the native path misbehaves on the box. Recommendation is to retire it in a follow-up once this has run on the demo box for a day, not in this PR. ## Testing * `go test ./apps/edge-api/... -count=1 -short` in the toolchain container: green, whole module, including nine new cases for the carrier and the fail-closed paths. * `python3 scripts/test_owui_agent_proxy.py`, wired into `make test-scripts` (a required CI check). Pure standard library, no framework, matching the other `scripts/test_owui_*.py` self-checks. It is mutation tested: swapping the two credentials onto the wrong headers, deleting the missing-token guard, and forwarding the whole request body each make it fail. * `npm run test:frontend` for the fork, covering the decoder, the status mapping and the four calls. **A gap named rather than hidden.** `ci.yml` has vitest lanes for `apps/desktop`, `apps/web-console` and `apps/agent-console`, and none for `vendor/open-webui`, so every test ever written under `src/lib/hive` was coverage on paper only. This adds `npm run test:frontend -- --run` to the image's frontend build stage, which means the fork's tests gate the nightly build and the demo deploy. That is not the same as gating a pull request and is not claimed as such; a PR-time lane for this tree is a follow-up. ## Buglog entry ```json {"id":"owui-agent-shim-principal-gap","date":"2026-08-17","title":"OWUI shim-key requests to /v1/agent/tasks would have bound to the shim's own principal","error_message":"none observed; latent","root_cause":"requiresPerUserAuth in apps/edge-api/internal/auth/owui_unwrap.go returned true only for /v1/chat/completions, so a shim-key request to any other path fell through with the shim key still on Authorization and resolved as the shim account. Correct for the paths that existed then (embeddings and text-to-speech authenticate as the shim by design), wrong the moment a second per-user OWUI path appeared. A present-but-empty __metadata.upstream_auth had the same effect on the chat path itself, because it returned unwrapOK with an empty token and skipped the fail-closed arm.","fix":"Extended requiresPerUserAuth to /v1/agent/tasks and its subtree, and made an empty upstream_auth report as a missing carrier so it takes the 401 arm and the warn log.","tags":["auth","edge-api","owui","fail-closed","tenancy"]} ``` ## Visual proof: not posted yet, and exactly what exists today Stated precisely, because an earlier version of this section said screenshots were "committed under `docs/proof/` and posted in a comment". Neither was true. Nothing is committed under `docs/proof/` in this diff, no image is on this PR, and `owui-nightly.yml` has no step that commits or comments; it only uploads a transient CI artifact. That sentence described intent before the run finished and read as completed work. It will not be restated until an image is actually visible on this page. **What has been verified on a real stack with a real signed-in session**, run 32066161156, the OWUI end-to-end job on this branch: - `the agent workspace opens inside the shell and frames nothing` passed. That is the load-bearing one: an `iframe` count of zero on `/agents`, the composer present and accepting typed input, both toggle options clickable, the list painting exactly the rows the API returned matched against that response's own text, and the call to the proxy answering something other than 401, which is the single failure mode this design has. - `proof capture: the agent surface, natively, in both palettes` passed, so the images were produced. - Every chat spec passed on that same build: send and stream, multi-turn, model switch, tenant model visibility, sign-out. That is evidence the composer extraction did not break chat, and it does not depend on anyone reading a screenshot. **Why no image was attached at first, and a finding worth its own paragraph.** The captures were being written into `playwright-report-owui/proof`, inside the HTML reporter's own output folder. That reporter clears its folder before writing the report, so **every capture was produced and then deleted**, and the result looked exactly like a capture step that had never run. Nothing failed, nothing warned, and the test that wrote them passed. Confirmed by downloading the artifact and finding no `proof/` directory rather than by inferring it. Anyone adding a capture step to a Playwright suite in this repository will walk into the same trap, which is why it is recorded here rather than only fixed: proof must never be written inside a reporter's output directory. The captures now go to a sibling directory with its own upload step. This is the same silent-absence shape that let the end-to-end gate rot unnoticed, where an artifact that is missing and an artifact that was never asked for are indistinguishable after the fact. **Two attempts to re-capture since then both failed outside this branch**, and neither is counted as anything: - Run 32112158077: the sign-in helper's consent hop exceeded its 30 second budget. The container log shows the token exchange completing 28 seconds after its redirect and the OAuth session being stored, so the login worked and was simply late. - Runs 32113599893 and 32114089597: the seeder's first write returned `POST /tenants -> 522` in both. Without fixture credentials the OWUI project matches zero spec files, so nothing in this PR ran at all. **Correction to an earlier version of this section**, which called that 522 an external Supabase problem. It is very probably ours. The same job log carries `(ECHECKOUTTIMEOUT) unable to check out connection from the pool after 15000ms in Session mode`, which is the documented 15-client session-mode pooler ceiling being hit, and that single cause explains both symptoms: a PostgREST request that cannot get a database connection hangs until Cloudflare gives up with a 522, while a direct `pg` client gets the checkout timeout by name. Blaming the provider was the comfortable read and the evidence does not support it. I also want to retract a piece of reasoning rather than quietly drop it: I probed `/rest/v1/` unauthenticated, got a 401, and treated that as evidence the origin had recovered. An unauthenticated request never reaches the connection pool, so that probe could not have detected the condition that matters and proved nothing. Neither failure touches a file in this diff, and a run that never reached the suite is an absence of information rather than a result. The captures are outstanding for that reason and for no other. **How they will be posted.** Through `scripts/post-pr-visual-proof.sh`, which uploads to a release asset. A raw link pinned to this branch 404s the moment the branch is deleted, and this repo squash-merges and deletes branches, so that mechanism produces proof which expires exactly when the PR it proves gets merged. ## A third thing, and the budget I did not raise Investigating why the proof runs kept failing turned up a cause that is not a test problem. The consent and sign-in pages are web-console's, on a different origin, and web-console is served by a development build, so it compiles each page the first time anyone requests it. On run 32112158077 the first two login attempts produced no page within 30 seconds and the third completed the whole exchange in 21 seconds once warm. That is a route compiling on demand, not a slow protocol. The harness now waits for the consent app to be serving before it starts timing the login. **The 30 second login budget is deliberately unchanged.** Raising it was the obvious move and the wrong one: it would have buried a cold compile inside a per-login timeout, and every future slow login would then look exactly like this one, which is how the next real regression gets absorbed instead of noticed. The new wait is a separate budget for a separate thing, the app becoming reachable at all. It also adds no new failure mode, because the assertion it precedes already requires a DOM that exists only on the consent origin. Why that app is served in dev mode is a product question, it is deliberate per D-022, and it is filed as #967 rather than decided here. It is a plausible cause of the slow sign-in being reported in real use. ## Two things a reviewer should know **The OWUI end-to-end harness was red before this branch and is fixed here.** It still is on `main`, at that same readiness check, which is independent evidence this fix is real and not a symptom of something else on this branch. It is also a different defect from the provisioning race PR #963 is separately fixing, so the two should not be read as one problem. Its sign-in readiness signal waited for a `button` named "New chat", and Open WebUI renders both New Chat controls as anchors carrying an `aria-label`, so that query could never match. The sign-in it guards actually succeeds; the downloaded failure screenshot shows a working signed-in chat page. A sibling file already matched either role for exactly this reason. That fix is why any of the verification above exists, and it is separate from this feature. **Two nav tests from #938 went red the first time they ever ran**, which was in this branch after that fix. Neither is a regression here and neither was weakened. Both assumed a sidebar that starts expanded while this fixture's starts collapsed, so a text assertion resolved to the icon-only rail and read "", and the collapsed-rail test waited for a control that only exists while the sidebar is open. Each test now drives the sidebar into the state it asserts about. --- ## Rebase onto current main, 2026-08-22, and what this closes Rebased onto `main` at `c30882491`. The base this branch was validated against, `1dc67ee69`, predates the migration of the entire database off hosted Supabase onto the self-hosted instance on the demo box, the move of the public auth origin to a same-origin `/auth/v1` route on the console, and the switch to admin-provisioned-only signup. One conflict, in `Makefile`, where `main` had added seven self-checks to `test-scripts` and this branch adds one; both are kept. ### This is the fix for the top demo blocker, issue #540 Re-confirmed live on the box on 2026-08-22: a user signs into `chat-hive.scubed.co` normally, clicks **Agents**, and is presented with an email and password form captioned that the workspace is separate from chat. Proved there by difference rather than guessed: chat's OAuth handshake mints an Open WebUI token and never writes the `sb-...-auth-token` cookie that the embedded `apps/agent-console` reads, and injecting the same session's Supabase cookies on the `chat-hive` origin makes the real workspace render immediately. That is the second-application problem this branch removes. The credential the embedded application was looking for is no longer needed by anything, because the surface is native and authenticates with the Open WebUI session the user already has: the panel calls `/api/v1/hive/agent/*` on the chat origin, and `deploy/docker/owui-patches/hive_agent_proxy.py` brokers it server side through `get_system_oauth_token`, which is the one place the user's Supabase token is reachable. #540's own analysis named that as the only possible broker. ### Does a second credential prompt remain reachable **Yes, by one route, and this branch does not close it.** `/agent-workspace/*` is still proxied to `apps/agent-console` by `deploy/docker/Caddyfile.owui`, and that application still renders its own email and password form, so a typed or bookmarked `https://chat-hive.scubed.co/agent-workspace` still reaches one. This branch's only edit to that file is a comment; it changes no route. No route inside the chat interface leads there any more. `vendor/open-webui/src/lib/hive/nav.ts` points the sidebar entry at `/agents`, and the only two remaining mentions of the old path anywhere in the front end are historical comments. So the demo path is fixed and the residual is a URL nobody is shown. Answering that path 404 was considered and rejected here rather than skipped: the Tauri desktop app targets `/agent-workspace` as its console base path (`apps/desktop/src/settings.ts`, `apps/desktop/src-tauri/src/settings.rs`), and `apps/web-console/tests/e2e/_probe/agent-workspace-flows.spec.ts` is a coverage ledger over that surface. Retiring `apps/agent-console`, or moving the desktop app onto the native surface first, is a separate decision with a far larger blast radius than this pull request. ### One coupling worth knowing before this is demoed The proxy reads the user's Supabase token through `get_system_oauth_token`, which goes through `OAuthManager.get_oauth_token`, which refreshes five minutes before expiry and **deletes the OAuth session outright when that refresh fails**. That is issue #782, still unfixed on `main` and fixed by PR #787. Until #787 lands, this agent surface stops working roughly 55 minutes after sign-in for the same reason chat does, and the panel will correctly report "Your Hive sign-in could not be confirmed. Sign in again and retry." A demo that runs longer than that from a single sign-in needs #787 merged first. ### Review findings addressed on the rebased head Two threads, both fixed rather than argued, plus one defect found while verifying them. A list refresh overlapping a create or a cancel replaced the whole task array with its older answer, dropping the row the user had just submitted or reverting one they had just cancelled, and when that stale answer held no in-flight task the poll loop stopped too and the screen did not recover without a reload. A refresh now captures a mutation counter before its request goes out and discards its own answer if a create or a cancel landed while the request was open. The third entry in the identity smell tuple in `scripts/test_owui_agent_proxy.py` could not fail: for `headers.get('Authorization')` the accessor templates expanded to `request.query_params.get('headers.get('Authorization')')`, a string no Python source can contain, so that iteration reported a pass over the header read it was named after. The header read now has its own direct test against the whole `request.headers` attribute, and it fails on purpose when the string is planted on a line that never executes. Found while running the above: `agentTasks.test.ts`, 203 lines of assertions, was running in no job at all. The module imported `$lib/constants`, which reaches `$app/environment`, and the only runner that covers this front end runs plain vitest with no alias resolution, so the file could not be loaded. It would have turned that required check red on whichever of #951 and #952 merged second. The API base is now a parameter with a production default and the component passes the dev-aware value, so a built bundle and `npm run dev` both behave exactly as before, and the tests load with no configuration: 16 of them, one of which fails when `encodeURIComponent` is dropped from the cancel path. ### Verification on the rebased head - 31 vendored front end tests pass across three files, 16 of them in `agentTasks.test.ts`, which had never executed before this rebase. - `make test-scripts` green, including `test_owui_agent_proxy.py` and the seven self-checks `main` added. - `go test ./apps/edge-api/internal/auth/... -short` green, which covers the `owui_unwrap.go` change against `main`'s newer `x-api-key` normalisation (#954). - `Caddyfile.owui` validates against the pinned Caddy image, using `main`'s new "Every Caddyfile adapts" CI step. ### Visual proof Posted on this pull request as release assets, with the full method, the artefacts of the stub, and the `/agent-workspace` residual in `docs/proof/agents-native-no-iframe-2026-08-22/README.md`. Measured in the page rather than asserted: zero password inputs, zero iframes, and no "Sign in" text on `/agents`. The capture uses the real bundle from `docker build --target frontend` on this branch with a stubbed backend on a loopback origin, and says so on every artefact. A live capture is not available before merge for two reasons that are not properties of this branch: the panel needs the `owui-patches` router this branch adds, which exists only in a rebuilt image, so bundle interception against the deployed origin would 404; and the development box's `.env` still points `SUPABASE_URL` at the hosted Supabase project deleted in the migration, which now returns `ENOTFOUND`, so no session can be minted. No password was set, reset or rotated to work around that. ### Buglog entry To be appended to `.wolf/buglog.jsonl` on `main` in a separate buglog-only pull request after this merges, per the openwolf protocol. ```json {"id":"owui-agents-second-credential-prompt","date":"2026-08-22","error_message":"Clicking Agents in the chat sidebar presented its own email and password form captioned that the workspace is separate from chat, one click after a successful chat sign-in","root_cause":"The Agents route embedded apps/agent-console in an iframe, and that application authenticates from a Supabase SSR cookie on the chat origin which chat's OAuth handshake never writes: the handshake mints an Open WebUI token only, and the user's Supabase token is reachable only server-side inside the chat container as the stored OAuth token","fix":"Removed the iframe and rendered the task surface natively in the chat application, authenticating with the Open WebUI session the user already holds and brokering the Supabase token server-side through a FastAPI router added by owui-patches/hive_agent_proxy.py","tags":["owui","auth","agents","iframe","session","demo-blocker","issue-540"]} {"id":"agent-tasks-stale-poll-overwrites-mutation","date":"2026-08-22","error_message":"A newly created agent task row disappeared, or a cancelled row reverted, and polling sometimes stopped entirely until the page was reloaded","root_cause":"refresh assigned the fetched list over the whole tasks array, so a poll already in flight when a create or cancel completed landed afterwards and overwrote it, and schedulePoll then decided from that stale array and stopped when it held no in-flight task","fix":"A mutation counter captured before the request goes out; refresh discards its own answer when a create or cancel landed while the request was open","tags":["svelte","race","polling","agents"]} {"id":"agenttasks-unit-tests-never-ran","date":"2026-08-22","error_message":"agentTasks.test.ts reported no failures because it was never loaded by any job","root_cause":"The module imported $lib/constants, which reaches $app/environment, and scripts/test-owui-hive-frontend.sh runs plain vitest over copied files with no SvelteKit alias resolution, so the test file could not be loaded at all","fix":"The API base is a parameter with a production default and the component passes the dev-aware value, so the module imports nothing and the tests load with no configuration","tags":["tests","unfailable-check","vitest","sveltekit"]} {"id":"owui-agent-proxy-smell-check-unfailable","date":"2026-08-22","error_message":"The identity smell loop in test_owui_agent_proxy.py reported a pass over a header read it was named after","root_cause":"The tuple entry headers.get('Authorization') was expanded through accessor templates into request.query_params.get('headers.get('Authorization')'), a string no Python source can contain, so that iteration asserted nothing","fix":"Separated the header smell into a direct substring test against the whole request.headers attribute, demonstrated failing on purpose","tags":["tests","unfailable-check","security"]} ``` <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> The PR replaces the iframe-hosted agent workspace with a native Open WebUI task surface and brokers per-user agent API credentials through a constrained server-side proxy. - Adds a guarded upstream-auth header carrier and fail-closed agent-task authentication. - Adds native task composition, listing, cancellation, polling, and shared composer presentation. - Updates frontend, proxy, authentication, and end-to-end verification coverage. </details> <details open><summary><h3>Confidence Score: 5/5</h3></summary> The PR appears safe to merge. No blocking failure remains; the mutation-generation guard prevents stale refreshes from overwriting completed creates or cancellations, and ID filtering prevents the create/refresh duplicate-row race. </details> <details open><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | apps/edge-api/internal/auth/owui_unwrap.go | Adds a stripped, shim-gated header carrier for per-user agent credentials and extends fail-closed authentication to agent-task paths. | | deploy/docker/owui-patches/hive_agent_proxy.py | Adds the authenticated server-side broker that resolves each user’s OAuth token and forwards only the four supported task operations. | | vendor/open-webui/src/lib/hive/AgentTasks.svelte | Implements native task composition, polling, cancellation, stale-refresh suppression, and create-row deduplication; both previously reported races are fixed. | | vendor/open-webui/src/lib/hive/agentTasks.ts | Defines the typed agent-task client, decoding, status handling, and constrained proxy calls used by the native surface. | | vendor/open-webui/src/routes/(app)/agents/+page.svelte | Replaces the framed agent application with the native AgentTasks component. | | vendor/open-webui/src/lib/components/chat/MessageInput.svelte | Adopts extracted composer shell and send-button components without changing the chat composer’s behavior. | | apps/web-console/e2e/phase-19/owui/09-agent-workspace-nav.spec.ts | Verifies native rendering, absence of iframes, proxy authentication behavior, navigation, task rows, and visual-proof capture. | </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant U as Signed-in user participant UI as Native OWUI agent surface participant P as OWUI agent proxy participant A as Edge API auth middleware participant T as Agent-task API U->>UI: Open /agents UI->>P: "GET/POST /api/v1/hive/agent/*" P->>P: Resolve server-side OAuth token P->>A: Shim authorization + upstream-auth carrier A->>A: Validate shim gate, strip carrier, unwrap user token A->>T: Request authorized as signed-in user T-->>A: Task response A-->>P: Task response P-->>UI: Sanitized response UI-->>U: Native task list and composer ``` </details> <sub>Reviews (4): Last reviewed commit: ["fix: deduplicate the created task row ag..."](f3df569) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=54086925)</sub> **Context used:** - Knowledge Base — [Control-plane marketplace-backed agent task workflow](https://app.greptile.com/scubed/-/custom-context/knowledge-base/sakibsadmanshajib/hive/-/docs/control-plane-agent-workflows.md) - Knowledge Base — [Edge API security boundary](https://app.greptile.com/scubed/-/custom-context/knowledge-base/sakibsadmanshajib/hive/-/docs/edge-api-security-boundary.md) <!-- /greptile_comment --> --- ## Buglog entry, third (added 2026-08-23) Alongside the two above. To be appended to `.wolf/buglog.jsonl` on `main` in the same buglog-only pull request after this merges, per the openwolf protocol. Not appended on this branch: a branch that appends a line conflicts with every other branch that appended one, and an unmergeable pull request gets no CI run at all (issue #873). ```json {"id":"owui-unwrap-carrier-strip-unobservable","date":"2026-08-23","title":"Header-carrier strip asserted on a path no test could observe","error_message":"TestOWUIUnwrap_HeaderCarrierPresentButBlank_StrippedAndRejected asserted only the Rejected half of its own name; the Stripped half was unobservable because next never runs on a 401","root_cause":"The strip was applied to a clone of the request, so the only observation point was the downstream handler. Every rejection branch answers without calling next, so nothing on those branches could be checked, and an outer middleware still holding the pre-clone pointer would also have kept seeing a live per-user token. Moving Header.Del into the forwarding branches alone would have left the whole test file green.","fix":"Strip the carrier from the inbound request in place instead of from a clone, making the invariant one fact rather than one fact per branch, and assert in both rejection tests that the header is gone from the request the middleware was handed. Safe because net/http never re-reads request headers after the handler returns and the header is ours alone.","verification":"Green confirmed on unmodified code first. The production strip was then narrowed to fire only for a usable carrier, which is the exact regression described; all four blank sub-cases and the over-long case went red naming the new assertion, and the pass-through case went red too. Restoring the unconditional strip returned ./apps/edge-api/... to green.","tags":["auth","edge-api","owui-unwrap","test-quality","guard-cannot-fail","security","pr-951"]} ``` ## Visual proof: now posted, correcting the section above The section headed "Visual proof: not posted yet" is out of date and its own condition has been met. Images are now visible on this page, posted as permanent release assets through `scripts/post-pr-visual-proof.sh`, from a live signed-in capture against a complete local stack rather than a bundle or a CI artifact. The capture was taken after rebasing onto `main` at `82375d07f`, where #952 and #956 have landed, so it exercises the real post-merge state. Measured in the live DOM at both hops: zero password inputs, zero iframes, zero child browsing contexts, zero anchors to `/agent-workspace`. `GET /api/v1/agent/tasks` answered 200 through the running proxy chain, so the authenticated data path is exercised and not only the rendering. `/agent-workspace` still answers 307 to `/agent-workspace/tasks`, verified after the rebase and deliberately unchanged. Method, substrate and credential handling: `docs/proof/agents-native-live-2026-08-23/README.md`. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>












Wave one of
spec-2026-08-16-hive-ui-redesign(Obsidian vault), which is the fork plus the navigation shell. It answers the three things the owner named: the left navigation carries neither the agent workspace nor the chat interface, the product still looks like Open WebUI, and the agent is reached through a button in the top right corner that opens a separate page.All three were properties of shipping a prebuilt vendor bundle, so this stops doing that.
What lands
The fork.
vendor/open-webuiis upstream atv0.10.2, added as a squashed git subtree.Dockerfile.open-webuigains a first stage that compiles its frontend the way upstream's own Dockerfile does (node:22-alpine3.20,npm ci --force,npm run build) and copies the result over/app/buildin the pinned image. The Python backend, its dependency set and every existing backend patch are untouched, so a digest bump keeps delivering backend fixes for free. The stage asserts the vendoredpackage.jsonversion equals the version inside the pinned image, so a bump without a matching subtree pull fails the build rather than a browser.Measured, cold, no cache:
npm ciabout 90 seconds,npm run buildabout 420 seconds. Warm, with the vendored tree untouched, both layers are cache hits.owui-nightly.ymlboots the stack with--build, so its timeout moves from 30 to 45 minutes;docker-bake.hclgains anopen-webuitarget so the image is one command anywhere.The identity.
packages/hive-tokens/tokens.csscarries the design system (spec-2026-08-11-hive-design-system) as plain CSS custom properties, outside the vendored tree on purpose so it survives a change of chat engine. Open WebUI funnels its entire surface and ink scale through fourteen values in one@themeblock; remapping those to the brand's warm ramp is what turns the product from grey to cream and charcoal in one place rather than a hundred. Lightness is held within a step or two of upstream's own ramp, so no contrast pair inverts. Hanken Grotesk and Geist Mono are served from the deployment, not from a font CDN.The shell.
vendor/open-webui/src/lib/hive/holds every Hive authored piece: the navigation as data, a row component, and the stylesheet. Chats, Agents and Knowledge are labelled destinations, present in both sidebar states, with the current row carrying a coral bar as well asaria-current. Upstream's own files carry four insertion points a rebase can replay, listed indocs/owui-fork.md.The agent inside the shell.
/agentsis a route in the chat application that renders the agent workspace over the same origin, so reaching the agent no longer leaves the product. The injected launcher overlay,loader.js, and the measuredright: 120pxapparatus incustom.cssare deleted with it.Surfaces removed in source rather than in a bundle. The Workspace tabs with no Hive counterpart and their routes, the Playground, the Admin Panel entry, the vendor's documentation, releases, changelog dialogue, social badges and copyright, the Settings Integrations tab, and the vendor links on the backend-required error screen. The accessible names the patch layer used to add are attributes in source now.
Visual proof
docs/proof/shell-nav-2026-08-17/and the comment below. Two images built from the same pinned digest, differing only in this diff, captured in both themes with the DOM read as well as the pixels, the way PR #909 did it. Reproducible with therun.shandcapture.mjscommitted beside the captures.Checked against the design system's own floors, from
dom.jsonrather than by eye: nav row height 32 matching the token, 44 under a coarse pointer, the two ring focus treatment (2px canvas then 2px coral) so the ring stays visible on the accent itself, transition 120ms which is--hv-duration-fast, and 0s underprefers-reduced-motion.What is deliberately not here
The bundle rewrite layer is not retired. It still runs, against the bundle it was written for, and the frontend built from source then replaces that bundle. That reads as waste and it is deliberate for one release. Those rewrites are exact literals over minified output and the identifiers in them are allocated per chunk, so any edit to our own source renames them: the first build of this stage failed on
sidebar-playground-item,changelog-modalandabout-vendor-social-badges, three surfaces that had not actually come back, and an identifier-tolerant pattern for two of them then matched sibling components that must not be touched. Every surface those rewrites remove is removed in source here, so nothing regresses; what they still buy is the digest-drift guard.Retiring them properly is the natural next change and it is out of this one on purpose, because #936 was in flight against
hive_ui_surfaces.pyand two edits to that file from different directions is how a surface silently returns.hive_ui_surfaces.py,apply_ui_surfaces_patch.pyandremove_integrations_tab.pyare byte identical tomainin this branch.Wave two and beyond. The composer mode control, the greeting, the suggestion chips, the settings reduction from nine sections to five, the console group in the same sidebar, Artifacts as a nav row (it needs its host origin injected into the chat frontend, which is a compose change), and everything in section 4 of the spec: the agent as a conversation, activity rows, approval, steering. The agent panel is framed rather than ported, because a native port needs a token bridge: this frontend holds Open WebUI's own session token while
/v1/agent/*authenticates the Supabase bearer that only the embedded application carries.What must not regress
run-owui-e2eto run it/workspace/knowledgeis unchanged and/workspacenow redirects straight to it/v1/agent/*API is untouched. The workspace moves from a corner link to a sidebar row; the application behind it is the same oneSecurity note
apps/agent-console/middleware.tsmoves fromframe-ancestors 'none'toframe-ancestors 'self', andX-Frame-OptionsfromDENYtoSAMEORIGIN, because the chat shell now frames it from the same origin through the same Caddy listener. Everything the previous value defended against, a third-party page framing this one to harvest a click or a session, is still refused.apps/web-consolekeeps'none'.Buglog entry
{"id":"owui-fork-literal-rewrites","date":"2026-08-17","area":"deploy/docker","error_message":"open-webui bundle no longer matches hive_ui_surfaces.py; a removed surface may have come back: sidebar-playground-item, changelog-modal, about-vendor-social-badges","root_cause":"The exact-literal bundle rewrites match on minified identifiers, which the minifier allocates per chunk. Building the frontend from vendored source renames them, so three of nineteen rewrites stopped matching even though every surface was still present. An identifier-tolerant regex is not a fix: for changelog-modal and about-vendor-social-badges it matched sibling components with identical compiled shape, which would have neutered Settings or the admin dialogue.","fix":"Remove those surfaces in the vendored source instead, and order the Dockerfile so the rewrite layer still runs against the upstream bundle before the source build replaces it. Retiring the layer entirely is the follow-up.","tags":["owui","fork","bundle-patch","minifier","docker"]}