diff --git a/CHANGELOG.md b/CHANGELOG.md index c079d9b0..cf317f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,15 @@ ### Added -- Optional passkey/WebAuthn sign-in for password-protected WebUI instances. Authenticated users can register/remove passkeys from Settings -> System, switch to passkey-only/passwordless sign-in after at least one passkey exists, and `/login` shows a passwordless sign-in button only after a passkey exists. Password auth remains the default-off bootstrap and recovery path until users choose to remove it. +- Optional passkey/WebAuthn sign-in for password-protected WebUI instances. Authenticated users can register/remove passkeys from Settings -> System, and `/login` shows a passwordless sign-in button only after a passkey exists. Password auth remains the default-off bootstrap and recovery path. **Opt-in default-off behind `HERMES_WEBUI_PASSKEY=1` env var or `webui_passkey_enabled: true` config flag** — when disabled, the UI block hides, all 6 `/api/auth/passkey/*` endpoints return 404, and `is_auth_enabled()` ignores any pre-existing credential file so the auth posture cannot silently flip if the flag is unset later. + +- A "Stop server" affordance in Settings → System that gracefully shuts down the local WebUI server. Useful when WebUI was launched via `./ctl.sh start` or the native macOS/Windows app and the user wants to stop it without context-switching to a terminal. Confirmation dialog before the actual shutdown. The `/api/shutdown` route is CSRF-gated and intended for local-loopback use. Originally a title-bar button in #2824; relocated to Settings per the project's deep-UX rule (default-hidden for niche destructive actions on always-visible surfaces). ### Fixed -- Clarify `Response interrupted` recovery markers so they report that the live response stream stopped instead of asserting that the WebUI process restarted. The recovery path now records distinct interruption causes for real process restarts, stream/run split-brain, and lost worker bookkeeping; browser-side SSE transport failures show a separate `Connection interrupted` message, client-side `BrokenPipeError` disconnects no longer get logged as server 500s, and chat/gateway SSE errors emit rate-limited, body-capped, sanitized client diagnostics to `/api/client-events/log` for future root-cause checks. The stream-status `terminal_state` value for lost-worker bookkeeping changes from `stale-from-restart` to `lost-worker-bookkeeping`, matching the new non-restart wording. +- Prevent replayed context in chat reconciliation and metering. When a WebUI session is recovered (e.g., after a process restart, network drop, or browser reload), the sidebar/`state.db` reconciliation logic walks the sidecar transcript in order and only skips rows that can actually be aligned with the remaining sidecar context. The prior set-membership check was too broad: a legitimate fresh message that happened to share a key with any older repeated short message in the sidecar was mis-classified as already-seen and dropped from the replay, leading to lost context and inconsistent metering. Also caps the per-turn live-tool-prompt token estimate at 12,000 to prevent unbounded growth on bursts of large tool reads before exact provider accounting overrides. Fixes #2685. + +- Clarify `Response interrupted` recovery markers so they report that the live response stream stopped instead of asserting that the WebUI process restarted. The recovery path now records distinct interruption causes for real process restarts, stream/run split-brain, and lost worker bookkeeping; browser-side SSE transport failures show a separate `Connection interrupted` message, client-side `BrokenPipeError` disconnects no longer get logged as server 500s, and chat/gateway SSE errors emit rate-limited (30 events / 60s / 4KB body cap), sanitized client diagnostics to `/api/client-events/log` for future root-cause checks. The stream-status `terminal_state` value for lost-worker bookkeeping changes from `stale-from-restart` to `lost-worker-bookkeeping`, matching the new non-restart wording. ## [v0.51.131] — 2026-05-24 — Release DC (stage-batch13 — 6-PR notes-drawer + context-parity + PWA-swipe + locale polish) diff --git a/api/routes.py b/api/routes.py index 43c36dff..0b623ed8 100644 --- a/api/routes.py +++ b/api/routes.py @@ -3752,7 +3752,7 @@ def handle_get(handler, parsed) -> bool: return t(handler, _page, content_type="text/html; charset=utf-8") if parsed.path == "/api/auth/status": - from api.auth import get_password_hash, is_auth_enabled, parse_cookie, verify_session + from api.auth import _passkey_feature_flag_enabled, get_password_hash, is_auth_enabled, parse_cookie, verify_session from api.passkeys import registered_credentials logged_in = False @@ -3760,7 +3760,8 @@ def handle_get(handler, parsed) -> bool: if auth_enabled: cv = parse_cookie(handler) logged_in = bool(cv and verify_session(cv)) - passkeys = registered_credentials() + passkey_flag = _passkey_feature_flag_enabled() + passkeys = registered_credentials() if passkey_flag else [] password_auth_enabled = get_password_hash() is not None return j(handler, { "auth_enabled": auth_enabled, @@ -3769,6 +3770,7 @@ def handle_get(handler, parsed) -> bool: "passwordless_enabled": bool(passkeys) and not password_auth_enabled, "passkeys_enabled": bool(passkeys), "passkeys_count": len(passkeys), + "passkey_feature_flag": passkey_flag, }) if parsed.path in ("/manifest.json", "/manifest.webmanifest"): @@ -5897,8 +5899,11 @@ def handle_post(handler, parsed) -> bool: 409, ) if requested_passwordless: + from api.auth import _passkey_feature_flag_enabled from api.passkeys import registered_credentials + if not _passkey_feature_flag_enabled(): + return bad(handler, "Passkey support is disabled. Enable HERMES_WEBUI_PASSKEY before going passwordless.", 409) if not registered_credentials(): return bad(handler, "Register a passkey before going passwordless.", 409) elif requested_clear_password: diff --git a/static/index.html b/static/index.html index 3ac56ce0..b3d5940a 100644 --- a/static/index.html +++ b/static/index.html @@ -1267,7 +1267,7 @@
./ctl.sh start and want to stop without switching to a terminal. You'll need to relaunch the server (./ctl.sh start or the native app) before the WebUI is reachable again.