Skip to content

Commit 928dcfc

Browse files
committed
Merge remote-tracking branch 'origin/release/v0.5.29'
2 parents 5907bc4 + 3d8bbf9 commit 928dcfc

8 files changed

Lines changed: 77 additions & 10 deletions

File tree

‎.env.example‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ WORKER_SLOT_COUNT=8
9696
SEED_DEMO_AUDITS=true
9797

9898
# --- Chat (Open WebUI) ---------------------------------------------------------
99+
# Required. Open WebUI's secret; it encrypts stored provider keys. Generate once with
100+
# `openssl rand -hex 32` and keep it: changing it makes stored keys unreadable.
101+
WEBUI_SECRET_KEY=
99102
# Single-origin model: Open WebUI (the subpath fork) serves its whole app at
100103
# /chat/ on Studio's own origin, and Studio's wrapper page (model-picker bar +
101104
# iframe) lives at /ai/. Caddy is the only published port and does the

‎chat/api.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ class ChatAPIError(RuntimeError):
4545
"""Open WebUI refused or could not be reached."""
4646

4747

48+
def service_token() -> str | None:
49+
"""A token for Studio's service account (admin), from the trusted-header sign-in.
50+
51+
Open WebUI takes the role from the trusted headers on each sign-in, so the
52+
account is admin here by design: the runner calls models and agents as admin.
53+
None when the chat is unreachable; callers treat that as "no key".
54+
"""
55+
identity = {
56+
chat.EMAIL_HEADER: chat.SERVICE_EMAIL,
57+
chat.NAME_HEADER: chat.SERVICE_NAME,
58+
chat.ROLE_HEADER: "admin",
59+
}
60+
try:
61+
return (ChatAPI(identity).sign_in() or {}).get("token") or None
62+
except Exception:
63+
logger.info("Service sign-in to Open WebUI failed", exc_info=True)
64+
return None
65+
66+
4867
class ChatAPI:
4968
"""An Open WebUI session for one Studio user.
5069

‎chat/config.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ def is_disabled(value: str | None) -> bool:
9595
EMAIL_HEADER = "X-Studio-Email"
9696
NAME_HEADER = "X-Studio-Name"
9797
ROLE_HEADER = "X-Studio-Role"
98+
99+
#: Studio's own Open WebUI account: the runner signs in as it (admin, via the
100+
#: trusted headers) to call agents and models on anyone's behalf. Not a secret.
101+
SERVICE_EMAIL = os.environ.get("SIMPLEAUDIT_CHAT_SERVICE_EMAIL", "studio-audit@studio.local")
102+
SERVICE_NAME = "Studio audit"
98103
#: Every header the proxy injects — it must strip all of them off the incoming
99104
#: request before adding its own, or a client could forge them.
100105
TRUSTED_HEADERS = (EMAIL_HEADER, NAME_HEADER, ROLE_HEADER)

‎chat/proxy.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -712,12 +712,17 @@ def _locate_owui_interpreter(home: Path) -> str:
712712

713713

714714
def _secret_key(home: Path) -> str:
715-
"""Open WebUI's signing key — generated once, persisted.
715+
"""Open WebUI's signing key: the shared ``WEBUI_SECRET_KEY`` if set, else one generated once and persisted.
716716
717-
A direct uvicorn launch has no `serve` to persist the key, and a fresh
718-
random key on every start would sign every session out; the file plays
719-
the role ``.webui_secret_key`` serves for `open-webui serve`.
717+
A shared key (set in `.env` for `manage.py dev`, or exported for `spin`) lets
718+
several installs read the same stored provider keys. Without it, a direct
719+
uvicorn launch has no `serve` to persist the key, and a fresh random key on
720+
every start would sign every session out; the file plays the role
721+
``.webui_secret_key`` serves for `open-webui serve`.
720722
"""
723+
shared = os.environ.get("WEBUI_SECRET_KEY", "").strip()
724+
if shared:
725+
return shared
721726
key_file = home / "webui_secret_key"
722727
try:
723728
if key_file.exists():

‎chat/tests/test_proxy.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,19 @@ def test_stale_wheel_triggers_rebuild(self):
404404
managed = Path(tmp)
405405
proxy._wheel_marker(managed).write_text("https://example.com/old.whl")
406406
self.assertFalse(proxy._managed_venv_matches_wheel(managed))
407+
408+
409+
class SecretKeyTests(SimpleTestCase):
410+
def test_a_shared_env_key_wins_and_writes_no_file(self):
411+
with tempfile.TemporaryDirectory() as tmp, patch.dict(os.environ, {"WEBUI_SECRET_KEY": "shared-key"}):
412+
home = Path(tmp)
413+
self.assertEqual(proxy._secret_key(home), "shared-key")
414+
self.assertFalse((home / "webui_secret_key").exists())
415+
416+
def test_without_env_the_key_is_generated_once_and_reused(self):
417+
with tempfile.TemporaryDirectory() as tmp, patch.dict(os.environ, {}, clear=False):
418+
os.environ.pop("WEBUI_SECRET_KEY", None)
419+
home = Path(tmp)
420+
first = proxy._secret_key(home)
421+
self.assertEqual(proxy._secret_key(home), first)
422+
self.assertTrue((home / "webui_secret_key").exists())

‎docker-compose.yml‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ services:
9696
dockerfile: deploy/compose/Dockerfile
9797
env_file: .env
9898
environment:
99+
# The agent connection's key is signed in against Open WebUI's internal address.
100+
SIMPLEAUDIT_CHAT_UPSTREAM: http://open-webui:8080/chat
99101
POSTGRES_HOST: postgres
100102
HATCHET_SERVER_URL: http://hatchet-server:8888
101103
HATCHET_GRPC_URL: hatchet-server:7077
@@ -194,6 +196,9 @@ services:
194196
WEBUI_AUTH_TRUSTED_NAME_HEADER: X-Studio-Name
195197
WEBUI_AUTH_TRUSTED_ROLE_HEADER: X-Studio-Role
196198
ENABLE_SIGNUP: "false"
199+
# Required. Open WebUI encrypts stored provider keys with this secret; if it
200+
# changes (for example, a generated one lost on recreate) those keys stop working.
201+
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:?set WEBUI_SECRET_KEY in .env (e.g. openssl rand -hex 32)}
197202
# Retrieval defaults Studio always wants (hybrid search, cached queries, KB exec).
198203
ENABLE_RAG_HYBRID_SEARCH: ${ENABLE_RAG_HYBRID_SEARCH:-true}
199204
ENABLE_QUERIES_CACHE: ${ENABLE_QUERIES_CACHE:-true}
@@ -208,14 +213,14 @@ services:
208213
# HOST/PORT and launches uvicorn itself.
209214
HOST: "0.0.0.0"
210215
PORT: ${SIMPLEAUDIT_CHAT_UPSTREAM_PORT:-8080}
211-
# Optional: export Open WebUI's spans to Studio's OTLP listener. Off by
212-
# default — set SIMPLEAUDIT_CHAT_OTLP=true to enable. Open WebUI passes
216+
# Export Open WebUI's spans to Studio's OTLP listener. On by default —
217+
# set SIMPLEAUDIT_CHAT_OTLP=false to opt out. Open WebUI passes
213218
# OTEL_EXPORTER_OTLP_ENDPOINT to its HTTP exporter as-is, so this must be
214219
# the full Studio ingestion URL. Exports
215220
# unauthenticated by default (matches an enabled "none" credential); for a
216221
# basic/bearer credential set OTEL_BASIC_AUTH_USERNAME / OTEL_BASIC_AUTH_PASSWORD.
217-
ENABLE_OTEL: ${SIMPLEAUDIT_CHAT_OTLP:-false}
218-
ENABLE_OTEL_TRACES: ${SIMPLEAUDIT_CHAT_OTLP:-false}
222+
ENABLE_OTEL: ${SIMPLEAUDIT_CHAT_OTLP:-true}
223+
ENABLE_OTEL_TRACES: ${SIMPLEAUDIT_CHAT_OTLP:-true}
219224
OTEL_OTLP_SPAN_EXPORTER: "http"
220225
OTEL_EXPORTER_OTLP_ENDPOINT: ${SIMPLEAUDIT_CHAT_OTLP_ENDPOINT:-http://web:8000/otlp/v1/traces}
221226
OTEL_SERVICE_NAME: ${SIMPLEAUDIT_CHAT_OTLP_SERVICE_NAME:-open-webui}

‎docs/chat.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ unset: the iframe loads the same-origin `/chat/`.
233233
| `ENABLE_QUERIES_CACHE` | `true` | cache retrieval queries; on by default |
234234
| `ENABLE_KB_EXEC` | `true` | filesystem-style knowledge exec for agentic RAG; on by default |
235235
| `ENABLE_VERSION_UPDATE_CHECK` | `false` | Open WebUI's "new version available" banner; off by default because Studio pins the build. Set `true` to show it |
236+
| `WEBUI_SECRET_KEY` | required, no default | Open WebUI's secret; encrypts stored provider keys. Keep it stable: a new value makes stored keys unreadable |
236237

237238
## Exporting Open WebUI's spans to Studio (OTLP)
238239

‎model_registry/services.py‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,25 @@ def is_internal_agent_connection(connection) -> bool:
3232

3333

3434
def connection_api_key(conn) -> str:
35-
"""The connection's API key: the stored key first, else the env var it names."""
35+
"""The connection's API key: the stored key, else the env var it names.
36+
37+
A connection to Studio's own Open WebUI has neither: it uses the service
38+
account's admin token, signed in fresh through the trusted headers.
39+
"""
3640
direct = (conn.api_key_direct or "").strip()
3741
if direct:
3842
return direct
3943
ref = (conn.secret_reference or "").strip()
40-
return os.environ.get(ref, "") if ref else ""
44+
if ref:
45+
return os.environ.get(ref, "")
46+
from chat import config as chat
47+
48+
base = (conn.base_url or "").strip().rstrip("/")
49+
if chat.ENABLED and base.startswith(chat.UPSTREAM.rstrip("/")):
50+
from chat.api import service_token
51+
52+
return service_token() or ""
53+
return ""
4154

4255

4356
def models_url(conn) -> str:

0 commit comments

Comments
 (0)