Skip to content

fix: remove dead Admin Panel entry, stop coverage suites littering the demo account (#846, #848) - #909

Merged
sakibsadmanshajib merged 4 commits into
mainfrom
fix/owui-demo-surface-litter-846-848
Aug 16, 2026
Merged

sakibsadmanshajib merged 4 commits into
mainfrom
fix/owui-demo-surface-litter-846-848

Conversation

@sakibsadmanshajib

Copy link
Copy Markdown
Owner

Summary

Two audience-visible demo-surface defects, two commits.

#846, dead Admin Panel entry. The chat user menu's "Admin Panel" item
navigates to /admin, which Caddyfile.owui already 404s outright:
.wolf/decisions.md D-014 makes web-console/control-plane the sole admin
surface and keeps Open WebUI's own admin panel off. The entry was the one
thing in deploy/docker/owui-patches/hive_ui_surfaces.py kept alive on
purpose (a GUARDS assertion), on the theory that Hive's tenant-owner
promotion made it a real control. It never was. Decision: remove, not
wire up.
Wiring the endpoint would stand up a second admin surface next
to web-console's, which is the exact duplication D-014 forecloses. Same
removal shape as the neighboring Playground entry: an unconditional !1
gate, not a permission narrowing (every tenant owner already passes
role === "admin", so a permission-scoped hide would hide nothing).

/api/v1/terminals/ 404ing on every page load is a different class, not a
bug: Caddyfile.owui already documents it as a deliberate block (#770, an
authenticated request-forgery primitive with no allowlist), and the front
end already degrades to an empty terminal list on any non-2xx. Left alone.

#848, demo-account test litter. Root cause: two currently-merged live
coverage paths silently defaulted their sign-in identity to
demo@hive-demo.invalid, the account the owner demos to prospects.

  • apps/web-console/tests/e2e/_probe/agent-workspace-flows.spec.ts
    defaulted to the demo account whenever HIVE_QA_AGENT_EMAIL was unset.
    Every run past that default creates one real, currently undeletable
    agent task (no per-task delete route exists yet), which is why the task
    list is a wall of interaction-coverage proof ... rows stuck Cancelled
    or Blocked.
  • .github/workflows/chat-coverage.yml fell back to
    secrets.HIVE_QA_TESTER_EMAIL whenever secrets.HIVE_QA_AGENT_EMAIL
    was unset, which it always was (no such secret exists on this repo).
    The sweep's own ledger proves the result:
    "demo@hive-demo.invalid -> hive-coverage-77811 survived a reload"
    (docs/proof/chat-interaction-coverage-2026-08-10/coverage.run.json).

Both now fail loudly instead of defaulting (the same requireMintEnv
pattern the first suite already used for its Supabase mint variables, and
the loud "inputs the sweep cannot run without" check the workflow already
had). docs/live-test-auth.md gets a new section stating the rule for the
next suite: read-only checks may use the demo account; anything that sends
a message, submits a task, or mints a key must use a dedicated identity,
the same way the plain E2E suite already isolates itself with
E2E_RUN_KEY-scoped fixture accounts.

Not fixed here, flagged instead of silently skipped: neither change
provisions the dedicated identity itself. HIVE_QA_AGENT_EMAIL has no
value to fail over to; standing one up needs live Supabase admin
credentials this worktree does not hold. Both suites now fail until that
identity is provisioned and the secret is set, which is the intended
consequence, not a regression: a workflow that used to succeed quietly
against the wrong account now requires the right one first.

Existing litter: enumerated, not deleted

Nothing is deleted in this PR. Per standing instruction, deletion needs an
explicit owner go-ahead after review of what's actually on the account.

Known rows, from issue #848, issue #858's 2026-08-11 demo-readiness walk,
and today's (2026-08-16) live reconfirmation:

On the 25 New Chat rows specifically: I cannot independently
re-enumerate the live account from this worktree (no browser tool, no
Supabase/DB access available to this agent), so this is a proposed
classification, not an executed one. Proposed criterion before any delete:
pull each New Chat row's created_at and first message. A row whose
first message is a synthetic literal already used by a coverage suite
(the Reply with exactly: <WORD> / ping family, or any string a suite
sends deterministically) is automation litter regardless of its stuck
title; OWUI's title generation is a second, async LLM call after the first
response, and a suite that asserts the message round-trip and moves on
without waiting for or asserting a title is exactly the shape that leaves
a chat stuck on New Chat forever, so I'd expect most or all of the 25 to
be this, not failed title-gen on a real conversation. A row whose first
message is not a known synthetic literal, or whose created_at does not
cluster with a known CI/coverage run, should be treated as a real
conversation with a failed title generation and left alone. I'm not able
to run that classification against the live rows myself; whoever has
live account access should run it before anything is deleted, and I'd
want to see the actual per-row list before endorsing any deletion set.

Also worth flagging, not part of this PR's diff: the in-flight, unmerged
test/interaction-coverage-gate branch (issue #885, PR #811) is a newer
console-wide interaction-coverage framework that per #885 has already
moved toward blocking mutating requests. The interaction gate probe API
keys and the interaction-gate@example.invalid spend alert most likely
predate that safeguard. Whoever owns that PR should confirm it also never
authenticates as the demo account once merged, same rule as this PR.

Visual proof

Not captured. This agent's toolset (Read/Write/Edit/Bash, no browser or
screenshot tool) cannot drive the running stack to capture the user-menu
before/after state orchestrator rule 8 requires before merge. Flagging
this as a structural capability gap rather than skipping it silently: a
Playwright-capable agent needs to capture the corrected user menu (Admin
Panel entry gone) against a real deployment before this merges.

Buglog entry

{"date":"2026-08-16","tags":["owui","chat","cowork","test-litter","demo-account","ci"],"error_message":"Demo account (demo@hive-demo.invalid) accumulates permanent chat, agent-task and API-key rows from live coverage suites; user menu also offered a dead Admin Panel entry (404 on every path)","root_cause":"Two live-coverage code paths silently defaulted their sign-in identity to the shared demo account: agent-workspace-flows.spec.ts's AGENT_EMAIL fallback and chat-coverage.yml's HIVE_QA_AGENT_EMAIL-to-HIVE_QA_TESTER_EMAIL secret fallback (which resolved to the demo identity in practice, per its own coverage ledger). Separately, hive_ui_surfaces.py deliberately kept the OWUI Admin Panel user-menu entry alive via a GUARDS assertion, while Caddyfile.owui already 404s every path it points at (D-014: web-console/control-plane is the sole admin surface).","fix":"Removed the Admin Panel entry from hive_ui_surfaces.py (moved from GUARDS to REWRITES, same !1 gate as the neighboring Playground entry). Changed both silent demo-account fallbacks to hard failures (agent-workspace-flows.spec.ts's requireMintEnv, chat-coverage.yml's HIVE_QA_AGENT_EMAIL secret) so a live coverage run can no longer land on the demo account by omission. Existing litter enumerated for owner review, not deleted; a dedicated non-demo QA identity still needs to be provisioned and wired to HIVE_QA_AGENT_EMAIL as a follow-up.","tags":["owui","chat","cowork","test-litter","demo-account","ci"]}

Test plan

  • python3 scripts/test_owui_ui_surfaces.py — pass
  • python3 scripts/test_caddy_owui_blocklist.py — pass (unchanged, confirms terminals/admin blocks untouched)
  • python3 scripts/test_owui_rag_env_config.py — pass (unrelated suite, sanity check for the same patch package)
  • python3 -c "import yaml; yaml.safe_load(open('.github/workflows/chat-coverage.yml'))" — valid YAML
  • TypeScript typecheck of agent-workspace-flows.spec.ts (docker compose run --build web-console npm run build) — not run in this worktree, no node_modules/Docker invoked here; the diff is a mechanical mirror of the already-passing requireMintEnv pattern in the same file, manually reviewed instead
  • Live screenshot of the corrected user menu — blocked on browser-tool availability, see Visual proof above
  • Live re-run of chat-coverage.yml / agent-workspace-flows.spec.ts against a provisioned HIVE_QA_AGENT_EMAIL — blocked on that identity not existing yet

The user menu's Admin Panel item navigated to /admin, which
Caddyfile.owui already 404s outright: D-014 makes web-console and
control-plane the sole admin surface and keeps Open WebUI's own admin
panel off. The entry was the one thing in hive_ui_surfaces.py kept
alive on purpose (asserted intact by a GUARDS entry), on the theory
that Hive's tenant-owner promotion made it a real control. It never
was: every tenant owner sees a top-level Admin Panel item that always
404s with no explanation, reconfirmed live in the 2026-08-11
demo-readiness walk (#858).

Wiring the endpoint up instead would stand up a second admin surface
next to web-console's, which is the exact duplication D-014
forecloses, so this removes the entry with the same unconditional
!1 gate already used for the neighboring Playground entry, moves its
pinned-bundle fixture from guards to rewrites, and updates the
self-check that pins each rewrite's marker.

/api/v1/terminals/ 404ing on every page load is not the same class of
bug: Caddyfile.owui already documents it as a deliberate block (#770,
an authenticated request-forgery primitive with no allowlist), and the
front end already degrades cleanly to an empty terminal list. Left
alone.
Every one of these suites signs in through the sanctioned magic-link
helper (docs/live-test-auth.md), so no password is ever read or
rotated, but the identity behind that session still matters: it is the
account that owns whatever chat, task or key the suite creates while
proving a control. Two currently-merged paths silently resolved that
identity to demo@hive-demo.invalid, the account the owner demos to
prospects.

- apps/web-console/tests/e2e/_probe/agent-workspace-flows.spec.ts
  defaulted AGENT_EMAIL to demo@hive-demo.invalid whenever
  HIVE_QA_AGENT_EMAIL was unset, "to keep the suite runnable by hand
  with no setup." Every run past that default creates one real,
  currently undeletable agent task (no per-task delete route exists
  yet), which is why the account's task list is now a wall of
  interaction-coverage and cancel-guard proof rows stuck Cancelled or
  Blocked. Now fails hard in the same requireMintEnv check the suite
  already uses for its Supabase mint variables, instead of defaulting.

- .github/workflows/chat-coverage.yml fell back to
  secrets.HIVE_QA_TESTER_EMAIL whenever secrets.HIVE_QA_AGENT_EMAIL was
  unset, which it always was: no such secret exists on this
  repository, so every live sweep ran that fallback. The sweep's own
  ledger records the result: "demo@hive-demo.invalid ->
  hive-coverage-77811 survived a reload"
  (docs/proof/chat-interaction-coverage-2026-08-10/coverage.run.json).
  The workflow already has a loud "inputs the sweep cannot run
  without" check for an empty HIVE_QA_AGENT_EMAIL; removing the silent
  fallback is what lets that check actually catch this instead of
  quietly succeeding against the wrong account.

docs/live-test-auth.md gets a new section stating the rule plainly for
the next suite: read-only live checks may use the demo account, same
as scripts/verify-control-plane.py; anything that sends a message,
submits a task, or mints a key must use a dedicated identity instead,
the same way the plain E2E suite already isolates itself with
E2E_RUN_KEY-scoped fixture accounts, or the way
seed-owui-e2e-user.py --account-slug stands up its own billing
account rather than sharing one.

Not fixed here, and flagged for the owner rather than silently
skipped: neither change provisions the dedicated identity itself
(HIVE_QA_AGENT_EMAIL has no value to fail over to), since that needs a
live account created with real Supabase admin credentials this
worktree does not hold. Both suites now fail loudly until that
identity exists and the secret is set, which is the intended
consequence: a workflow that used to succeed quietly against the wrong
account now requires the right one to be provisioned first.

The existing litter itself (chats, tasks, API keys, a spend alert on
the demo account) is not touched by this commit. It is enumerated for
the owner in the PR body; nothing is deleted without an explicit go.

Buglog entry (for the follow-up buglog-only PR against main, per
.claude/rules/openwolf.md):
{"date":"2026-08-16","tags":["owui","chat","cowork","test-litter","demo-account","ci"],"error_message":"Demo account (demo@hive-demo.invalid) accumulates permanent chat, agent-task and API-key rows from live coverage suites; user menu also offered a dead Admin Panel entry (404 on every path)","root_cause":"Two live-coverage code paths silently defaulted their sign-in identity to the shared demo account: agent-workspace-flows.spec.ts's AGENT_EMAIL fallback and chat-coverage.yml's HIVE_QA_AGENT_EMAIL-to-HIVE_QA_TESTER_EMAIL secret fallback (which resolved to the demo identity in practice, per its own coverage ledger). Separately, hive_ui_surfaces.py deliberately kept the OWUI Admin Panel user-menu entry alive via a GUARDS assertion, while Caddyfile.owui already 404s every path it points at (D-014: web-console/control-plane is the sole admin surface).","fix":"Removed the Admin Panel entry from hive_ui_surfaces.py (moved from GUARDS to REWRITES, same !1 gate as the neighboring Playground entry). Changed both silent demo-account fallbacks to hard failures (agent-workspace-flows.spec.ts's requireMintEnv, chat-coverage.yml's HIVE_QA_AGENT_EMAIL secret) so a live coverage run can no longer land on the demo account by omission. Existing litter enumerated for owner review, not deleted; a dedicated non-demo QA identity still needs to be provisioned and wired to HIVE_QA_AGENT_EMAIL as a follow-up.","tags":["owui","chat","cowork","test-litter","demo-account","ci"]}
@coderabbitai

coderabbitai Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@sakibsadmanshajib, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f3ee58e2-c019-4f73-9503-9f99e2a7402a

📥 Commits

Reviewing files that changed from the base of the PR and between bd25ed7 and d1f5332.

⛔ Files ignored due to path filters (10)
  • docs/proof/pr-909-demo-surface-2026-08-16/live-1-chat-sidebar-empty.png is excluded by !**/*.png
  • docs/proof/pr-909-demo-surface-2026-08-16/live-2-chat-usermenu-deployed-main.png is excluded by !**/*.png
  • docs/proof/pr-909-demo-surface-2026-08-16/live-3-console-api-keys.png is excluded by !**/*.png
  • docs/proof/pr-909-demo-surface-2026-08-16/live-4-console-spend-alerts.png is excluded by !**/*.png
  • docs/proof/pr-909-demo-surface-2026-08-16/live-console.log is excluded by !**/*.log
  • docs/proof/pr-909-demo-surface-2026-08-16/live.log is excluded by !**/*.log
  • docs/proof/pr-909-demo-surface-2026-08-16/usermenu-main-image.log is excluded by !**/*.log
  • docs/proof/pr-909-demo-surface-2026-08-16/usermenu-main-image.png is excluded by !**/*.png
  • docs/proof/pr-909-demo-surface-2026-08-16/usermenu-pr909-image.log is excluded by !**/*.log
  • docs/proof/pr-909-demo-surface-2026-08-16/usermenu-pr909-image.png is excluded by !**/*.png
📒 Files selected for processing (10)
  • .github/workflows/chat-coverage.yml
  • apps/web-console/tests/e2e/_probe/agent-workspace-flows.spec.ts
  • deploy/docker/owui-patches/apply_ui_surfaces_patch.py
  • deploy/docker/owui-patches/hive_ui_surfaces.py
  • deploy/docker/owui-patches/pinned-bundle-excerpts.json
  • docs/live-test-auth.md
  • docs/proof/pr-909-demo-surface-2026-08-16/README.md
  • scripts/test_owui_ui_surfaces.py
  • scripts/test_verify_control_plane.py
  • scripts/verify-control-plane.py

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.

Comment thread docs/live-test-auth.md Outdated
Comment thread deploy/docker/owui-patches/hive_ui_surfaces.py
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Adversarial review (stage 6), pipeline mode, single pass since the builder had no agent-dispatch tool.

Verified against the pushed diff and the surrounding untouched files, not just the PR body's claims.

Fail-loud change (#848), risk of a permanently-red check nobody looks at: does not apply here. chat-coverage.yml's self-check job (runs on every PR) never touches HIVE_QA_AGENT_EMAIL at all; its live-sweep job only runs on workflow_dispatch or a PR explicitly labelled run-chat-coverage, never automatically. agent-workspace-flows.spec.ts lives under tests/e2e/_probe, which ci.yml excludes from its test glob, and no workflow in .github/workflows/ references the file at all. So the new hard failure only fires when someone deliberately triggers one of these two paths, not on an automatic, recurring, ignorable schedule. Confirmed no cry-wolf risk.

Admin Panel removal, cosmetic vs real: real, and correctly scoped. /admin was already 404ing via Caddyfile.owui's @blocked regex before this PR (D-014, issues #769/#772), independent of the client-bundle rewrite. This PR only removes the menu item that pointed at that dead route; it does not change, and does not need to change, the route's own reachability, which was never open. No gap introduced.

Third silent-default identity path: found one, posted inline on docs/live-test-auth.md. scripts/verify-control-plane.py defaults HIVE_VERIFY_EMAIL to demo@hive-demo.invalid and its default run mints a real API key plus a real chat completion against the demo tenant, the same litter class as the two paths this PR fixes. This PR's own new doc section cites that script as a compliant, read-only example, which its actual behaviour contradicts.

Pinned-bundle fixture drift: verified robust. test_the_excerpt_fixture_is_from_the_digest_the_dockerfile_pins fails the fixture the moment Dockerfile.open-webui's pinned digest moves, and apply_ui_surfaces_patch.py independently re-verifies every rewrite against the real built bundle at image-build time, so a stale fixture cannot silently certify a broken rewrite. The relocated usermenu-admin-panel excerpt is byte-identical to the one already pinned and verified when it was a guard. One stale docstring sentence in the untouched apply_ui_surfaces_patch.py posted inline as LOW.

Terminals claim (#770): verified true and pre-existing, untouched by this PR. Caddyfile.owui's @blocked regex already blocks the whole /api/v1/terminals family, and scripts/test_caddy_owui_blocklist.py (not in this diff's file list) already pins it with the same clean-degradation rationale the PR body states.

Review Summary

Severity Count Status
CRITICAL 0 pass
HIGH 1 warn
MEDIUM 0 info
LOW 1 note

Verdict: WARNING. One HIGH issue (scripts/verify-control-plane.py's silent demo-account default, contradicting this PR's own new rule) should be resolved or the doc's endorsement corrected before merge. The LOW doc-drift note can ship as a fast follow. Everything else interrogated (fail-loud blast radius, Admin Panel removal, bundle-fixture drift, terminals claim) checks out against the real files, not just the PR body's account of them.

Stage 6 review on PR #909 found a third silent-default identity path
this PR had missed: scripts/verify-control-plane.py:49 defaulted
HIVE_VERIFY_EMAIL to demo@hive-demo.invalid, the same ??-style
fallthrough already fixed in the other two paths. Unlike those two,
this script does not just sign in: its api-key lifecycle check mints
a real API key and sends a real POST /v1/chat/completions through it,
a real write and a real spend against whatever tenant the caller
belongs to. The mint call was correctly attributed as a HIGH finding.

Fixed the same way as the other two paths: HIVE_VERIFY_EMAIL now has
no default and main() exits loudly, before any HTTP call, when it is
unset. Confirmed no other caller depends on the old default: the
script is invoked from no CI workflow and imported by no other module
(the hyphenated filename cannot be imported as a regular module,
confirmed by grep across .yml/.md/.py for both the script name and the
two env vars it reads).

docs/live-test-auth.md's new section from the previous commit had
cited this script as a compliant read-only example, which was itself
wrong given what the script actually does; corrected both the new
section and the pre-existing paragraph about it to stop endorsing that
reading.

Also fixes the LOW finding from the same review: apply_ui_surfaces_patch.py's
docstring still described the Admin Panel entry as a kept, guarded
surface after the previous commit moved it to REWRITES. Updated to
name the two guards that remain (Knowledge, Settings dialog) and to
record why Admin Panel is no longer among them.

Added scripts/test_verify_control_plane.py: a minimal self-check
(no framework, no network) asserting the default is empty and that
main() exits before any HTTP call when HIVE_VERIFY_EMAIL is unset,
mirroring the importlib pattern already used by
scripts/test_seed_demo_owner.py for a hyphenated script filename.

All four self-checks re-run clean: test_owui_ui_surfaces.py,
test_caddy_owui_blocklist.py, test_owui_rag_env_config.py (unchanged,
sanity check), test_verify_control_plane.py (new).
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Both threads addressed in e070537.

HIGH (verify-control-plane.py). Confirmed and fixed the same way as the
other two paths. HIVE_VERIFY_EMAIL now has no default; main() exits
loudly, before any HTTP call (including the mint and the chat completion),
when it is unset. Confirmed no other caller depended on the old default: no
CI workflow invokes this script, and its hyphenated filename means nothing
else can import it. Also corrected docs/live-test-auth.md, both the new
section from the prior commit and the pre-existing paragraph about this
script, since both were endorsing a reading the script's actual behavior
contradicts. Added scripts/test_verify_control_plane.py, a minimal
self-check asserting the default is empty and that main() fails before any
request when the identity is unset.

LOW (apply_ui_surfaces_patch.py docstring). Fixed: now names the two
guards that remain (Knowledge, Settings dialog) and records that Admin Panel
moved from guarded to removed in #846.

All four self-checks re-run clean after the fix.

@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Visual proof, with an explicit split between what is deployed and what is not

Captured 2026-08-16. Every artifact is committed in d1f5332, under
docs/proof/pr-909-demo-surface-2026-08-16/, alongside the logs that back it.

This branch is not deployed. The demo box runs main, so nothing captured
against chat-hive.scubed.co can show the Admin Panel entry gone. Rather than
present a stale capture as proof of the fix, the menu change is proven on two
locally built images, and the deployed menu is shown separately, as it actually
is today.

1. The #846 claim: the entry leaves the client bundle

Both images were built from the same pinned upstream digest and the same
deploy/docker/Dockerfile.open-webui, differing only in the three patch files
this pull request touches, then run with WEBUI_AUTH=false. That is the method
the #833 and #772 proofs used.

Built from main:

user menu built from main, Admin Panel present

Built from this branch:

user menu built from this branch, Admin Panel absent

The menus read out of the DOM, so the difference does not depend on reading
pixels:

main   : ["Settings","Admin Panel","Archived Chats","Calendar","Automations","Sign Out", …]
branch : ["Settings","Archived Chats","Calendar","Automations","Sign Out", …]
ADMIN PANEL ENTRY PRESENT: true   (main)
ADMIN PANEL ENTRY PRESENT: false  (branch)

Nothing else in the menu moved.

What this does not prove. It proves the menu, not the route. /admin is
already 404ed by Caddyfile.owui independently of this change, so the route was
never reachable and this pull request does not make it less so. The claim is
that an audience no longer sees a control that leads nowhere.

2. The deployed menu today, shown rather than omitted

the deployed chat user menu, still showing Admin Panel

ADMIN PANEL ENTRY PRESENT ON THE DEPLOYED BUILD: true. Expected: this branch
is unmerged and undeployed. It becomes false on the demo box only after this
merges and deploy-demo-box.yml runs.

3. The #848 account state, after the enumerated litter was removed

These are live captures. They show the account, not the branch's code.

Chat sidebar, GET /api/v1/chats/?page=1 returning 0 rows (48 sidebar rows plus
1 archived row deleted through Open WebUI's own delete route, under owner
authorization):

the chat sidebar with no chat rows

API keys, with the six interaction gate probe rows now revoked through the
console's own revoke path (revoked rather than deleted, so the record stays
auditable; their expiry still reads 1 Jan 2026, which is issue #915):

the API keys page, every key revoked

Spend alerts, after removing the one addressed to
interaction-gate@example.invalid:

spend alerts, none configured

Cowork task rows were deliberately left alone in this pass: another change is
in flight against a stranded task row and deleting task rows underneath it would
destroy its evidence.

Provenance and redaction

Sessions came from apps/web-console/tests/e2e/support/live-auth.mjs, which
mints a magic link with the service-role key and touches no password;
scripts/seed-owui-e2e-user.py was not run. No URL in these journeys carries a
credential, and the capture script scrubs token-like query and fragment
parameters before any URL reaches a log. The key column in the API keys
screenshot is the product's own redacted-suffix display, on keys that are all
revoked. The full enumeration behind the deletions, including the per-row
evidence, is in the classification handed to the owner; the two open questions
it raised are filed as #915 and #916.

@sakibsadmanshajib
sakibsadmanshajib marked this pull request as ready for review August 16, 2026 16:07
@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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

@sakibsadmanshajib
sakibsadmanshajib merged commit 1c85179 into main Aug 16, 2026
31 of 33 checks passed
@github-actions
github-actions Bot deleted the fix/owui-demo-surface-litter-846-848 branch August 16, 2026 16:55
sakibsadmanshajib added a commit that referenced this pull request Aug 17, 2026
Two Dockerfile.open-webui images (main vs this branch), same pinned
digest, differing only in owui-patches/, run standalone against a stub
gateway serving the real six-alias catalog shape. Confirms the fix
still applies after the Phase 20 catalog/routing changes that landed
on main since this PR opened: main lists all six aliases in the chat
picker, this branch lists only the three chat ones. Same method PR
#909 used for its user-menu proof.
sakibsadmanshajib added a commit that referenced this pull request Aug 17, 2026
…eads (#792) (#814)

Fixes #792. Supersedes the mechanism in #776, which is left in place but
is not what fixes this.

## The diagnosis, confirmed against the pinned image

#776 hid `hive-embedding-default`, `hive-stt` and `hive-tts` by writing
Open WebUI's per-model `access_control`. It was reviewed, tested, merged
and deployed, and it changed nothing. Two independent reasons, both read
out of `ghcr.io/open-webui/open-webui:v0.10.2@sha256:9fcea9c…` rather
than assumed:

1. `deploy/docker/docker-compose.yml:738` sets
`BYPASS_MODEL_ACCESS_CONTROL: "true"`. `env.py:749` turns that into a
module constant, and the picker's listing path, `main.py:842` in the
`@app.get('/api/models')` handler, calls `get_filtered_models`, which
begins `if (user.role == 'user' or ...) and not
BYPASS_MODEL_ACCESS_CONTROL:` and otherwise `return models` unchanged
(`utils/models.py:418-472`). The flag switches the filter off for every
role, so the `access_control` values #776 writes are never read.
2. Even with that flag off, `get_filtered_models` exempts administrators
whenever `BYPASS_ADMIN_ACCESS_CONTROL` is set, and that defaults to true
(`config.py:2029`), while the `#457` tenant-role patch promotes every
tenant owner to an Open WebUI administrator. Every human on the demo box
is an admin.

So the mechanism could not fire on either count. Note that every test
shipped with #776 still passed, because they all asserted that the
correct values were **written** and none asserted that anything was
**read**.

## What actually depends on the bypass, measured

The tempting fix is to drop the flag. #792 flags document RAG and
text-to-speech as the risk. That turned out to be wrong in both
directions, and the measurement is what decided this PR's design.

An isolated harness (the pinned image, a pgvector container, and a stub
gateway serving exactly the six catalog aliases and answering chat,
embeddings, `audio/speech` and `audio/transcriptions`), with Open
WebUI's environment mirroring the `open-webui` service block, run
against two personas: an Open WebUI `admin` (what a tenant owner
actually is) and an Open WebUI `user`.

| | admin persona | member persona |
| --- | --- | --- |
| Bypass on (as deployed) | picker lists all 6, chat 200 | picker lists
all 6, chat 200 |
| Bypass removed | picker **still lists all 6**, chat 200 | picker
**empty**, chat **400 "Model not found"** |

Document RAG upload, ingest and query, TTS and STT all returned 200 for
both personas in both configurations. None of them consults Open WebUI's
model registry: `routers/audio.py` contains no access-control call at
all, and the RAG embedder issues a direct HTTP request to
`RAG_OPENAI_API_BASE_URL`.

So the bypass does not protect RAG or TTS. It protects chat for
non-admin members, and removing it would not have hidden a single alias
from the people who use the demo. Full runs in
`docs/proof/issue-792/probe-*.json`.

## The fix

Filter the listing the picker reads, and nothing else.

A build-time splice into `main.py`'s `/api/models` handler, the same
posture as this Dockerfile's other Open WebUI patches. It runs on the
response: `request.app.state.MODELS` is untouched, so chat, RAG
embeddings and text-to-speech still resolve every alias. `GET
/v1/models` on the API origin is not touched at all, there are no Go
changes in this PR, and that endpoint stays an OpenAI-contract surface
that `support-matrix.json` marks supported and that direct API clients
depend on.

Two things are deliberate because of how #776 failed:

- The inserted call is **unconditional**, and `assert_unconditional` in
the patch fails the build if a future edit puts it behind a flag, a role
check or an access-control branch. There is no conditional available
here that this deployment does not already disable.
- The transform lives in a `patch()` function rather than inline, so the
guard can run the real thing against a checked-in verbatim excerpt of
the pinned image's own handler (`pinned-main-excerpts.json`). PR CI
never builds this image, which is exactly why that excerpt is committed,
following the precedent `dump_bundle_excerpts.py` already sets.

The hidden set is never hardcoded. It is `HIVE_PICKER_HIDDEN_MODEL_IDS`
(compose owns it) unioned with whatever `RAG_EMBEDDING_MODEL`,
`AUDIO_TTS_MODEL` and `AUDIO_STT_MODEL` name, so changing the
admin-selected embedding alias does not need a second edit (D-001).
Unset variables contribute nothing, so a deployment that sets none of
them keeps upstream behaviour.

### Rejected

- **Dropping `BYPASS_MODEL_ACCESS_CONTROL`.** Measured above: breaks
chat for every member, hides nothing from admins.
- **Anything access-control shaped**, including repairing #776. Inert by
construction on this deployment.
- **Filtering `/v1/models` in edge-api by shim identity.**
`owui_unwrap.go` keeps `hasShimAuthorization` unexported precisely so no
route branches on "is this the shim key", and `handleModels` documents
that there is no exception for it. Both would have had to be reversed.
- **Filtering `/v1/models` for everyone.** Violates the OpenAI contract;
upstream OpenAI lists embedding and audio ids too.

## The guard

`scripts/test_owui_model_picker_filter.py`, wired into `make
test-scripts`, which the required `repo-policy-lints` check runs on
every non-docs change.

It is built to fail the way #776 would have failed. It asserts the
filter reaches the image and lands on the response path between
upstream's own filtering and the return, rather than asserting a value
was written, and it asserts the coupling directly: while compose sets
the bypass, a picker filter that does not depend on access control must
be present and must run in the image.

**Red, against the pre-fix image with the bypass on, which is the
deployed world:**

```
OWUI chat model picker filter regression (1 failure(s)):
  run_live: the chat picker still lists ['hive-embedding-default', 'hive-stt', 'hive-tts'] (issue #792). The picker filter is not on the response path, or something is gating it.
exit=1
```

**Green, patched image, bypass still on, same harness, same command:**

```
live: gateway serves 6 models including all 3 non-chat aliases; picker lists 3 and none of them
OWUI chat model picker filter: 10 checks passed
exit=0
```

The static half that CI runs goes red the same way with the Dockerfile's
`RUN` line neutered and compose untouched, naming both the missing
invocation and the bypass coupling. Transcripts in
`docs/proof/issue-792/README.md`.

## Live proof

Both halves, captured against a running stack, in one signed-in session.

Picker before, signed in as the owner persona. All six, including the
three that cannot serve a completion:

![picker before, all six
aliases](https://raw.githubusercontent.com/sakibsadmanshajib/hive/fix/model-picker-access-control-792/docs/proof/issue-792/01-before-picker-owner.png)

Picker after, same persona, same harness, patched image. Three chat
aliases:

![picker after, three chat
aliases](https://raw.githubusercontent.com/sakibsadmanshajib/hive/fix/model-picker-access-control-792/docs/proof/issue-792/02-after-picker-owner.png)

The gateway's own list in that same session, post-fix, still carrying
all six:

![gateway list still returns
six](https://raw.githubusercontent.com/sakibsadmanshajib/hive/fix/model-picker-access-control-792/docs/proof/issue-792/04-after-gateway-list-unfiltered.png)

And `/api/models`, the listing the picker reads, post-fix:

![api models listing after the
fix](https://raw.githubusercontent.com/sakibsadmanshajib/hive/fix/model-picker-access-control-792/docs/proof/issue-792/03-after-picker-listing-endpoint.png)

Probe C confirms the rest of the surface is unaffected with the fix in
place and the bypass left on: chat 200, RAG ingest completed, RAG query
returning a hit, TTS 200 and STT 200, for both personas.

## Test plan

- [x] `make test-scripts` green, including the new guard
- [x] New guard proved red against the pre-fix image and green against
the patched one, same harness
- [x] New guard proved red statically with the Dockerfile `RUN` line
neutered
- [x] Image builds; every patch assertion in `Dockerfile.open-webui`
passes
- [x] Picker shows only the three chat aliases, admin and member
personas
- [x] Gateway model list still returns all six in the same session
- [x] Chat, document RAG ingest and query, TTS and STT all still work
post-fix
- [ ] Confirmed on the demo box after deploy, which this PR does not
perform

## Notes for the reviewer

The Open WebUI `AUDIO_*` settings are wired in the harness even though
`docker-compose.yml` does not set them today. #792 named text-to-speech
as a risk, and a risk that is not configured cannot be measured. That
harness wiring is not part of this diff.

#776's control-plane machinery is left untouched. It is inert on this
deployment but harmless, and it becomes live if the bypass is ever
turned off, which is a decision for whoever makes it. The guard makes
that coupling explicit rather than silent.

## Rebased onto main, 2026-08-17

This PR sat open since 2026-08-09. A lot landed on `main` in that
window,
including Phase 20 provider-catalog waves and pricing corrections, so
this
branch was merged forward (`4e39de40`) and re-verified against current
code
rather than assumed still correct.

Checked, not assumed:

- The pinned image digest is unchanged
(`ghcr.io/open-webui/open-webui:v0.10.2@sha256:9fcea9c…`).
- `public.model_aliases` has not grown a new non-chat alias and none of
the
  three (`hive-embedding-default`, `hive-stt`, `hive-tts`) were renamed;
confirmed against every migration that touches the table, most recently
  `20260801_13_alias_price_unit.sql`.
- `scripts/test_owui_model_picker_filter.py` still passes statically
(10/10)
  against the checked-in `pinned-main-excerpts.json`.
- `docker-compose.yml`'s `HIVE_PICKER_HIDDEN_MODEL_IDS` default and
`RAG_EMBEDDING_MODEL`/`OWUI_RAG_EMBEDDING_ALIAS` wiring are unchanged
and
  still name the same three aliases.

One cleanup from the merge itself: `.wolf/buglog.jsonl`'s `merge=union`
driver kept this branch's own append after merging `main` forward.
Branch
appends to that file are never allowed regardless of the merge driver
(`.claude/rules/openwolf.md`); dropped it back to `main`'s content in a
separate commit and moved the entry to **Buglog entry** below.

### Fresh A/B proof, same method as PR #909

The demo box still runs `main`, so nothing captured against it can show
this
fix. Two `Dockerfile.open-webui` images, same pinned digest, differing
only
in `owui-patches/`, run standalone against a stub gateway serving the
real
six-alias catalog shape. Full method and logs:
`docs/proof/pr-814-rebase-verification-2026-08-17/README.md`.

Built from `main` (no patch) — all six aliases in the dropdown:

![picker built from main, six
aliases](https://raw.githubusercontent.com/sakibsadmanshajib/hive/fix/model-picker-access-control-792/docs/proof/pr-814-rebase-verification-2026-08-17/picker-main.png)

Built from this branch — three chat aliases:

![picker built from this branch, three
aliases](https://raw.githubusercontent.com/sakibsadmanshajib/hive/fix/model-picker-access-control-792/docs/proof/pr-814-rebase-verification-2026-08-17/picker-branch.png)

DOM read backing each screenshot (which of the six known ids actually
rendered in the opened dropdown), not just the image:

```
main:   ["hive-auto","hive-default","hive-fast","hive-embedding-default","hive-stt","hive-tts"]
branch: ["hive-auto","hive-default","hive-fast"]
```

## Is filtering the picker the right fix, or should these aliases not be
in the chat-facing catalog at all?

Asked directly, and the honest answer has two parts, because a design
pass
run in parallel on this same question reached a real finding that this
PR's
own "Rejected" section above already anticipated and rejected once.

**The finding, verified against current code and correct as stated:**
`public.model_aliases` carries no `modality`/`is_chat_model` column,
`apps/control-plane/internal/catalog/repository.go`'s alias-listing
queries
(`ListPublicAliases`, the tenant-visibility query, `GetAlias`,
`ListAllAliases`) never join `provider_capabilities`, and
`apps/edge-api/cmd/server/main.go`'s `handleModels` (~line 774)
serializes
`snapshot.Models` verbatim. So yes: `GET /v1/models` on the API origin
lists
`hive-embedding-default`, `hive-stt` and `hive-tts` with nothing marking
them
as non-chat, for every caller, chat-scoped or not.

**Where the conclusion drawn from that finding doesn't hold:** the
proposal
was to filter `/v1/models` itself so it "returns only chat-capable
models
for a chat-scoped request." This PR's own Rejected section already
considered and rejected the unscoped version of that
("Filtering `/v1/models` for everyone. Violates the OpenAI contract;
upstream
OpenAI lists embedding and audio ids too.") for a reason that's still
true:
a real OpenAI `/v1/models` response is undifferentiated too, and a
direct API
client that wants to call `POST /v1/embeddings` or `POST
/v1/audio/speech`
with a Hive alias needs to discover it via `GET /v1/models` first, the
same
way it would against upstream OpenAI. Stripping the three aliases from
the
endpoint unconditionally would break that discovery path for every
non-chat
SDK integrator to fix a problem that, for them, does not exist: nobody
calling `/v1/embeddings` is confused by seeing `hive-embedding-default`
in
the model list.

**So: keep #814's picker filter. Do not expand this PR to touch
`/v1/models`.**
The client-side fix stays the answer to the owner's actual complaint
(Open
WebUI's chat picker specifically), and it is not "defense in depth" for
a
server fix that doesn't exist — until something reconfigures Open WebUI
to
consume a genuinely chat-scoped listing, this patch is the only thing
making
the picker correct.

There is a real, narrower question worth designing separately: a
**chat-scoped** variant of the listing, for a caller that opts in to
"only
models I can send a chat completion to" (which Open WebUI's picker could
then consume server-side instead of client-side). That needs its own
signaling mechanism (query param, header, or a distinct endpoint) so the
unparameterized `GET /v1/models` keeps its current, contract-correct
shape.
No migration is needed to build it either way: the capability data
already
exists twice over and is unused by the listing endpoint today —
`apps/control-plane/internal/catalog/http.go`'s tested
`isNonChatModality`
over `capability_badges` (currently wired only to the inert, bypassed
#776
OWUI `access_control` sync), and
`provider_capabilities.supports_chat_completions`
/ `supports_embeddings` / `supports_tts` / `supports_stt`, already
joined
per-route in `listRouteSnapshots` for routing decisions but never
surfaced to
`handleModels`. Filed as #931 rather than folded into this PR, since
it's a
genuine design question (is there real demand for it beyond this one
picker?)
and, if built, its own reviewed diff.

## Buglog entry

Carried here per branch policy (never appended to `.wolf/buglog.jsonl`
on a
feature branch). To be appended to `main` in a separate buglog-only PR
once
this merges:

```json
{"id":"bug-msmbcjgc-8012bd","timestamp":"2026-08-09T21:27:12.251Z","related_bugs":[],"occurrences":1,"last_seen":"2026-08-09T21:27:12.251Z","error_message":"Open WebUI chat model picker still listed hive-embedding-default, hive-stt and hive-tts after PR #776 merged and deployed (issue #792)","root_cause":"#776 hid them by writing Open WebUI per-model access_control, but deploy/docker/docker-compose.yml sets BYPASS_MODEL_ACCESS_CONTROL true, which makes main.py skip get_filtered_models for every role in the pinned v0.10.2 image. Even with that flag off, get_filtered_models exempts admins whenever BYPASS_ADMIN_ACCESS_CONTROL is set, and it defaults to true while this deployment promotes every tenant owner to an Open WebUI admin. The mechanism could never fire, and every test shipped with it asserted that values were written rather than read.","fix":"filter the /api/models response instead, via a build-time patch (deploy/docker/owui-patches/apply_model_picker_patch.py plus hive_model_picker.py). Unconditional, env-driven, applied after upstream own filtering and before the return, leaving request.app.state.MODELS untouched so chat, RAG embeddings and TTS still resolve every alias. Measured on a booted container: removing the bypass instead gives members an empty picker and HTTP 400 Model not found on chat, while leaving RAG and TTS unaffected, so the bypass protects chat and not the named risks. Guard: scripts/test_owui_model_picker_filter.py, with a --live mode that fails when the picker still lists the aliases.","tags":["open-webui","model-picker","access-control","inert-fix","issue-792","issue-776","docker-compose"]}
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)
sakibsadmanshajib added a commit that referenced this pull request Aug 17, 2026
…ust from one compose file (#936)

Owner instruction, 2026-08-17: the chat interface is "overly
future-proofed with unnecessary features" and the unused elements must
go. This is the subtraction half only. The information architecture and
visual redesign belong to the separate agent already working on them, so
nothing here designs replacement navigation.

## What was actually wrong

The three surfaces the owner named (Notes, Calendar, Automations) were
already off on the demo box, and only there. `docker-compose.yml` names
three environment variables and `owui-patches/hive_rag_env_config.py`
reconciles them onto that deployment's database. That is a complete
control for one deployment and no control at all anywhere else:
upstream's own defaults are all on, so a plain `docker run` of the Hive
image rendered every one of them in full. The `before` capture in this
pull request produces the owner's list word for word, which is what
identifies the image, rather than the deployment, as what he was looking
at. Off has to be a property of the product, not of one compose file
that a bare run, an enterprise override or the next proof harness can
fail to repeat.

The second half of the question was whether hiding a menu entry is
honest in each case. For three of the four flag-backed surfaces it is:
`calendar.py` checks its flag on all 13 of its routes, `automations.py`
on all 8, `memories.py` on all 11. `notes.py` checks it on none of its
9, so with the entry gone and the page route already 404'd, `POST
/api/v1/notes/create` still created a note for any signed-in user. That
one gets a proxy rule; the other three deliberately do not, because a
second control there would be decoration.

## Changes

| file | change |
| --- | --- |
| `Dockerfile.open-webui` | `ENV
ENABLE_NOTES/ENABLE_CALENDAR/ENABLE_AUTOMATIONS/ENABLE_MEMORIES/ENABLE_VERSION_UPDATE_CHECK=false`,
so the image itself ships the reduced product. Docker `ENV` rather than
a rewrite of upstream's own `os.getenv` lines: no literal to drift on a
digest bump, and compose still overrides it, so an enterprise deployment
that wants Notes back sets the variable to true and the reconcile flips
the persisted row |
| `docker-compose.yml` | adds `ENABLE_MEMORIES: "false"`. The compose
values stay, all of them: on a deployment that has already booted they
are still the only thing that reaches the database |
| `hive_rag_env_config.py` | adds `memories.enable` to the reconciled
set |
| `hive_ui_surfaces.py` | two new bundle rewrites, `settings-admin-link`
and `settings-vendor-translate-link` |
| `Caddyfile.owui` | `api/v*/notes` added to `@blocked` |
| `scripts/test_owui_ui_surfaces.py`,
`scripts/test_caddy_owui_blocklist.py` | assertions for all of the
above, written before the changes |

Settings > Personalization is the only tab removed. Its feature is
upstream's own Memory: nobody chose it, `ENABLE_MEMORIES` defaults to
true, and while it is on `Chat.svelte` adds `features.memory: true` to
every turn, so each message also pays for a lookup against a second
memory store that Open WebUI owns and Hive does not read. Hive's memory
subsystem is one subsystem on Hive's own Postgres (`.wolf/decisions.md`
D-020) and is not built yet, so this is a competing store rather than a
head start on it.

The two new bundle rewrites were found by looking at the screen this
change was already photographing, not by re-reading an issue. The
Settings dialog's bottom-left "Admin Settings" link is the twin of the
user menu's Admin Panel entry removed in #909 and points at the same
`/admin` route Caddy already 404s, so every tenant owner sees a
permanent control that always fails. The language picker still carried
"Help us translate Open WebUI!", linking a Hive customer to the vendor's
contributing guide by name, which is the same family as the
Documentation and Releases links removed in #772.

## Two different promises, said plainly

Removing a surface from one compose file while the feature still ships
in the image is a half-removal that reads as complete and is not. This
pull request makes the promise explicit per surface, because they are
not the same promise.

**Gone from the built image.** Cannot be brought back by configuration
on any deployment, because the entry no longer exists in the client
bundle. Workspace Models, Prompts, Tools and Skills; the Playground in
both the sidebar and the user menu; the user menu's Admin Panel; the
vendor's Documentation and Releases links; the release-notes dialog; the
vendor branding on Settings > About; the Settings > Integrations tab;
and now the Settings dialog's "Admin Settings" link and its "Help us
translate Open WebUI!" link.

**Off in the image, end to end, and re-enablable on purpose.** Notes,
Calendar, Automations, and the Memory feature behind Settings >
Personalization. The image now ships all four false, so the reduced
product is what any run gets, and the navigation entry, the page and the
API go together because each router refuses on the same flag. A
deployment that genuinely wants one back sets the variable to true and
the reconcile flips the persisted row, which is why these stay flags
rather than becoming bundle rewrites. Before this change all four were
on in the image and off only where `docker-compose.yml` was present,
which is exactly the half-removal the title names.

**One residual, stated rather than papered over.** `/api/v1/notes` is
closed at the proxy, not in the image. `notes.py` is the one router in
that set that checks its flag on none of its routes, so with
`notes.enable` false its 9 endpoints still answer and `Caddyfile.owui`
is what refuses them. Every deployment this repository ships puts that
Caddy in front of Open WebUI, so there is no shipped configuration where
those endpoints are reachable, but a bare `docker run` of the image is
not covered. Closing it in the image would mean unmounting an upstream
router in `main.py`, which would also make the feature un-re-enablable
by configuration and so break the promise directly above. Deleting the
upstream routers and Svelte pages outright is a fork-and-rebuild move
that belongs to the redesign, not to a subtraction pass.

## Enumeration

Taken from the pinned image itself,
`ghcr.io/open-webui/open-webui:v0.10.2@sha256:9fcea9c...`, not from the
deployment and not from memory. Worth recording separately: that image
ships JavaScript source maps whose `sourcesContent` carries the complete
original Svelte source, 612 files. The repeated claim in `owui-patches/`
that the image ships "no frontend source" is true only of the build
tooling. Reading a gate no longer requires reverse-engineering minified
identifiers.

### Sidebar

| entry | verdict | why |
| --- | --- | --- |
| New Chat | keep | the product |
| Search | keep | chat history search, used |
| Notes | remove | upstream note-taking, no Hive counterpart, no Hive
code reads it |
| Workspace | keep | its only remaining tab is Knowledge, which is the
RAG document upload |
| Folders | keep | chat organisation |
| Chats, pinned chats, archived | keep | the product |
| Channels | already off | upstream default is off; group chat, not a
Hive feature |
| pinned Models section | keep | renders only when a user pins a model |

### User menu

| entry | verdict | why |
| --- | --- | --- |
| Settings | keep | reduced, see below |
| Archived Chats | keep | real, reachable, used |
| Workspace | keep | as above |
| Notes, Calendar, Automations | remove | as above |
| Sign Out | keep | the product |
| Admin Panel, Playground, Documentation, Releases, Keyboard Shortcuts
help block | already removed | #772, #909 |
| Update your status, and the profile header it is wrapped in | design
question, see below |
| Active Users count | design question, see below |

### Settings dialog

Nine tabs upstream, eight after #771 removed Integrations, six after
this change.

| tab | verdict | why |
| --- | --- | --- |
| General | keep | theme, language, per-user system prompt, advanced
parameters. Its vendor translation link is removed here |
| Interface | keep for now | see the design question below. Roughly
forty upstream toggles, which is the "confusing" the owner named, but
reducing it is composition, not subtraction |
| Personalization | remove | upstream Memory, nobody chose it, competes
with D-020 |
| Audio | keep | speech-to-text and text-to-speech, a locked demo
capability |
| Data Controls | keep | chat import, export, archive-all, delete-all |
| Account | keep, with a flag raised below | name and profile image are
real. Its API keys, JWT token, notification webhook and password-change
blocks are already hidden by upstream defaults this deployment does not
change, correctly, since the Hive console owns keys and every account is
SSO-only |
| About | keep | already de-branded by #784 |
| Connections | already off | `ENABLE_DIRECT_CONNECTIONS` defaults off;
direct browser-to-provider connections would bypass the gateway |
| Integrations | already removed | #771 |

## Deferred as design questions

Each of these is a removal whose only available mechanism also takes
something else, or whose answer is where the feature moves rather than
whether it dies. Left alone deliberately, for the redesign owner.

1. **Update your status, and the user menu's profile header.** The
presence feature is unambiguously unused. Its only flag,
`ENABLE_USER_STATUS`, gates the whole `{#if profile}` block, so turning
it off also removes the avatar, name and email from the top of the menu.
Removing the status controls alone needs a bundle rewrite, and whether
the identity header belongs in that menu at all is a navigation
decision.
2. **Active Users count.** Its flag does not reach this deployment's
audience: the gate is `enable_public_active_users_count || role ===
'admin'`, and every tenant owner is an Open WebUI admin here, so hiding
it needs a bundle rewrite. Small, but it is a decision about what the
menu is for.
3. **Settings > Interface.** Around forty upstream toggles, most of
which no Hive user will ever want. Reducing it is the settings rebuild,
explicitly out of scope for this pull request.
4. **Settings > Account bio, gender and birth date.** Ungated
personal-data collection with no consumer anywhere in Hive: nothing
reads these fields. Flagged rather than removed because it sits inside a
tab the redesign is about to rework, and because deleting input fields
for data some accounts may already hold is a data question, not a
navigation one.
5. **Per-message rating.** `ENABLE_MESSAGE_RATING` defaults on, so every
assistant message carries thumbs up and down, and the feedback rows they
write are only ever surfaced in Open WebUI's own admin Evaluations page,
which is blocked. The data goes nowhere. Not reachable from the sidebar,
the user menu or Settings, so it is outside this pull request's
enumeration.
6. **Code interpreter and code execution.** Both default on and put a
toggle in the chat input. Whether they overlap Cowork and the coding
agent, or complement them, is a product call.
7. **Follow-up, tag and title auto-generation.** All default on and each
spends a real model call per turn. A cost decision, not a surface one.

## Files touched, for collision checking

The navigation-shell work should not overlap any of these. Eight files
outside `docs/proof/`:

```
deploy/docker/Caddyfile.owui
deploy/docker/Dockerfile.open-webui
deploy/docker/docker-compose.yml
deploy/docker/owui-patches/hive_rag_env_config.py
deploy/docker/owui-patches/hive_ui_surfaces.py
deploy/docker/owui-patches/pinned-bundle-excerpts.json
scripts/test_caddy_owui_blocklist.py
scripts/test_owui_ui_surfaces.py
```

`hive_ui_surfaces.py` and its `pinned-bundle-excerpts.json` fixture are
the one real collision risk, since any other change that removes a
surface by bundle rewrite lands in the same table and regenerates the
same fixture. Nothing else here is frontend code at all.

## Test plan

- [x] `make test-scripts`, which is what CI runs for these files. New
assertions were written first and observed failing: image ENV absent,
`ENABLE_MEMORIES` absent from compose and from the reconcile,
`/api/v1/notes` not blocked, memories missing from the coverage table.
- [x] Image built from this branch, the bundle rewrite pass reporting
all 22 surfaces matched and both guards intact.
- [x] Before and after images built from the same pinned digest, menus
and settings tabs read back out of the DOM.
- [x] Notes API probed with a real session token, direct and through a
real Caddy on both configurations.
- [x] Whole interface run through the branch's own Caddy, recording
every console error and every 4xx or 5xx: one pre-existing 404 on
`/api/v1/terminals/` (#770) and no request to `/api/v1/notes` at all.
- [x] `npm run lint:proof-tokens`.
- Not run: the six locked demo capabilities are untouched by this diff.
Chat, embeddings, voice, knowledge, Cowork and the coding agent have no
path through Notes, Calendar, Automations, Memory, the Settings admin
link or the vendor translation link, and the kept APIs were probed
alongside the blocked one.

## Visual proof

`docs/proof/owui-surface-subtraction-2026-08-17/`, per the visual-proof
rule. Attached below as well.

## Buglog entry

```json
{"id":"owui-surfaces-image-vs-compose-2026-08-17","date":"2026-08-17","title":"Removed Open WebUI surfaces were off in one compose file and on in the image","error_message":"Notes, Calendar and Automations rendered in the user menu and sidebar, and Settings > Personalization in the settings dialog, on any run of the Hive Open WebUI image that did not repeat docker-compose.yml's ENABLE_* lines, including the repo's own before/after proof builds. Separately, POST /api/v1/notes/create returned 200 and created a note with notes.enable false.","root_cause":"Two independent gaps with the same shape. First, the removal of a product surface was expressed only as environment variables in docker-compose.yml plus a persisted-config reconcile, so it reached exactly one deployment and nothing else; upstream's defaults for all of them are on, and PR CI never builds Dockerfile.open-webui, so image and compose could disagree indefinitely with no signal. Second, a feature flag was assumed to be a complete control without checking that the backend honours it: calendar.py, automations.py and memories.py check theirs on every route, notes.py checks its own on none of its 9, so the flag hid the navigation entry and left the API fully callable.","fix":"Dockerfile.open-webui sets ENABLE_NOTES, ENABLE_CALENDAR, ENABLE_AUTOMATIONS, ENABLE_MEMORIES and ENABLE_VERSION_UPDATE_CHECK false as image ENV defaults, so the reduced product is a property of the image while compose keeps overriding it and the reconcile keeps reaching already-booted databases. Caddyfile.owui adds api/v*/notes to @Blocked. scripts/test_owui_ui_surfaces.py gains _image_env() and asserts the image, the compose value and the reconcile entry together for every flag-backed surface, plus that a non-persisted flag is deliberately absent from the reconcile.","tags":["open-webui","docker","feature-flags","caddy","persisted-config","dead-code","ui"]}
```

Closes nothing on its own; the redesign issue tracks the rest.
sakibsadmanshajib added a commit that referenced this pull request Aug 17, 2026
…ne Hive navigation (#938)

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

```json
{"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"]}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant