This is Open WebUI with an Extra assistant embedded in it, wired to act as whoever is signed in rather than through one shared admin key. Everything Extra-specific lives in extra/; the rest is upstream Open WebUI (its own README).
Open WebUI — two terminals, per the official dev guide:
# terminal 1 — frontend
cp -RPp .env.example .env
npm install
npm run build
npm run dev# terminal 2 — backend
cd backend
cp .env.example .env # dev.sh needs WEBUI_SECRET_KEY set; unlike start.sh it won't generate one
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt -U
sh dev.shFrontend at http://localhost:5173, backend at http://localhost:8080.
Extra — set up extra/.env per the Environment section below, then from a clone of extra-org/extra:
make dev AGENTS=<path-to-this-repo>/extra/agents.yml ENV_FILE=<path-to-this-repo>/extra/.envOr with Docker instead of a local extra checkout:
docker run -d -p 8100:8100 \
-v <path-to-this-repo>/extra:/workspace -w /workspace \
--env-file <path-to-this-repo>/extra/.env \
ghcr.io/extra-org/extra:latest \
agent-manager --config agents.yml --port 8100Playground at http://localhost:8100/playground. Sign into Open WebUI and the assistant appears there too.
cp extra/.env.example extra/.envEverything below is already filled in except the model key — open extra/.env and add that, and you're running:
ANTHROPIC_API_KEY=your-key-here # or see the Ollama comment for a free local model instead
OPEN_WEBUI_URL=http://localhost:8080
AGENT_AUTH_MODE=mint
AGENT_AUTH_SECRET=pLiUxoi+ziwTUIhNhVg/AN1U50UMom00
AGENT_AUTH_CLAIM_USER_ID=id
AGENT_AUTH_CLAIM_ROLES=role
CORS_ORIGINS=http://localhost:5173What each of these is, and why it has to be that exact value:
| Key | Value | Why |
|---|---|---|
ANTHROPIC_API_KEY |
your key | the model the agents run on — skip it and use Ollama for free instead, see the comment above |
OPEN_WEBUI_URL |
http://localhost:8080 |
the backend, where tool calls actually land |
AGENT_AUTH_SECRET |
pLiUxoi+ziwTUIhNhVg/AN1U50UMom00 — same fixed value as backend/.env.example's WEBUI_SECRET_KEY |
Extra verifies Open WebUI's session JWT itself; HMAC means one shared secret signs and verifies on both sides |
AGENT_AUTH_MODE |
mint |
token-url points at Open WebUI's dedicated /auths/agent-chat/token, not the user's regular session — mint is for exactly that: a short-lived token minted specifically for us, with its lifetime capped (AGENT_AUTH_MAX_TTL_SECONDS, 1h default) rather than trusted at whatever the token claims |
AGENT_AUTH_CLAIM_USER_ID |
id |
Open WebUI's token carries the user id under id, not the usual sub |
AGENT_AUTH_CLAIM_ROLES |
role |
which claim plugins/access.py reads to decide who can reach the protected admin_management node |
CORS_ORIGINS |
http://localhost:5173 |
where the browser loads the page from, not where the backend answers — the two are different ports in dev |
AGENT_AUTH_SECRET/WEBUI_SECRET_KEY is a fixed, publicly-known dev value —
the same one ships in both .env.example files, on purpose. That's fine only
because this all runs on your machine with no real users and nothing else
trusts it; change both together to something private (openssl rand -base64 24) the moment this runs anywhere else reachable.
Get the rest wrong and the failure points back here: a mismatched
AGENT_AUTH_SECRET fails every request with a signature error, a wrong
CORS_ORIGINS shows up as a browser console error before any request lands
at all.
A sequence of commits, each a step in the same story: a naive assistant sharing one admin API key between every user, then made to act as the caller instead, then made to survive runs longer than a session token's lifetime, then made to hide the whole admin path from anyone who isn't actually an Open WebUI admin, and finally made to stop describing that path to people who cannot use it. Read them in order — git log extra/ — to see why each change was needed, not just what it does.
Those last two are the pair worth studying together, because either alone leaves the assistant inconsistent:
protected: true(agents.yml) plusplugins/access.pydecides what the router can reach. A node markedprotectedis invisible to the router, not just refused, unless the access plugin allows it.{{admin_routing}}in the router's prompt, filled byplugins/resolvers/openwebui.py, decides what the router is told exists. Without it the prompt named a destination a non-admin would never be offered, so the assistant announced admin help it could not deliver.
Both are general engine features, not specific to this example — see access control and resolvers. Both read the caller's role, and plugins/_identity.py is the one place that decides what "admin" means so the two can never drift apart.
backend/open_webui/routers/auths.py and the four files under src/ are the Open WebUI side of the wiring: one endpoint that mints a short-lived token for the signed-in user — carrying their role, which is what the access plugin checks — and the widget embed that calls it.