Skip to content

fix(settings): route seven editable keys to the service that reads them - #2517

Open
Hoang130203 wants to merge 2 commits into
Osmantic:mainfrom
Hoang130203:fix/settings-apply-routing-gaps
Open

fix(settings): route seven editable keys to the service that reads them#2517
Hoang130203 wants to merge 2 commits into
Osmantic:mainfrom
Hoang130203:fix/settings-apply-routing-gaps

Conversation

@Hoang130203

@Hoang130203 Hoang130203 commented Aug 7, 2026

Copy link
Copy Markdown

Summary

settings._match_apply_service() maps a changed .env key to the service that
needs recreating. A key it does not recognise falls through to the manual
bucket, and the dashboard tells the user "A manual stack restart is still
required for: …"
.

I cross-checked every documented, editable key against the ${VAR} references
in each service's compose block. Seven keys that exactly one service reads
were falling through:

Key Reader Why it was missed
OPEN_WEBUI_LLM_BASE_URL open-webui OPEN_WEBUI_* does not start with the WEBUI_ prefix the rule checks
OPEN_WEBUI_LLM_API_KEY open-webui same
TARGET_API_KEY privacy-shield TARGET_API_URL is in the set; this one is not
PII_CACHE_SIZE privacy-shield PII_CACHE_ENABLED is in the set; this one is not
PII_CACHE_TTL privacy-shield same
BRAVE_SEARCH_API_KEY brave-search no rule for this service at all
BRAVE_SEARCH_PORT brave-search same

The privacy-shield rows are the clearest evidence this is oversight rather than
intent: TARGET_API_URL and PII_CACHE_ENABLED are routed, while the keys
sitting next to them in .env are not.

The effect is downtime, not data loss. Changing the open-webui upstream or a
privacy-shield cache setting made the dashboard ask for a full stack restart —
every service down — when recreating one container was enough.

I want to be accurate about what this is not: the change was never silently
dropped. The apply plan does tell you a manual restart is needed, so nothing was
left in an inconsistent state. This is a worse-than-necessary remedy, not a
missing one.

Fix

  • key.startswith(("WEBUI_", "OPEN_WEBUI_")) instead of "WEBUI_" alone.
  • The three missing privacy-shield keys added to _PRIVACY_SHIELD_APPLY_KEYS.
  • New _BRAVE_SEARCH_APPLY_KEYS, with brave-search added to
    _SETTINGS_APPLY_ALLOWED_SERVICES.

The correction in the second commit is the interesting part

Routing a key is only half the job — _compute_env_apply_plan schedules a
service only if it is also in _SETTINGS_APPLY_ALLOWED_SERVICES. My first
pass routed brave-search and tailscale keys without checking that gate, so
five of the ten I originally touched changed nothing at all:

  BRAVE_SEARCH_API_KEY  services=[]  "A manual stack restart is still required..."
  TS_AUTHKEY            services=[]  "A manual stack restart is still required..."

brave-search now joins the allowed set — it is an ordinary optional HTTP
service, the same class as searxng and perplexica, which are already there.

The TS_* routing is removed rather than allowed. Recreating the tailscale
container can sever the very tailnet connection the operator is using to reach
the dashboard, so promoting it from "restart the stack yourself" to "we will
restart it for you" is your call, not a drive-by. There is a test asserting
TS_AUTHKEY still lands in the manual bucket on purpose.

The tests now assert the end-to-end apply plan for every routed key, not
just _match_apply_service — checking only the router is exactly what let the
gate slip past me.

AI Assistance

AI assisted with drafting the tests and this description. I built the inventory
by parsing every service's compose block for ${VAR} references and running
each key through the shipped _match_apply_service; I caught the allowed-services
gate by re-running the full apply plan rather than trusting the router alone.

Release Lane

  • Stable hotfix targeting release/2.6.x
  • Mainline change targeting main
  • Next-minor work targeting the next feature/minor release
  • Not sure; reviewer should help classify

Stable hotfix reason:

n/a

Changed Surface

  • Docs only
  • Tests only
  • Dashboard UI
  • Dashboard API / host agent
  • Installer / bootstrap / lifecycle
  • Docker Compose / service manifests
  • Model routing / Hermes / capabilities
  • Network exposure / auth / proxy
  • Dependencies / runtime wiring

(Two key sets, one prefix tuple and one entry in the allowed-services frozenset
in settings.py, plus tests.)

Risk And Validation

  • Risk level: Low
  • Validation run:
    • git diff --check
    • Markdown/link sanity for docs
    • Focused tests listed below
    • Dashboard lint/test/build
    • Extension audit / compose validation
    • Release-grade fleet or scoped hardware validation
    • Stable-lane patch validation, if targeting release/2.6.x

Commands/results:

$ python3 -m py_compile extensions/services/dashboard-api/settings.py
py OK

$ python3 -m pytest tests/test_settings_env.py -q
109 passed

# end-to-end apply plan for each routed key:
  OPEN_WEBUI_LLM_BASE_URL  services=['open-webui']
  OPEN_WEBUI_LLM_API_KEY   services=['open-webui']
  TARGET_API_KEY           services=['privacy-shield']
  PII_CACHE_SIZE           services=['privacy-shield']
  PII_CACHE_TTL            services=['privacy-shield']
  BRAVE_SEARCH_API_KEY     services=['brave-search']
  BRAVE_SEARCH_PORT        services=['brave-search']
  TS_AUTHKEY               services=[]              (deliberate)

# the new assertions against origin/main's settings.py:
11 failed, 20 passed

The assertions that pass either way are the deliberate ones: fifteen existing
routes (WEBUI_PORT, CTX_SIZE, LLAMA_THREADS, SEARXNG_URL, QDRANT_PORT,
APE_STRICT_MODE, …) asserted unchanged, plus _MANUAL_RESTART_KEYS and the
*_PUBLIC_URL family asserted to stay unrouted. Those are there so this does
not quietly grow into routing things that genuinely need a manual restart.

Operational Change Check

_match_apply_service() runs when the dashboard saves settings, to build the
apply plan. The change moves seven keys from "restart the whole stack" to
"recreate one container", and adds one service to the set the dashboard is
permitted to recreate. No key moves the other way, and nothing already routed
changes — the fifteen unchanged-route assertions pin that. No write path or
schema change.

  • This is not an operational change.
  • This is an operational change and validation is recorded above.
  • This is an operational change and validation is intentionally deferred for:

Notes For Reviewers

The inventory found more than I fixed, and the rest needs a decision I should
not make alone.
_match_apply_service returns a single service, but some
keys are read by several:

  BIND_ADDRESS        18 services
  TIMEZONE            hermes, n8n, open-webui
  UID / GID           hermes, n8n, privacy-shield
  EMBEDDING_MODEL     embeddings, open-webui
  ODS_DEVICE_NAME     dashboard-api, ods-proxy, open-webui, tailscale
  LLM_API_URL         llama-server (routed) + open-webui (not)

LLM_API_URL is the one worth your attention: it is routed, to llama-server,
and returns before the open-webui branch — so changing it recreates llama-server
and leaves open-webui pointing at the old URL, with no manual-restart warning
either
. That is the one genuinely silent case I found, and unlike the rest of
this PR it needs _match_apply_service to return a set rather than one service,
which changes the apply plan and its summary text. Say the word and I will send
it separately.

BIND_ADDRESS and the port keys are already in _MANUAL_RESTART_KEYS, so they
are handled deliberately.

Tailscale. If you do want TS_* handled automatically, adding tailscale
to _SETTINGS_APPLY_ALLOWED_SERVICES and restoring a three-line key set is the
whole change — I just did not want to make that trade on your behalf.

Litellm provider keys (ANTHROPIC_API_KEY, TOGETHER_API_KEY,
MINIMAX_API_KEY) are also unrouted and read only by litellm in compose. I left
them out because they may be read elsewhere at runtime in cloud mode, and I
could not confirm that from compose alone. Easy to add if you know they are
litellm-only.

_match_apply_service() maps a changed .env key to the service that needs
recreating. A key it does not recognise falls through to the manual
bucket, and the dashboard tells the user to restart the whole stack.

Ten documented, editable keys that exactly one compose service reads were
falling through:

  OPEN_WEBUI_LLM_BASE_URL   open-webui   OPEN_WEBUI_* does not start with
  OPEN_WEBUI_LLM_API_KEY    open-webui   the WEBUI_ prefix the rule checks

  TARGET_API_KEY            privacy-shield   sibling of TARGET_API_URL,
  PII_CACHE_SIZE            privacy-shield   which is routed; sibling of
  PII_CACHE_TTL             privacy-shield   PII_CACHE_ENABLED, likewise

  BRAVE_SEARCH_API_KEY      brave-search   no rule for this service at all
  BRAVE_SEARCH_PORT         brave-search
  TS_AUTHKEY                tailscale      no rule for this service at all
  TS_HOSTNAME               tailscale
  TS_EXTRA_ARGS             tailscale

The privacy-shield ones are the clearest sign this is oversight rather
than intent: TARGET_API_URL and PII_CACHE_ENABLED are in the set while
the keys next to them in .env are not.

Effect is downtime, not data loss: changing a Tailscale auth key made the
dashboard ask for a full stack restart, taking every service down, when
recreating the tailscale container was enough.

Adds TestApplyServiceRouting (31 assertions): the ten newly routed keys,
fifteen existing routes asserted unchanged, and the keys that must stay
unrouted — _MANUAL_RESTART_KEYS and the *_PUBLIC_URL family — so this
does not quietly grow into routing things that genuinely need a manual
restart.
Correction to the previous commit. Routing a key is only half the job:
_compute_env_apply_plan schedules a service only when it is also in
_SETTINGS_APPLY_ALLOWED_SERVICES, and neither brave-search nor tailscale
was. Those five keys routed and then fell straight back to the manual
bucket, so the change did nothing for them.

  BRAVE_SEARCH_API_KEY  services=[]  "A manual stack restart is still..."
  TS_AUTHKEY            services=[]  "A manual stack restart is still..."

brave-search joins the allowed set. It is an ordinary optional HTTP
service, the same class as searxng and perplexica, which are already
there.

The TS_* routing is removed rather than allowed. Recreating the tailscale
container can sever the very tailnet connection the operator is using to
reach the dashboard, so promoting it from "restart the stack yourself" to
"we will restart it for you" is a call for a maintainer, not a drive-by.

Tests now assert the end-to-end plan for every routed key rather than
_match_apply_service alone, which is what let the gap through, plus that
TS_AUTHKEY still lands in the manual bucket on purpose.
@Hoang130203 Hoang130203 changed the title fix(settings): route ten editable keys to the service that reads them fix(settings): route seven editable keys to the service that reads them Aug 7, 2026
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