Per the stage-batch14 ship plan, passkey/WebAuthn support is shipped
opt-in default-off behind an explicit feature flag so deployments can
disable the entire surface (UI + endpoints + credential storage) without
needing to delete code.
Enable via either:
- HERMES_WEBUI_PASSKEY=1 environment variable, OR
- webui_passkey_enabled: true in config.yaml
With the flag off:
- are_passkeys_enabled() returns False even if credentials exist
- is_auth_enabled() falls back to password-only checking
- /login renders password-only (no passkey button)
- All 6 /api/auth/passkey/* endpoints return 404 with a clear message
- Settings → System → Passkeys section is hidden
Mirrors the #2527 notes-drawer flag shape (env-or-config, truthy parse).
Auth is high-stakes; opt-in lets us land the code while keeping default
deployments on the well-tested password-only path.
Touches: api/auth.py (new _passkey_feature_flag_enabled helper, gated
are_passkeys_enabled), api/routes.py (6 endpoint guards).
Per project deep-UX standards (default-hidden for niche destructive
actions). The title bar is shared real estate where always-visible
chrome competes with the title text and reload button — adding a
prominent destructive button there fails the 'kid clicks it' test even
with a confirmation modal. Moved to Settings → System where the user
who actively wants to stop the server can still find it, while everyone
else doesn't have to look at it.
Changes:
- Removed app-titlebar-shutdown button from <header> in index.html
- Removed dead .app-titlebar-shutdown CSS rule
- Added Settings → System → Stop server affordance (label + description + button)
- shutdownServer() and _showServerStopped() now use i18n keys
- Added 8 new locale keys to en + tr blocks (settings_label_shutdown,
settings_desc_shutdown, settings_btn_shutdown, settings_shutdown_confirm_*,
settings_shutdown_stopped_message). Other 9 locales fall back to English
via the existing locale fallthrough — follow-up issue tracked separately.
Preserves all of gavinssr's backend work (/api/shutdown route after CSRF
gate, BroadcastChannel for multi-tab signaling, app dialog with danger
styling) — only the placement is changed.
Move POST /api/shutdown routing after the CSRF check so drive-by
cross-origin requests cannot bring down a dev server with auth off.
Also replace os._exit(0) with os.kill(os.getpid(), signal.SIGINT)
so atexit handlers and pending session writes run during shutdown.
Add a power button (⏻) in the title bar that gracefully stops the
WebUI server process from the browser.
- api/routes.py: POST /api/shutdown endpoint with threaded os._exit(0)
- static/boot.js: shutdownServer() with confirm prompt, BroadcastChannel
cross-tab notification, and _showServerStopped() placeholder UI
- static/index.html: shutdown button HTML in title bar (after reload btn)
- static/style.css: .app-titlebar-shutdown styles, hover turns red
The contributor used pr-artifacts/ as a working scratchpad during PR
development. The real test count and failure-mode docs are already
covered by inline test comments and CHANGELOG entries; this directory
adds nothing for upstream readers.
Opus Advisor verdict: SHIP-AS-IS. Zero MUST-FIX, three SHOULD-FIX
filed as follow-up issues:
- Notes drawer: 10 non-en locales ship English fallback (default-off so user impact = 0)
- _joplin_api_get URL-token defense-in-depth (move to Authorization header)
- prefill_messages setattr cache-reuse safety on older agent builds
6,503 pytest passed (sequential mode — xdist not supported by test infra).
The cherry-pick of #2882 brought in an accidental two-space indent on a
zh-TW key. Restored the existing two-space indentation level so the
zh-CN clarification stays the only behavioural change.
PR #2882 was based on stale master (66de2367, pre-stage-batch7); naive
merge would delete 5,627 lines of subsequent work. Extracted the actual
zh-CN diff and applied it on top of fresh stage.
Co-authored-by: john <yuanchangjun@gmail.com>
Opus pre-release advisor caught a #2762 parity gap. api/streaming.py:5078
(_run_agent_streaming worker, background thread) correctly passes
profile= to sync_session_usage post-#2827. But the SECOND production
call site at api/routes.py:9007 (_handle_chat_sync, HTTP thread) does
not. Safe TODAY (HTTP thread sets TLS correctly), but it's a
defense-in-depth gap: anyone wrapping that handler in a worker pool
later silently regresses the fix. Closes the parity gap so the
threat-model invariant holds regardless of future threading changes.
My earlier conflict resolution between #2716 master and #2726 PR
dropped the 'const sessionModelState=...' assignment that the
.then() callback body uses on 6 different lines (1596, 1600, 1601,
1607, 1608, 1610). Without it boot.js would ReferenceError on every
boot. Caught by tests/test_new_chat_default_model_frontend.py::test_boot_model_hydration_prefers_active_session_over_persisted_model
which I'd missed in the initial touched-tests gate. Adds the
assignment back at the top of the .then() callback — semantically
matches the original #2716 master shape (S.session.model → wrap in
{model,model_provider} object, else null).
- Patch tests/test_issue2762_state_sync_profile_kwarg.py::_read_session
helper to query the real state.db schema (sessions.id PRIMARY KEY,
not sessions.session_id). Was always broken — the test never matched
any actual schema. Fix: SELECT id AS session_id + WHERE id = ?
- Patch tests/test_session_metadata_fast_path.py::test_failed_boot_model_catalog_prime_is_retryable
to accept both populateModelDropdown() and populateModelDropdown({preferProfileDefaultOnFreshBoot:true})
signatures (sibling-collision with #2726).
- Patch tests/test_model_default_boot_precedence.py::test_boot_model_dropdown_explicitly_requests_profile_default_precedence
to accept either the original allowBootSavedModelOverride variable
name OR the post-#2716-cherry-pick stateToApply equivalent
(!window._defaultModel?savedState:null gate).
- Stamp CHANGELOG for v0.51.130 (Release DB).
Cherry-picked via 3-way apply onto stage HEAD.
Resolved workspace.js conflict: kept master's #2716 sessionId-capture
stale-session guard (closure-scoped sessionId check after await), AND
added PR's renderSessionArtifacts() call to refresh the new Artifacts
tab when the file tree updates. Wrapped in typeof check for defense.
Co-authored-by: AJV20 <abdielvc@me.com>
Cherry-picked via 3-way apply onto stage HEAD (post-Release-A/B/C1).
Resolved boot.js conflict: took PR's parameterized
populateModelDropdown({preferProfileDefaultOnFreshBoot:true}) call
(the whole point of #2726) on top of master's #2716 boot path.
Co-authored-by: starship-s <starship-s@github.users.noreply.github.com>