Skip to content

feat: build the chat frontend from a forked source tree and give it one Hive navigation - #938

Merged
sakibsadmanshajib merged 7 commits into
mainfrom
feat/owui-fork-shell-nav
Aug 17, 2026
Merged

sakibsadmanshajib merged 7 commits into
mainfrom
feat/owui-fork-shell-nav

Conversation

@sakibsadmanshajib

Copy link
Copy Markdown
Owner

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-webui is upstream at v0.10.2, added as a squashed git subtree. Dockerfile.open-webui gains 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/build in 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 vendored package.json version 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 ci about 90 seconds, npm run build about 420 seconds. Warm, with the vendored tree untouched, both layers are cache hits. owui-nightly.yml boots the stack with --build, so its timeout moves from 30 to 45 minutes; docker-bake.hcl gains an open-webui target so the image is one command anywhere.

The identity. packages/hive-tokens/tokens.css carries 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 @theme block; 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 as aria-current. Upstream's own files carry four insertion points a rebase can replay, listed in docs/owui-fork.md.

The agent inside the shell. /agents is 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 measured right: 120px apparatus in custom.css are 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 the run.sh and capture.mjs committed beside the captures.

Checked against the design system's own floors, from dom.json rather 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 under prefers-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-modal and about-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.py and two edits to that file from different directions is how a surface silently returns. hive_ui_surfaces.py, apply_ui_surfaces_patch.py and remove_integrations_tab.py are byte identical to main in 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

Capability Status
Chat The transcript, composer and model picker are upstream's, unchanged in behaviour. The nightly Open WebUI e2e suite is the gate; label this PR run-owui-e2e to run it
Embeddings Untouched. No API surface changes here
Voice to text Untouched. The microphone is upstream's own control
Knowledge work Knowledge is promoted to a top level row; /workspace/knowledge is unchanged and /workspace now redirects straight to it
Cowork and the coding agent The /v1/agent/* API is untouched. The workspace moves from a corner link to a sidebar row; the application behind it is the same one

Security note

apps/agent-console/middleware.ts moves from frame-ancestors 'none' to frame-ancestors 'self', and X-Frame-Options from DENY to SAMEORIGIN, 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-console keeps '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"]}

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.
@sakibsadmanshajib

sakibsadmanshajib commented Aug 17, 2026 •

Copy link
Copy Markdown
Owner Author

Visual proof

Two 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 docs/proof/shell-nav-2026-08-17/run.sh, which needs no credential: both containers run with WEBUI_AUTH=False and the agent workspace is built against placeholder public values.

The shell as a user meets it, light

main, with the agent behind a corner link and no agent or chat destination in the navigation:

main: an icon rail, no agent or chat destination, and the agent behind a corner link

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:

this branch: labelled Chats, Agents and Knowledge, no corner link, cream and charcoal

The same pair, dark

main, dark

this branch, dark

The agent, reached from the sidebar rather than from a corner

The sidebar is still there, Agents is the current row, and the panel fills the region in the shell's own theme:

the agent workspace inside the shell, dark, with Agents as the current row

the same in light

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 focused

Every 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:

the collapsed rail, every destination still present

keyboard focus on the Agents row, two ring treatment

What the DOM says, from the same runs

Read from docs/proof/shell-nav-2026-08-17/dom.json, not from the pixels.

Claim Evidence
The floating launcher is on main and gone here before-light.launcherCount 1, after-light.launcherCount 0
Three labelled destinations, with real hrefs Chats /, Agents /agents, Knowledge /workspace/knowledge
Current destination is not colour alone aria-current="page" moves from Chats to Agents between / and /agents
Rows are the size the token specifies height 32 expanded, 36 square on the rail, 44 under a coarse pointer
The brand face actually loads hankenLoaded false on main, true here
The panel is same origin, embedded, themed sameOrigin true, embeddedFlag 1, themeFlag dark
Motion follows the catalogue row transition 0.12s, and 0s under prefers-reduced-motion
Focus ring survives on any surface box-shadow: 0 0 0 2px canvas, 0 0 0 4px coral

The authenticated panel with a real task list in it is captured against the demo box once this merges and deploys.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026 •

Copy link
Copy Markdown

Caution

Review failed

An 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

github-actions Bot added a commit that referenced this pull request Aug 17, 2026
github-actions Bot added a commit that referenced this pull request Aug 17, 2026
github-actions Bot added a commit that referenced this pull request Aug 17, 2026
@github-actions

Copy link
Copy Markdown

Cowork visual proof, captured in CI

Captured against a stack booted from refs/pull/938/merge on a hosted
runner, with a real Apptainer sandbox behind the socket arm of the agent
engine, in run 32038737735.

launch-liveness-01-empty-console

launch-liveness-01-empty-console

launch-liveness-02-sandbox-launched

launch-liveness-02-sandbox-launched

Run log (screenshot stamps carry no URL, and the log is redacted and linted by `lint:proof-tokens`)
scenarios: launch-liveness
launcher runtime dir: /mnt/agent-runtime, live sessions at start: 0
signed in as user 942aa7e4-9f41-4552-88a3-55e2cd5e5aeb
token claims: aal,amr,app_metadata,aud,email,exp,iat,is_anonymous,iss,owui_role,phone,role,session_id,sub,tenant_id,tenants,user_metadata
token tenant claim: 4e16ddd1-8f3b-7d28-353b-a30e308c4e55 · role=OWNER · aal=aal1
--- scenario: launch-liveness ---
wrote /home/runner/work/hive/hive/docs/proof/agent-visual-proof/run-32038737735/launch-liveness-01-empty-console.png
create answered HTTP 201 in 536ms, status=queued
wrote /home/runner/work/hive/hive/docs/proof/agent-visual-proof/run-32038737735/launch-liveness-02-sandbox-launched.png
row "proof-32038737735 liveness: list the fil…" reached Cancelled
scenario launch-liveness: ok

…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.
@sakibsadmanshajib
sakibsadmanshajib marked this pull request as ready for review August 17, 2026 14:38
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown

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.
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Adversarial review

Streams that ran, and streams that did not. An absent stream is not a pass.

Stream Result
Plain adversarial pass, no specialised tool RAN, below
Design checklists (emil-design-eng, frontend-ui-engineering, frontend-design, ui-ux-pro-max) RAN AS CHECKLISTS, NOT AS SKILLS. This agent has no Skill or SlashCommand tool, so none of the four could be invoked. Their rule sets were read from their own sources on disk and applied by hand. Reported as a capability gap rather than worked around
CodeRabbit CLI SKIPPED. coderabbit review --agent --committed --base main failed twice with Connection failed: WebSocket closed (TRPCWebSocketClosedError), once in full mode and once with --light. Not a clean pass, an unavailable stream
ecc:code-review, adversarial-pr-review, /codex:adversarial-review SKIPPED, same capability gap: they are Skill and slash-command entry points and this agent has neither tool
CI Two failures on this head are GitHub infrastructure, not this diff: Go tests (storage) failed in Set up job with Failed to download archive 'actions/setup-go' ... 429 (Too Many Requests) after three retries, and the API is intermittently answering 503. Re-run when GitHub is healthy

Findings on this diff, then findings on the product it lands in. The second set is written up in full at plan-2026-08-17-ui-ux-flow-audit in the vault.

Found and fixed before this comment

Finding Fix
git subtree add writes a tree directly and bypassed /vendor/* in .gitignore, so every new file under the vendored tree was silently unstageable. The entire src/lib/hive/ shell, the /agents route and the three font files were absent from the first pushed commit while the image still built green, because the build reads the working tree and not the index .gitignore re-includes /vendor/open-webui/ explicitly, and the pushed tree was rebuilt from git archive of the exact commit rather than from the working tree, which is the only way that class of defect surfaces
The demo box deploy job recreates the stack with --build under a 15 minute ceiling, and the chat image now compiles a frontend first. A deploy that trips its own timeout mid recreate is the worst outcome available Ceiling raised to 30 with the measured reason, and the stale-lock sweep documented to match it moved in the same step
The panel was hidden until its load event fired, so a load event that never arrives would leave a permanent "Opening the agent workspace" over a working panel The line sits behind the frame instead. An unpainted frame is transparent; the panel's own ground covers the line the moment it paints
Nav rows had no press feedback transform: scale(0.98) on :active at 100ms, removed under prefers-reduced-motion

Open on this diff, accepted with reasons

Finding Why it is accepted here
The bundle rewrite layer still runs and its output is then discarded Measured: those literals match on minified identifiers, so any source edit renames them and three of nineteen stop matching surfaces that have not come back. Retiring the layer is the follow-up, kept out because #936 was in flight against hive_ui_surfaces.py. Those three files are byte identical to main here
Chats and New Chat both target / Chats is the label that makes chat a named destination; the list of conversations is directly beneath it. A real Chats destination with an archived filter is wave two of the spec
Changing the theme remounts the agent panel {#key} on the resolved theme, so in-frame state is lost. A theme change is rare and deliberate; the alternative is writing into another document from the parent
The palette remap touches every surface in the application Verified in both themes on the chat home, the agents route and the collapsed rail. Not verified on every modal, the settings dialogue or the knowledge screens. That pass is named as wave seven in the vault plan rather than claimed here
The embedded panel shows the agent console's own sign-in That is today's behaviour and it is the third login. It is the first item in the flow plan

Design checklist findings, Emil format

Two of these are ours and are fixed above; the rest are inherited chrome this change does not touch, and they are wave seven.

Before After Why
No :active on a nav row transform: scale(0.98), 100ms Fixed here. A control that does not respond to a press feels dead
Panel hidden until load Loading line behind the frame Fixed here. A hidden panel plus a permanent line is the failure that looks like a hang
transition-all duration-300 on the sidebar container (Sidebar.svelte) transition: width var(--hv-duration-base) var(--hv-ease-out) Inherited. all animates layout properties, and 300ms is past the catalogue's 260ms ceiling
transition:slide 250ms on sidebar open The width transition alone Inherited. Two motions describing one act
Upstream header buttons carry focus:outline-hidden The two ring focus treatment from the token file Inherited, and it is the accessibility floor rather than a style preference
Stock suggestion chips, "Tell me a fun fact about the Roman Empire" Four Hive chips Inherited. In a product sold to regulated buyers this is the clearest signal nobody has been through the screen. Wave 0 of the spec, needs no fork
Greeting prints "Hello, User" The person's name and one line naming what the surface does Inherited, wave 0
Model selector as a page title with "Set as default" under it Model selector in the composer rail A model is a property of the turn, not of the page. Wave 0 and 2

Security

apps/agent-console/middleware.ts moves from frame-ancestors 'none' to 'self' and X-Frame-Options from DENY to SAMEORIGIN, because the shell frames it from the same origin behind the same listener. A third-party page still cannot frame it. Two tests cover it, plus two more asserting the embed and theme parameters survive the sign-in redirect and that an unknown theme value is dropped rather than reflected into a redirect target. apps/web-console keeps 'none'.

Not merging

GitHub is degraded (429 on action downloads, intermittent 503 on the API), so this stays open. The two red checks on this head are that outage, not this diff.

Comment thread .github/workflows/deploy-demo-box.yml
Comment thread vendor/open-webui/package.json
Comment thread deploy/docker/Dockerfile.open-webui
Comment thread docs/owui-fork.md Outdated
Comment thread .gitignore
Comment thread vendor/open-webui/src/lib/components/chat/Settings/About.svelte
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Security and supply-chain review, PR #938 (vendor/open-webui fork)

Scope was the six risks named in the review brief: provenance, malicious content, license integrity, build integrity, the .gitignore bug, and the deploy-workflow timeout change. Detailed findings on items 3, 4, 5 and 6 are posted inline. Summary of everything, including the items that came back clean:

1. Provenance: verified clean, independently. The squash commit's trailer records git-subtree-split: ecd48e2f718220a6400ecf49eafd4867a38feb10. I shallow-fetched that exact commit from github.com/open-webui/open-webui directly and confirmed two things myself, not from the PR's own claims: that commit is refs/tags/v0.10.2 upstream, and the root tree hash of the subtree-add commit in this repo is byte-for-byte identical to that tag's tree. The squash destroyed per-file history as expected, but the subtree-split trailer is exactly the re-derivation record the brief asked whether one exists, and it holds up. docs/owui-fork.md also documents the tag and the git subtree pull command for future updates.

2. Malicious or surprising content: clean. No preinstall/postinstall/prepare lifecycle scripts in the vendored package.json (17 scripts total, none of those three). .npmrc is one line, engine-strict=true, identical to upstream. No .patch files anywhere in the vendored tree. One WASM binary (static/sql.js/sql-wasm.wasm, upstream's own client-side SQLite); hash matches upstream exactly. No curl/wget/network calls in any vendored build script beyond upstream's own pre-existing backend/start.sh localhost health check, which this PR does not touch or execute.

3. License integrity: see inline comment on About.svelte. LICENSE, LICENSE_HISTORY, LICENSE_NOTICE and CONTRIBUTOR_LICENSE_AGREEMENT are present and byte-for-byte identical to upstream, confirmed by hash. What was removed is UI-rendered text (a copyright line, an author-attribution line, three vendor badges) from the Settings > About screen, not a license file. Flagged as a documentation-parity suggestion, not a violation: this removal likely rests on the same LICENSE clause 4 branding carve-out (under 50 end users) the Dockerfile already cites explicitly for the WEBUI_NAME suffix, but it does not carry the same explicit citation or the same "re-evaluate before scaling past 50 users" reminder that the Dockerfile gives its own removal.

4. Build integrity: verified correct, inline comment on Dockerfile.open-webui. The exact-literal bundle rewrites run against the pinned image's stock bundle, then that bundle is discarded and replaced by the Hive source build last, matching the ordering the PR body and buglog entry claim. Backend patches are untouched. The frontend/backend version-match assertion is present and fails the build on mismatch.

5. .gitignore bug: verified fixed. Confirmed directly against the pushed head commit (cf79a0956), not against the working tree: vendor/open-webui/src/lib/hive/ (all 5 files), the /agents route, and all three brand .woff2 fonts are present in the final tree. This was the single highest-value check per the brief, and it holds.

6. Deploy workflow timeout: verified, no race. timeout-minutes and the stale-lock sweep's -mmin threshold both moved from 15 to 30 together, keeping them equal. That is the correct invariant (sweep threshold must never be shorter than the job's own worst-case runtime); this PR preserves it rather than letting it drift.

Bonus finding, outside the six listed risks but in scope for a fork this size: ran npm audit --audit-level=high against the vendored package-lock.json. 29 advisories total: 1 critical, 15 high, 12 moderate, 1 low. The critical and most of the high-severity cluster are in vitest/vite/cypress, which are devDependencies not shipped in the production build. Four direct runtime dependencies are worth triage regardless: xlsx (high, prototype pollution and ReDoS, no fix available upstream), undici (high, several advisories, fix available), dompurify (moderate, XSS-relevant, fix available), and @huggingface/transformers/kokoro-js pulling a vulnerable sharp transitively (high, no fix available). Also confirmed: no .github/dependabot.yml exists anywhere in this repo (a pre-existing gap, not introduced here), despite docs/owui-fork.md stating the vendored tree now belongs in Dependabot's scope. Worth wiring up given this PR roughly triples the repo's third-party JS surface in one commit.

Not run: eslint-plugin-security static scan was not run against the vendored tree; not requested in the review brief and 4,986 files makes a full lint pass a separate, larger task. Flagging as SKIPPED rather than implying a clean pass on that specific tool.

No CRITICAL or HIGH finding blocks this PR on the six requested risks. The npm audit results are a follow-up item, not a block on this PR's own changes (the vulnerable packages are upstream's existing dependency choices, inherited by the fork decision itself, not introduced by this diff).

…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.
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Response to the security and supply-chain review

Thank you for verifying provenance against upstream directly rather than against this PR's own claims, and for checking the .gitignore fix on the pushed commit rather than on a working tree. The second one is the exact reading that would have caught the defect in the first place.

Dispositions, one per finding. Two produced code, four were verifications with nothing to fix, one is out of scope.

Finding Disposition
1. Provenance Verified clean. No action
2. Malicious or surprising content Verified clean. No action
3. License integrity Out of scope for this PR by owner decision. Settled in .wolf/decisions.md D-041, which landed on main after this branch was cut, which is why it was not visible to you. No citation added, no tracking issue opened. The non-licensing half of your finding stands and is appreciated: the four licence files are byte identical to upstream and only rendered interface text changed
4. Build integrity Verified correct. It also surfaced a real documentation defect, below
5. .gitignore bug Verified fixed
6. Deploy workflow timeout Verified, invariant preserved
Bonus: npm audit Acted on. Triage written, no upgrades

The two that produced work

docs/owui-fork.md stated the patch ordering backwards. Corrected in 909542b93. The rewrites run against the stock bundle inside the pinned upstream image; rm -rf /app/build plus COPY --from=frontend replace that bundle afterward; the rewrites never see the Hive built output. The section now also says what that means for retiring the layer, which is the thing the wrong wording would have cost: those rewrites protect a drift check on upstream's own bundle, not the bundle we ship, because every surface they remove is removed again independently in the vendored source and that source removal is the one a user meets.

Advisory triage, written down so it is not redone. Same commit. The critical advisory and most of the high cluster are devDependencies that never reach the emitted bundle. The four direct dependencies were answered by reachability read in this tree:

  • xlsx ships and is reachable, via a dynamic import in src/lib/utils/excelToTable.ts that parses a spreadsheet the user uploaded, with no fix available upstream. This is the one that needs a decision rather than a bump, and it is named as such.
  • dompurify ships and is reachable, and it is the sanitiser the application trusts. Fix available; it wants its own change with the XSS-relevant call sites exercised.
  • undici does not ship. Its only importer is scripts/prepare-pyodide.js, a build time Node script.
  • sharp does not ship. Verified against the built image: /app/build contains no import of it.

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 .github/dependabot.yml exists in this repository, that the gap predates the fork, and that wiring it at vendor/open-webui is the follow-up.

Noted as skipped

eslint-plugin-security was not run. Recorded rather than treated as a clean pass, same posture this PR takes for the CodeRabbit CLI stream that failed to connect.

github-actions Bot added a commit that referenced this pull request Aug 17, 2026
github-actions Bot added a commit that referenced this pull request Aug 17, 2026
github-actions Bot added a commit that referenced this pull request Aug 17, 2026
@github-actions

Copy link
Copy Markdown

Cowork visual proof, captured in CI

Captured against a stack booted from refs/pull/938/merge on a hosted
runner, with a real Apptainer sandbox behind the socket arm of the agent
engine, in run 32042972088.

launch-liveness-01-empty-console

launch-liveness-01-empty-console

launch-liveness-02-sandbox-launched

launch-liveness-02-sandbox-launched

Run log (screenshot stamps carry no URL, and the log is redacted and linted by `lint:proof-tokens`)
scenarios: launch-liveness
launcher runtime dir: /mnt/agent-runtime, live sessions at start: 0
signed in as user a5dea318-6724-4ecd-8162-31224151a417
token claims: aal,amr,app_metadata,aud,email,exp,iat,is_anonymous,iss,owui_role,phone,role,session_id,sub,tenant_id,tenants,user_metadata
token tenant claim: d3738227-904d-42f9-3ae4-ad1782059acf · role=OWNER · aal=aal1
--- scenario: launch-liveness ---
wrote /home/runner/work/hive/hive/docs/proof/agent-visual-proof/run-32042972088/launch-liveness-01-empty-console.png
create answered HTTP 201 in 719ms, status=queued
wrote /home/runner/work/hive/hive/docs/proof/agent-visual-proof/run-32042972088/launch-liveness-02-sandbox-launched.png
row "proof-32042972088 liveness: list the fil…" reached Cancelled
scenario launch-liveness: ok

@sakibsadmanshajib
sakibsadmanshajib merged commit 1dc67ee into main Aug 17, 2026
32 of 33 checks passed
@github-actions
github-actions Bot deleted the feat/owui-fork-shell-nav branch August 17, 2026 16:56
sakibsadmanshajib added a commit that referenced this pull request Aug 18, 2026
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.
sakibsadmanshajib added a commit that referenced this pull request Aug 18, 2026
## 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)
sakibsadmanshajib added a commit that referenced this pull request Aug 18, 2026
…#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"]}
```
sakibsadmanshajib added a commit that referenced this pull request Aug 22, 2026
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.
sakibsadmanshajib added a commit that referenced this pull request Aug 22, 2026
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.
sakibsadmanshajib added a commit that referenced this pull request Aug 23, 2026
…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>
sakibsadmanshajib added a commit that referenced this pull request Aug 23, 2026
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.
sakibsadmanshajib added a commit that referenced this pull request Aug 23, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant