- Declare `__all__` in every package `__init__.py` whose `PROJECT_ROOT`
is imported by its modules (163 packages), so the constant counts as
exported; list every public name where the package defines more.
- Empty the 15 package `__init__.py` files whose `PROJECT_ROOT` no module
imports.
- Declare the public API of `utils.github.variant.axes` in `__all__`,
re-exports included, instead of silencing them with `noqa: F401`; the
list matches the one `feature/store-i18n` already carries.
- Use `import unittest.mock as mock` in `test_applications.py`, as the
rest of the suite does.
- Spell `_NON_PRINTABLE` in `utils/yaml_bootstrap.py` as the equivalent
BMP-only positive class; it matches exactly the same 2111 code points
as the old negated class across U+0000..U+10FFFF.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The deploy decides what ADAPTER_PORT or OPENWEBUI_ADMIN_NAME is and
writes it into the environment. A default in the reader is a second place
that value lives, and it is the one nobody greps for. os.environ["X"]
fails loudly on a missing key, which is the behaviour a misconfiguration
deserves.
Empty values stay free, because they add no second source and only say
absent: "", None, False, 0, the empty containers, and the "[]"/"{}" a
json.loads reader hands an absent variable. A default that is itself an
environ.get is free too, since it reads the one source again under
another key; a literal ending such a chain is still reported. Detection
runs on the parsed tree, so a call split across lines reads the same.
Five sites were real duplication and lost their default: the adapter
port, openwebui's admin name (a role that runs as a bot user), odoo's
OIDC uid field, seaweedfs' public-primary flag, and the swarm hosts
entry IP. Nineteen carry a nocheck naming why the value is not the
deploy's: contributor tooling whose default is the only source, test
scaffolding that writes the file it names, and two readers of
CA_TRUST_CERT_EXTRA_HOST, which nothing in the repo sets.
utils/diagnostics/container.py resolves without a marker: RESCUE_DEPTH is
a recursion base case, and the integer 0 says what the string "0" said.
The file sits exactly at the 500-line cap, so a comment line was not
available.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolved 35 conflicts per the designer x store-i18n checklist: store-i18n stays the single point of truth for credentials and role logic, designer's verified design work is kept.
Credential convergers: store-i18n's realign contract wins where both sides have one (mediawiki realign_admin_password.sh over converge_admin.php, pgadmin realign over administrator.yml, matomo utils/converge_superuser.yml over 09_admin_password.yml); the designer-only ones with no counterpart stay.
READMEs: test_readme.py hard-fails a committed Quick Setup or Credits block, both of which designer's side carries, so those go and only the Further Resources heading rename and the gallery links remain. SuiteCRM takes main's session fix, leaving exactly one SUITECRM_SESSION_CONFIG_HOST and dropping the Symfony cache-rebuild task the entrypoint now covers.
Two defects the rename detection hid: git paired store-i18n's web-app-meta against designer's web-app-sphinx, so sphinx's design slots would have been lost and its require(./test-design) landed in web-app-meta, whose spec has no such file. The sphinx design spec and its require now sit in web-app-docs, the role store-i18n renamed sphinx to.
Designer's eleven new Playwright design specs read env with || "" defaults, which test_unguarded_env_default.py refuses because an empty string matches every substring assertion; 35 bindings now go through requireDotenvValue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Open WebUI loads /static/custom.css on every page, so tasks/00_core.yml
renders templates/custom.css.j2 when design is on and meta/volumes.yml
mounts it read-only there; it maps the sign-in page, the chat shell,
Markdown, tables and code onto the design tokens and leaves the OLED
dark theme to the user. templates/design.js.j2 mirrors the native theme
setting into data-design-theme. meta/services.yml keeps logo and title
replacement off, so Open WebUI keeps its own branding.
With sso and ldap both off, tasks/utils/admin.yml runs files/python/
administrator.py, which creates the native administrator or resets its
password and promotes it, so the native sign-in has an account; the
identity is lookup('users', 'platform_administrator') as on
feature/store-i18n, declared in meta/users.yml next to the user the
role still reads elsewhere. tests/unit/python/roles/web-app-openwebui/
files/test_administrator.py covers the script with fake values.
files/playwright/test-design.js checks tokens, sign-in page, chat shell,
a stored answer, the native theme setting, the provisioned
administrator, the in-house custom.css, logo and title, and captures
the gallery; files/playwright/_shared.js waits for editable password
fields. README.md links the gallery; TODO.md records the open
self-registration with sso and ldap off and the bot password and
bearer tokens in container exec argv.
The design was verified green on the compose stack (10 passed); the
switch to the platform administrator is checked statically only, the
first live run is the verification on the merged state.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Of the 76 open code scanning alerts, 39 point at code that can change.
svc-ai-mcp-adapter:
- A path argument that is empty or made only of dots is refused.
Percent-encoding keeps a separator inside its segment, but "." and ".."
need none, so an upstream that resolves dot segments could answer from a
path the contract does not name.
- parse_duration splits on the units. The pattern it used before rescanned
a long run of digits from every start position.
Tests and the README generators:
- Every Jinja environment built with autoescape=False and a "noqa: S701"
uses select_autoescape now, as six test modules and
utils/templating/jinja.py already did. It escapes templates named
*.html, *.htm and *.xml, which none of these call sites loads.
- Dead code goes: a doubled "rel" block in the email lint, an unread
_SKIP_DIRS, and a module that _resolve returned to no caller.
- Two "except ...: pass" blocks become contextlib.suppress and
assertRaises.
- Nine test modules import unittest.mock one way instead of two, and one
lookup test imports its module once.
Closes#807
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With svc-ai-agent-broker in the inventory, Open WebUI adds the broker as a
second OpenAI connection (OPENAI_API_BASE_URLS/KEYS). It forwards the
user's id and email headers and waits up to the broker's request_timeout,
so a cold agent start can finish.
BYPASS_MODEL_ACCESS_CONTROL turns false while the broker is enabled.
agent_model_grants.py then runs inside the container and:
- grants every LiteLLM model read access for all users;
- grants hermes and openclaw read access for their
/roles/web-app-<platform>/agent-user group only;
- reads grants back from the database, because the API view is filtered by
the admin access control.
The grant rows also give every gateway model the database entry whose
absence made check_model_access answer "Model not found".
Open WebUI shows a model without a grant to administrators only. So a
svc-ai-litellm deploy now reruns the grant step in a running Open WebUI,
and a model added to the gateway is visible without an Open WebUI deploy.
The include sets application_id explicitly, because the app loader's
include var would otherwise resolve Open WebUI's lookups against LiteLLM.
Without the broker the rendered connection, key and bypass are unchanged;
only the variable name moves to the plural form Open WebUI falls back from.
The Playwright spec:
- revokes biber in Keycloak;
- removes the stale Open WebUI membership, which the OIDC sync keeps when
the groups claim turns empty;
- asserts every served gateway model is listed for biber and no agent
model is;
- grants the groups and expects both agents to answer through the broker.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GET /api/v1/tools/ builds its answer for an MCP server out of stored config alone - routers/tools.py:125-126 reads info off the connection row and never touches the network. So the three specs that read it pass unchanged against a provider that is down, refuses the deployed bearer, or serves nothing. What they establish is registration and the OIDC group to access-grant chain, not that the bridge carries.
POST /api/v1/configs/tool_servers/verify is the one route in 0.11.0 that does a real MCP initialize plus tools/list without an LLM, and it authenticates with the key in the body it is handed. The spec reads the stored connection and echoes it back, so the handshake runs over the deployed URL with the deployed credential through Open WebUI's own client, and asserts three things per server: it answers, it serves at least one tool, and the tool set contains the read probe its meta/mcp.yml declares.
Only bearer connections are verified. A system_oauth one resolves the caller's own upstream token per request and the administrator holds none, so it would fail on the caller rather than on the bridge; no provider declares auth: oidc today, but the filter keeps the spec honest when one does. The per-test budget scales with the number of registered servers instead of a fixed guess.
MCP_EXPECTED_TOOLS carries the declared read probe per server. Its rendering is unit-tested by extracting the block from the template itself rather than restating it, and decoded the way a spec decodes it - dotenv_quote emits an escaped JSON string that decodeDotenvQuotedValue unwraps before the spec parses the payload, so comparing after one decode would test a value no spec ever sees.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role scripts moved into per-language folders but their unit mirrors did
not follow, so the index carried paths under tests/unit/roles/ that no longer
existed on disk while their counterparts under tests/unit/python/roles/ were
untracked.
That stale index broke the gate while naming the wrong culprit: the
repository lint reports "rsync --backup in 19 place(s)" because it counts
files it cannot open, and every entry was an [Errno 2] No such file or
directory rather than an actual rsync call.
Fifteen of the moves are recognised as renames; the rest are __init__ files
that collapsed into the new tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>