_gateway_root_pid_path() unconditionally returned <hermes_root>/gateway.pid.
Profile-scoped gateways (started with --profile <name> or via active_profile)
write their runtime files under <hermes_root>/profiles/<name>/ instead of the
root, so the root-level path never existed.
build_agent_health_payload() therefore always received a non-existent pid_path,
fell through to the stale root-level gateway_state.json, and returned alive=None.
This caused the cron/scheduled-jobs page to display "Gateway not configured" even
when a gateway was actively running.
Fix: after failing to find a root-level gateway.pid, fall back to the active
profile directory via get_active_hermes_home(). Root-level wins when it exists,
so deployments that do write there are unaffected. Errors from profile lookup are
swallowed and the root path is returned, preserving the previous safe default.
Adds five focused unit tests covering the new fallback, the priority rule, and
the error-handling path.
Inline fixes for 4 of 5 Opus SHOULD-FIX items before tag:
1. /api/auth/status now gates passkeys_enabled / passwordless_enabled on
_passkey_feature_flag_enabled() — when flag is off, status reports
no credentials even if passkeys.json has legacy entries. New
passkey_feature_flag field added to the response for the frontend.
2. Settings → System Passkeys block (passkeysSettingsBlock) now starts
display:none and loadPasskeys() reveals it only when the server
confirms passkey_feature_flag === true AND /api/auth/passkeys
doesn't return {disabled: true}. Stops the broken-affordance trap
where users would see Add passkey → click → 404.
3. /api/settings/save now refuses to set passwordless mode when the
passkey feature flag is off. Closes the auth-bypass path Opus flagged:
user goes passwordless while flag on → admin unsets flag → restart
serves the WebUI fully unauthenticated.
4. CHANGELOG entries added for PR #2685 (replayed-context dedup +
per-turn metering cap) and PR #2824 (Stop server affordance,
relocated to Settings) — both PRs had functional changes but no
release-notes entries. Also enriched the rate-limit detail on the
#2739 entry (30 events / 60s / 4KB body cap).
Deferred to follow-up issue (#5 in Opus review):
- Live tool metering cumulative cap across many tool calls — non-trivial
refactor of _bump_live_prompt_estimate, will be a separate PR
Adds the 7 shutdown-related i18n keys to all 10 non-en/tr locales
(it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr) with proper translations.
Resolves test_*_locale.py::test_*_locale_covers_english_keys failures
that were caught by full sequential pytest. Locale parity is enforced
because untranslated keys would surface in non-en deployments as
English fallback text in the Stop Server affordance.
Italian + Portuguese translations use \' to escape apostrophes inside
the single-quoted JS string literals.
test_passwordless_mode_keeps_auth_enabled_with_passkeys now sets
HERMES_WEBUI_PASSKEY=1 via monkeypatch since are_passkeys_enabled()
gates on the feature flag.
Adds 2 new tests:
- test_passkey_feature_flag_off_disables_passkeys_even_with_credentials
- test_passkey_feature_flag_via_config
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).