- Privacy diagnostics: the provider name is None for unknown endpoints
and when no base URL is set; calling .lower() on it raised and dropped
every issue of the check, including the warning for privacy_mode=none
with an unknown remote endpoint.
- Telemetry: the pattern allowlist built a PatternLoader without its
required directories, so it was always empty and every pattern label
became __unknown__. It now reads ids from the shared loader through a
new PatternLoader.get_pattern_ids().
- Error summary: the LLM context builder read the category from a nested
pattern_match key that the pattern stage never writes; it now reads the
top-level category (nested key kept as a fallback).
- Sidebar: an error delivered before the sidebar panel existed (it waits
for UI text) threw on auto-open, and so did the menu toggle. Visibility
is now applied when the panel is created.
- OpenAPI info.version now matches the package version, and the version
metadata test enforces it.
- A missing optional patterns/custom directory is logged at debug level
instead of a warning; missing built-in directories still warn.
An encrypted server-side credential store that could not be opened was
destroyed on the next access: with the encryption key missing, the next
save rewrote it as plaintext; with a different key, the failed MAC check
renamed it aside as corrupt and a new store was started under the new key.
The store now classifies its state as ok, empty, locked_key_required,
locked_key_mismatch or corrupt. A locked store is never rewritten,
downgraded or moved: reads return nothing, writes raise
SecretStoreLockedError, and the locked state is logged once per change
without key material. Only structurally unreadable files are still
rotated aside.
GET /doctor/secrets/status reports store.state, and PUT /doctor/secrets
and DELETE /doctor/secrets/{provider} return HTTP 409 with error code
secret_store_locked. The OpenAPI contract and the configuration security
guide document the states and the recovery steps.
The analyze, chat, verify-key and model-list routes sent server-side
requests to a caller-supplied base URL guarded only by a pre-flight
denylist. Callers could choose arbitrary hosts and paths, reach shared
address space and metadata ranges, drive local LLM write APIs, and use
DNS rebinding to deliver server-held keys to loopback services.
Requests now select an entry of an immutable destination registry built
at startup from the built-in provider catalog, local Ollama/LM Studio,
and operator environment variables (DOCTOR_LLM_ALLOWED_BASE_URLS and the
existing base URL variables). Outbound URLs are built from the approved
entry, resolved addresses are checked at connect time inside the shared
connector, redirects are never followed or treated as success, and error
responses no longer reflect upstream bodies or network details. The UI
shows localized operator guidance, a static rule enforces disabled
redirects and policy-managed sessions, and .comfyignore keeps tests and
tooling out of the published archive.