Node-health statistics read node_info and error_type, but stored
history entries carry node_context and pattern metadata instead, so
node_health was always empty outside of tests. The node class now comes
from node_context (node_class, then node_name), with the old node_info
shape still accepted, and counts stay weighted by repeat_count.
The last_error label is the matched pattern id or category ("unknown"
otherwise); raw error text, paths and node ids never enter the
statistics payload.
- 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 chat route built its estimator settings with a keyword the settings
type does not have, so every budgeted chat raised a TypeError that the
fail-open handler logged and ignored; the token budget never trimmed a
chat request. The settings now use the correct field, the budget service
estimates with the per-request estimator settings instead of always
using its construction-time estimator, and a skipped budget is logged
with a stable code and the exception type only.
Oversized contexts are now trimmed along the configured ladder before
the prompt is built, budget metadata is returned for streaming and
non-streaming chat, and the existing remote/local switches still turn
the budget off.
Server-held credentials were chosen from the caller's provider hint, so an
approved destination could receive another provider's key, local services
received provider keys, and connectivity-only gateways received the generic
key. Release is now decided from the matched destination only:
- official providers get their own key, then the generic key;
- the DOCTOR_LLM_BASE_URL endpoint gets the generic key only;
- DOCTOR_LLM_ALLOWED_BASE_URLS endpoints and local services get none;
- request keys are unchanged, and a hint cannot make an official
provider keyless.
Provider detection uses exact hosts instead of substring matching.
- Let an inherited proxy on an unregistered host:port (e.g. localhost:7890)
through the connect-time resolver; registered host:port pairs stay checked.
- Accept underscores inside host labels so Docker service names such as
my_ollama can be approved.
- Refuse IPv4-compatible and site-local IPv6 for official providers and
judge NAT64/6to4/compatible forms by the IPv4 address they carry.
- Fix approved destinations when routes are registered instead of on the
first request.
- Static outbound check: exact path matching, import-alias and
TCPConnector detection, policy factory wiring, raw HTTP client imports,
no nosec for policy rules, fail closed on unparseable files.
- Settings: restore the model controls when the base URL is empty.
ResolveResult only exists in aiohttp 3.10+, but the node uses the
host-bundled aiohttp. Import it for type checking only so hosts that still
ship aiohttp 3.9 load the node and keep the connect-time destination check.
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.
Refresh exact frontend lanes and relocated host contracts while preserving
Desktop settings behavior. Recognize first-party Switch nodes and optional
branches without suppressing required-input or broken-link diagnostics.
Revalidate canvas, workflow and target identity across focus navigation;
cancel stale requests on newer actions and sidebar teardown. Keep legacy
positioning and nested graph traversal covered by action-level regressions.
Validation: 75 source contracts, 84 frontend unit tests, Windows full gate
with 871 backend tests and 125 subtests, and 149 mocked browser E2E tests.
Seven existing backend skips remain. No live-host certification is claimed.