HERMES-GIELIE/api/onboarding.py

1136 lines
46 KiB
Python
Raw Normal View History

"""Hermes Web UI -- first-run onboarding helpers."""
from __future__ import annotations
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
import json
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) * security: fix bandit security issues (B310, B324) - Add usedforsecurity=False to MD5 hash in gateway_watcher.py - Add URL scheme validation to prevent file:// access in config.py - Add URL validation to bootstrap.py health check - Add nosec comments where runtime validation exists * fix: handle ConnectionResetError gracefully and add debug logging - Add QuietHTTPServer class to suppress noisy connection reset errors caused by clients disconnecting abruptly (fixes log spam from 'ConnectionResetError: [Errno 54] Connection reset by peer') - Replace silent 'pass' statements with logger.debug() calls across api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py, and api/onboarding.py for better observability during troubleshooting - All tests pass (25 passed in test_regressions.py) * chore: add debug logging to profiles and routes modules - Replace silent 'pass' statements with logger.debug() calls in api/profiles.py for better error visibility during profile switching and module patching - Add logger initialization to api/routes.py * security: fix B110 bare except/pass issues (bandit security scan) - Replace bare except/pass patterns with logger.debug() calls - Fixes CWE-703 (improper check/handling of exceptional conditions) - Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py - All tests pass successfully * security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) - api/gateway_watcher.py: MD5 usedforsecurity=False (B324) - api/config.py, bootstrap.py: URL scheme validation before urlopen (B310) - 12 files: replace bare except/pass with logger.debug() (B110) - server.py: QuietHTTPServer suppresses client disconnect log noise - server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail) - tests/test_sprint43.py: 19 new tests covering all security fixes - CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822) --------- Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
import logging
import os
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
import socket
import urllib.error
import urllib.request
from pathlib import Path
from urllib.parse import urlparse
from api.auth import is_auth_enabled
from api.config import (
DEFAULT_MODEL,
DEFAULT_WORKSPACE,
_FALLBACK_MODELS,
_HERMES_FOUND,
invalidate_models_cache,
_PROVIDER_DISPLAY,
_PROVIDER_MODELS,
_get_config_path,
get_available_models,
get_config,
load_settings,
reload_config,
save_settings,
verify_hermes_imports,
)
from api.providers import _write_env_file # shared impl with _ENV_LOCK (#1164)
from api.workspace import get_last_workspace, load_workspaces
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) * security: fix bandit security issues (B310, B324) - Add usedforsecurity=False to MD5 hash in gateway_watcher.py - Add URL scheme validation to prevent file:// access in config.py - Add URL validation to bootstrap.py health check - Add nosec comments where runtime validation exists * fix: handle ConnectionResetError gracefully and add debug logging - Add QuietHTTPServer class to suppress noisy connection reset errors caused by clients disconnecting abruptly (fixes log spam from 'ConnectionResetError: [Errno 54] Connection reset by peer') - Replace silent 'pass' statements with logger.debug() calls across api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py, and api/onboarding.py for better observability during troubleshooting - All tests pass (25 passed in test_regressions.py) * chore: add debug logging to profiles and routes modules - Replace silent 'pass' statements with logger.debug() calls in api/profiles.py for better error visibility during profile switching and module patching - Add logger initialization to api/routes.py * security: fix B110 bare except/pass issues (bandit security scan) - Replace bare except/pass patterns with logger.debug() calls - Fixes CWE-703 (improper check/handling of exceptional conditions) - Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py - All tests pass successfully * security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) - api/gateway_watcher.py: MD5 usedforsecurity=False (B324) - api/config.py, bootstrap.py: URL scheme validation before urlopen (B310) - 12 files: replace bare except/pass with logger.debug() (B110) - server.py: QuietHTTPServer suppresses client disconnect log noise - server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail) - tests/test_sprint43.py: 19 new tests covering all security fixes - CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822) --------- Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger = logging.getLogger(__name__)
_SUPPORTED_PROVIDER_SETUPS = {
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
# ── Easy start ──────────────────────────────────────────────────────
"openrouter": {
"label": "OpenRouter",
"env_var": "OPENROUTER_API_KEY",
"default_model": "anthropic/claude-sonnet-4.6",
"requires_base_url": False,
"models": [
{"id": model["id"], "label": model["label"]} for model in _FALLBACK_MODELS
],
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"category": "easy_start",
"quick": True,
},
"anthropic": {
"label": "Anthropic",
"env_var": "ANTHROPIC_API_KEY",
"default_model": "claude-sonnet-4.6",
"requires_base_url": False,
"models": list(_PROVIDER_MODELS.get("anthropic", [])),
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"category": "easy_start",
"oauth_provider": "anthropic",
"oauth_label": "Claude Code OAuth",
},
"openai": {
"label": "OpenAI",
"env_var": "OPENAI_API_KEY",
"default_model": "gpt-4o",
"default_base_url": "https://api.openai.com/v1",
"requires_base_url": False,
"models": list(_PROVIDER_MODELS.get("openai", [])),
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"category": "easy_start",
},
# ── Open / self-hosted ─────────────────────────────────────────────
"ollama": {
"label": "Ollama",
"env_var": "OLLAMA_API_KEY",
"default_model": "qwen3:32b",
"default_base_url": "http://localhost:11434/v1",
"requires_base_url": True,
fix(onboarding): allow keyless setup for self-hosted providers (#1499 third sub-bug) Pre-fix, the wizard rejected an empty api_key for every provider in _SUPPORTED_PROVIDER_SETUPS — including lmstudio, ollama, and custom, which run keyless on the vast majority of local installs. The agent's LMSTUDIO_NOAUTH_PLACEHOLDER substitution at chat-time was the workaround for the no-auth case, but the wizard side rejected the empty input first. Users had to type random gibberish into the API key field to clear the form — the third sub-bug from #1420 that the prior commit's PR description explicitly punted to a follow-up. Surfaced by Nathan during PR review: "I think it's too weird for users to have to type a string into the API key field, right?" Yes — and the probe (#1499) makes the cleanest fix strictly better: we accept empty keys, and the probe gives instant feedback ("Connected. 2 model(s) available." for keyless servers, "401" for auth-required servers). Backend changes --------------- * `api/onboarding.py` — `_SUPPORTED_PROVIDER_SETUPS` gains `key_optional: True` for `lmstudio`, `ollama`, `custom`. Cloud providers (openrouter, anthropic, openai, gemini, deepseek, …) remain key_required. * `apply_onboarding_setup` skips the "{env_var} is required" check when `key_optional` is set AND no key is supplied. No write to .env for the empty-key case (no `LM_API_KEY=*** placeholder lying in the user's .env`). * `_status_from_runtime` reports `provider_ready=True` for key_optional providers based on `requires_base_url` alone, so the wizard doesn't refire on the next page load just because there's no api_key. Cloud providers still need a key for provider_ready=True. * `_build_setup_catalog` exposes the `key_optional` flag to the frontend. Frontend changes ---------------- * `static/onboarding.js` — new `_renderOnboardingApiKeyField()` helper. For key_optional providers: - Label: "API key (optional)" - Placeholder: "Leave blank for keyless servers" - Inline italic muted help: "Most LM Studio / Ollama / vLLM installs run keyless — leave this blank if your server doesn't require authentication. Use the Test connection button to verify." For cloud providers: unchanged (label "API key", standard placeholder, no help block). * The api-key input also now triggers `_scheduleOnboardingProbe()` on oninput, so changing the key re-runs the probe — handles "the server rejected my empty key with 401, let me add one and retry." * `static/i18n.js` — 3 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8). * `static/style.css` — `.onboarding-api-key-help` rule for the muted italic helper paragraph. Verified end-to-end on port 8789 -------------------------------- Spun up an isolated test server + a mock LM Studio at `127.0.0.1:11234/v1/models`. Stepped through the wizard: * Picked LM Studio → field label flipped to "API key (optional)", placeholder showed "Leave blank for keyless servers", help text rendered in italic muted gray below. * Switched to Anthropic → label reverted to "API key", help text disappeared. Visual hierarchy correct. * Left api_key blank, set base_url to the mock, clicked Test connection → green "Connected. 2 model(s) available." banner. Probe-discovered models populated the workspace-step dropdown. * Continued through to the finish step. config.yaml written with provider/model/base_url. **`.env` does NOT exist** — no placeholder string written. `chat_ready: true`, `state: ready`. * Vision tool confirmed the visual hierarchy: subtle italic help reads as documentation, prominent green banner pops as status. Tests ----- `tests/test_issue1499_keyless_onboarding.py` — 16 tests in 3 classes: TestKeyOptionalProviderSchema (5) - lmstudio / ollama / custom declare key_optional=True - openrouter / anthropic / openai do NOT (regression defense) - setup catalog exposes the flag TestKeylessOnboarding (6) - lmstudio / ollama / custom: empty api_key accepted, no .env write - openrouter / anthropic: empty api_key still rejected - lmstudio with explicit key still writes .env (regression defense) TestKeylessChatReady (5) - lmstudio / ollama: provider_ready=True with no key - custom: provider_ready=True with key+base_url, False without base_url - openrouter: provider_ready=False with no key (regression defense) - End-to-end get_onboarding_status reports chat_ready=True Full suite: 3901 → 3917 passing (+16 from this commit; +22 cumulative from the PR's earlier commit). 0 failures. Closes #1499 (all three sub-bugs from #1420 now addressed)
2026-05-03 03:07:07 +00:00
# Local Ollama runs keyless by default — only Ollama Cloud requires
# OLLAMA_API_KEY. The wizard accepts an empty api_key for this
# provider; users with auth enabled can still type one. See #1499.
"key_optional": True,
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"models": [],
"category": "self_hosted",
},
"lmstudio": {
"label": "LM Studio",
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
# Canonical env var matches the agent CLI runtime (hermes_cli/auth.py:182,
# api_key_env_vars=("LM_API_KEY",)). Onboarding writes this name so the
# agent runtime actually picks up the key on the next chat — pre-#1499/#1500
# the WebUI wrote LMSTUDIO_API_KEY which the agent runtime ignored, masked
# in practice by the LMSTUDIO_NOAUTH_PLACEHOLDER fallback for keyless installs.
"env_var": "LM_API_KEY",
# Legacy env var written by older WebUI builds (≤ v0.50.272). Detection
# paths (_provider_api_key_present here, _provider_has_key in providers.py)
# also read this name so existing users with the old key in their .env
# don't flip to "no key" in Settings → Providers after upgrading.
# Onboarding only writes the canonical name going forward.
"env_var_aliases": ["LMSTUDIO_API_KEY"],
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"default_model": "gpt-4o-mini",
"default_base_url": "http://localhost:1234/v1",
"requires_base_url": True,
fix(onboarding): allow keyless setup for self-hosted providers (#1499 third sub-bug) Pre-fix, the wizard rejected an empty api_key for every provider in _SUPPORTED_PROVIDER_SETUPS — including lmstudio, ollama, and custom, which run keyless on the vast majority of local installs. The agent's LMSTUDIO_NOAUTH_PLACEHOLDER substitution at chat-time was the workaround for the no-auth case, but the wizard side rejected the empty input first. Users had to type random gibberish into the API key field to clear the form — the third sub-bug from #1420 that the prior commit's PR description explicitly punted to a follow-up. Surfaced by Nathan during PR review: "I think it's too weird for users to have to type a string into the API key field, right?" Yes — and the probe (#1499) makes the cleanest fix strictly better: we accept empty keys, and the probe gives instant feedback ("Connected. 2 model(s) available." for keyless servers, "401" for auth-required servers). Backend changes --------------- * `api/onboarding.py` — `_SUPPORTED_PROVIDER_SETUPS` gains `key_optional: True` for `lmstudio`, `ollama`, `custom`. Cloud providers (openrouter, anthropic, openai, gemini, deepseek, …) remain key_required. * `apply_onboarding_setup` skips the "{env_var} is required" check when `key_optional` is set AND no key is supplied. No write to .env for the empty-key case (no `LM_API_KEY=*** placeholder lying in the user's .env`). * `_status_from_runtime` reports `provider_ready=True` for key_optional providers based on `requires_base_url` alone, so the wizard doesn't refire on the next page load just because there's no api_key. Cloud providers still need a key for provider_ready=True. * `_build_setup_catalog` exposes the `key_optional` flag to the frontend. Frontend changes ---------------- * `static/onboarding.js` — new `_renderOnboardingApiKeyField()` helper. For key_optional providers: - Label: "API key (optional)" - Placeholder: "Leave blank for keyless servers" - Inline italic muted help: "Most LM Studio / Ollama / vLLM installs run keyless — leave this blank if your server doesn't require authentication. Use the Test connection button to verify." For cloud providers: unchanged (label "API key", standard placeholder, no help block). * The api-key input also now triggers `_scheduleOnboardingProbe()` on oninput, so changing the key re-runs the probe — handles "the server rejected my empty key with 401, let me add one and retry." * `static/i18n.js` — 3 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8). * `static/style.css` — `.onboarding-api-key-help` rule for the muted italic helper paragraph. Verified end-to-end on port 8789 -------------------------------- Spun up an isolated test server + a mock LM Studio at `127.0.0.1:11234/v1/models`. Stepped through the wizard: * Picked LM Studio → field label flipped to "API key (optional)", placeholder showed "Leave blank for keyless servers", help text rendered in italic muted gray below. * Switched to Anthropic → label reverted to "API key", help text disappeared. Visual hierarchy correct. * Left api_key blank, set base_url to the mock, clicked Test connection → green "Connected. 2 model(s) available." banner. Probe-discovered models populated the workspace-step dropdown. * Continued through to the finish step. config.yaml written with provider/model/base_url. **`.env` does NOT exist** — no placeholder string written. `chat_ready: true`, `state: ready`. * Vision tool confirmed the visual hierarchy: subtle italic help reads as documentation, prominent green banner pops as status. Tests ----- `tests/test_issue1499_keyless_onboarding.py` — 16 tests in 3 classes: TestKeyOptionalProviderSchema (5) - lmstudio / ollama / custom declare key_optional=True - openrouter / anthropic / openai do NOT (regression defense) - setup catalog exposes the flag TestKeylessOnboarding (6) - lmstudio / ollama / custom: empty api_key accepted, no .env write - openrouter / anthropic: empty api_key still rejected - lmstudio with explicit key still writes .env (regression defense) TestKeylessChatReady (5) - lmstudio / ollama: provider_ready=True with no key - custom: provider_ready=True with key+base_url, False without base_url - openrouter: provider_ready=False with no key (regression defense) - End-to-end get_onboarding_status reports chat_ready=True Full suite: 3901 → 3917 passing (+16 from this commit; +22 cumulative from the PR's earlier commit). 0 failures. Closes #1499 (all three sub-bugs from #1420 now addressed)
2026-05-03 03:07:07 +00:00
# Most LM Studio installs run keyless (LMSTUDIO_NOAUTH_PLACEHOLDER on the
# agent side handles this). The wizard accepts an empty api_key; auth-
# enabled servers still need one but the user types it in the same field.
# See #1499 (third sub-bug from #1420).
"key_optional": True,
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"models": [],
"category": "self_hosted",
},
"custom": {
"label": "Custom OpenAI-compatible",
"env_var": "OPENAI_API_KEY",
"default_model": "gpt-4o-mini",
"requires_base_url": True,
fix(onboarding): allow keyless setup for self-hosted providers (#1499 third sub-bug) Pre-fix, the wizard rejected an empty api_key for every provider in _SUPPORTED_PROVIDER_SETUPS — including lmstudio, ollama, and custom, which run keyless on the vast majority of local installs. The agent's LMSTUDIO_NOAUTH_PLACEHOLDER substitution at chat-time was the workaround for the no-auth case, but the wizard side rejected the empty input first. Users had to type random gibberish into the API key field to clear the form — the third sub-bug from #1420 that the prior commit's PR description explicitly punted to a follow-up. Surfaced by Nathan during PR review: "I think it's too weird for users to have to type a string into the API key field, right?" Yes — and the probe (#1499) makes the cleanest fix strictly better: we accept empty keys, and the probe gives instant feedback ("Connected. 2 model(s) available." for keyless servers, "401" for auth-required servers). Backend changes --------------- * `api/onboarding.py` — `_SUPPORTED_PROVIDER_SETUPS` gains `key_optional: True` for `lmstudio`, `ollama`, `custom`. Cloud providers (openrouter, anthropic, openai, gemini, deepseek, …) remain key_required. * `apply_onboarding_setup` skips the "{env_var} is required" check when `key_optional` is set AND no key is supplied. No write to .env for the empty-key case (no `LM_API_KEY=*** placeholder lying in the user's .env`). * `_status_from_runtime` reports `provider_ready=True` for key_optional providers based on `requires_base_url` alone, so the wizard doesn't refire on the next page load just because there's no api_key. Cloud providers still need a key for provider_ready=True. * `_build_setup_catalog` exposes the `key_optional` flag to the frontend. Frontend changes ---------------- * `static/onboarding.js` — new `_renderOnboardingApiKeyField()` helper. For key_optional providers: - Label: "API key (optional)" - Placeholder: "Leave blank for keyless servers" - Inline italic muted help: "Most LM Studio / Ollama / vLLM installs run keyless — leave this blank if your server doesn't require authentication. Use the Test connection button to verify." For cloud providers: unchanged (label "API key", standard placeholder, no help block). * The api-key input also now triggers `_scheduleOnboardingProbe()` on oninput, so changing the key re-runs the probe — handles "the server rejected my empty key with 401, let me add one and retry." * `static/i18n.js` — 3 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8). * `static/style.css` — `.onboarding-api-key-help` rule for the muted italic helper paragraph. Verified end-to-end on port 8789 -------------------------------- Spun up an isolated test server + a mock LM Studio at `127.0.0.1:11234/v1/models`. Stepped through the wizard: * Picked LM Studio → field label flipped to "API key (optional)", placeholder showed "Leave blank for keyless servers", help text rendered in italic muted gray below. * Switched to Anthropic → label reverted to "API key", help text disappeared. Visual hierarchy correct. * Left api_key blank, set base_url to the mock, clicked Test connection → green "Connected. 2 model(s) available." banner. Probe-discovered models populated the workspace-step dropdown. * Continued through to the finish step. config.yaml written with provider/model/base_url. **`.env` does NOT exist** — no placeholder string written. `chat_ready: true`, `state: ready`. * Vision tool confirmed the visual hierarchy: subtle italic help reads as documentation, prominent green banner pops as status. Tests ----- `tests/test_issue1499_keyless_onboarding.py` — 16 tests in 3 classes: TestKeyOptionalProviderSchema (5) - lmstudio / ollama / custom declare key_optional=True - openrouter / anthropic / openai do NOT (regression defense) - setup catalog exposes the flag TestKeylessOnboarding (6) - lmstudio / ollama / custom: empty api_key accepted, no .env write - openrouter / anthropic: empty api_key still rejected - lmstudio with explicit key still writes .env (regression defense) TestKeylessChatReady (5) - lmstudio / ollama: provider_ready=True with no key - custom: provider_ready=True with key+base_url, False without base_url - openrouter: provider_ready=False with no key (regression defense) - End-to-end get_onboarding_status reports chat_ready=True Full suite: 3901 → 3917 passing (+16 from this commit; +22 cumulative from the PR's earlier commit). 0 failures. Closes #1499 (all three sub-bugs from #1420 now addressed)
2026-05-03 03:07:07 +00:00
# Many self-hosted OpenAI-compatible servers (vLLM, llama-server,
# TabbyAPI, etc.) run keyless behind a private network. The wizard
# accepts an empty api_key — auth-protected endpoints can still
# supply one. See #1499.
"key_optional": True,
"models": [],
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"category": "self_hosted",
},
# ── Specialized / extended ──────────────────────────────────────────
"gemini": {
"label": "Google Gemini",
"env_var": "GOOGLE_API_KEY",
"default_model": "gemini-3.1-pro-preview",
"default_base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
"requires_base_url": False,
# _PROVIDER_MODELS in api/config.py is keyed under "google" even though
# the agent's alias map normalizes "google" → "gemini". Use the catalog
# key here so the wizard surfaces the actual model list.
"models": list(_PROVIDER_MODELS.get("google", [])),
"category": "specialized",
},
"deepseek": {
"label": "DeepSeek",
"env_var": "DEEPSEEK_API_KEY",
"default_model": "deepseek-v4-flash",
"default_base_url": "https://api.deepseek.com",
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"requires_base_url": False,
"models": list(_PROVIDER_MODELS.get("deepseek", [])),
"category": "specialized",
},
2026-05-10 23:33:52 +00:00
"xiaomi": {
"label": "Xiaomi MiMo",
"env_var": "XIAOMI_API_KEY",
"default_model": "mimo-v2.5-pro",
"default_base_url": "https://api.xiaomimimo.com/v1",
"requires_base_url": False,
"models": list(_PROVIDER_MODELS.get("xiaomi", [])),
"category": "specialized",
},
"zai": {
"label": "Z.AI / GLM (智谱)",
"env_var": "GLM_API_KEY",
"default_model": "glm-5.1",
"default_base_url": "https://open.bigmodel.cn/api/paas/v4",
"requires_base_url": False,
"models": list(_PROVIDER_MODELS.get("zai", [])),
"category": "specialized",
},
"nvidia": {
"label": "NVIDIA NIM",
"env_var": "NVIDIA_API_KEY",
"default_model": "nvidia/llama-3.3-nemotron-super-49b-v1.5",
"default_base_url": "https://integrate.api.nvidia.com/v1",
"requires_base_url": False,
"models": list(_PROVIDER_MODELS.get("nvidia", [])),
"category": "specialized",
},
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"mistralai": {
"label": "Mistral",
"env_var": "MISTRAL_API_KEY",
"default_model": "mistral-large-latest",
"default_base_url": "https://api.mistral.ai/v1",
"requires_base_url": False,
# No catalog entry for mistralai today — wizard shows a free-text input.
"models": list(_PROVIDER_MODELS.get("mistralai", [])),
"category": "specialized",
},
"x-ai": {
"label": "xAI (Grok)",
"env_var": "XAI_API_KEY",
"default_model": "grok-4.20",
"default_base_url": "https://api.x.ai/v1",
"requires_base_url": False,
# Agent normalizes "x-ai" → "xai"; _PROVIDER_MODELS is also keyed "xai"
# when populated, so check both keys for forward-compatibility.
"models": list(_PROVIDER_MODELS.get("xai", []) or _PROVIDER_MODELS.get("x-ai", [])),
"category": "specialized",
},
}
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
_PROVIDER_CATEGORIES = [
{"id": "easy_start", "label": "Easy start", "order": 0},
{"id": "self_hosted", "label": "Open / self-hosted", "order": 1},
{"id": "specialized", "label": "Specialized", "order": 2},
]
_UNSUPPORTED_PROVIDER_NOTE = (
2026-05-04 21:07:16 +00:00
"Advanced provider flows such as Nous Portal and GitHub Copilot are still "
"terminal-first. OpenAI Codex and Anthropic Claude Code can be authenticated in this onboarding flow "
"when your Hermes config selects the corresponding provider."
)
def _get_active_hermes_home() -> Path:
try:
from api.profiles import get_active_hermes_home
return get_active_hermes_home()
except ImportError:
return Path.home() / ".hermes"
def _load_env_file(env_path: Path) -> dict[str, str]:
values: dict[str, str] = {}
if not env_path.exists():
return values
try:
for raw in env_path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
except Exception:
return {}
return values
def _load_yaml_config(config_path: Path) -> dict:
try:
import yaml as _yaml
except ImportError:
return {}
if not config_path.exists():
return {}
try:
loaded = _yaml.safe_load(config_path.read_text(encoding="utf-8"))
return loaded if isinstance(loaded, dict) else {}
except Exception:
return {}
def _save_yaml_config(config_path: Path, config: dict) -> None:
try:
import yaml as _yaml
except ImportError as exc:
raise RuntimeError("PyYAML is required to write Hermes config.yaml") from exc
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(
_yaml.safe_dump(config, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
def _normalize_model_for_provider(provider: str, model: str) -> str:
clean = (model or "").strip()
if not clean:
return ""
if provider in {"anthropic", "openai"} and clean.startswith(provider + "/"):
return clean.split("/", 1)[1]
return clean
def _normalize_base_url(base_url: str) -> str:
return (base_url or "").strip().rstrip("/")
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
# ── Provider endpoint probe (#1499) ─────────────────────────────────────────
# Probe error codes — stable strings the frontend can switch on for inline
# error rendering. Add new codes only by extending this set; never reuse.
PROBE_ERROR_CODES = (
"invalid_url", # base_url failed urlparse / scheme / host check
"dns", # hostname did not resolve
"connect_refused", # TCP RST on connect (server not listening)
"timeout", # exceeded probe timeout
"http_4xx", # endpoint returned 4xx (auth required, wrong path, …)
"http_5xx", # endpoint returned 5xx (server-side fault)
"parse", # body not JSON or not the OpenAI /models shape
"unreachable", # other network / SSL / unknown error
)
PROBE_TIMEOUT_SECONDS = 5.0
# OpenAI /models response can list dozens of entries on Ollama / LM Studio.
# 256 KB is more than enough for any realistic catalog and bounds the worst
# case for a hostile / mis-pointed endpoint that streams forever.
PROBE_MAX_BYTES = 256 * 1024
fix(onboarding,probe): refuse HTTP redirects on probe path (reviewer-flagged on PR #1501) SSRF defense-in-depth: `urllib.request.urlopen` follows redirects by default, so a probe at `http://example.com/v1/models` could be redirected to `http://internal-service:8080/admin` — surfacing internal HTTP services to the authenticated user. The probe is already gated behind WebUI auth and the local-network check, so the practical attack surface is 'authenticated user enumerating internal services' (same as `curl` from their browser DevTools). Tightening the redirect default is cheap insurance. Implementation: - New module-level `_NoRedirectHandler` (subclasses `urllib.request.HTTPRedirectHandler`, overrides `redirect_request` to return None — urllib then raises `HTTPError(3xx)` rather than following). - New module-level `_PROBE_OPENER = urllib.request.build_opener(_NoRedirectHandler())`. - `probe_provider_endpoint` switches from `urlopen(req, …)` to `_PROBE_OPENER.open(req, …)`. - The existing `HTTPError` handler now categorizes 3xx as `unreachable` with a detail string mentioning 'redirect' so the user understands what happened. 3xx does NOT get its own error code in `PROBE_ERROR_CODES` — the error taxonomy contract stays the same shape (frontend i18n unchanged). Added regression test `test_probe_does_not_follow_redirects` in `tests/test_issue1499_onboarding_probe.py`. Spins up a tiny HTTP server that 302-redirects `/v1/models` to `/different-endpoint` (which would return `{'data': [{'id': 'should-not-see'}]}` if followed). Asserts the probe returns `{ok: False, error: 'unreachable', status: 302, detail: …'redirect'…}` and that the 'should-not-see' string never appears in the result. Mutation-verified: reverting `_PROBE_OPENER.open` back to `urlopen` causes the test to fail with "Probe followed a redirect — should have refused". Suite delta: 3917 → 3918 passing (+1). Reviewer-flagged in PR #1501. Per the 'reviewer-flagged-fix-in-release-not-followup' policy: <20 LOC defensive fix, regression test path obvious, ship in this release rather than punting.
2026-05-03 03:21:22 +00:00
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
"""Refuse to follow HTTP redirects on the probe path.
`urllib.request.urlopen` follows redirects by default — without this
handler, a probe at `http://example.com/v1/models` could be redirected
to `http://internal-service:8080/admin`, surfacing internal HTTP services
to whatever the probe targets next. The probe is already gated behind
WebUI auth and the local-network check, so the threat model is
"authenticated user enumerating internal services" — same as `curl`
from their browser DevTools. Disabling redirects tightens defaults
without breaking any legitimate use case (a self-hosted /models endpoint
that 3xx-redirects is itself misconfigured). Redirects surface to the
caller as `unreachable` (mapped from `HTTPError(3xx)` in the probe).
Reviewer-flagged in PR #1501 (#1499 + #1500).
"""
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None # tell urllib to NOT follow; raises HTTPError(3xx) instead
_PROBE_OPENER = urllib.request.build_opener(_NoRedirectHandler())
_DNS_ONLY_TEST_TLDS = frozenset({"invalid", "test", "example"})
def _hostname_uses_reserved_dns_tld(hostname: str | None) -> bool:
host = str(hostname or "").strip().rstrip(".").lower()
if not host or "." not in host:
return False
return host.rsplit(".", 1)[-1] in _DNS_ONLY_TEST_TLDS
def _exception_chain_text(exc) -> str:
parts: list[str] = []
seen: set[int] = set()
cur = exc
while cur is not None and id(cur) not in seen:
seen.add(id(cur))
parts.append(str(cur))
cur = getattr(cur, "__cause__", None) or getattr(cur, "__context__", None)
return " ".join(parts).lower()
def _probe_failure_is_dns(exc, hostname: str | None) -> bool:
if isinstance(exc, socket.gaierror):
return True
text = _exception_chain_text(exc)
if any(
marker in text
for marker in (
"getaddrinfo",
"gaierror",
"name or service not known",
"temporary failure in name resolution",
"nodename nor servname provided",
"no address associated with hostname",
)
):
return True
return _hostname_uses_reserved_dns_tld(hostname)
fix(onboarding,probe): refuse HTTP redirects on probe path (reviewer-flagged on PR #1501) SSRF defense-in-depth: `urllib.request.urlopen` follows redirects by default, so a probe at `http://example.com/v1/models` could be redirected to `http://internal-service:8080/admin` — surfacing internal HTTP services to the authenticated user. The probe is already gated behind WebUI auth and the local-network check, so the practical attack surface is 'authenticated user enumerating internal services' (same as `curl` from their browser DevTools). Tightening the redirect default is cheap insurance. Implementation: - New module-level `_NoRedirectHandler` (subclasses `urllib.request.HTTPRedirectHandler`, overrides `redirect_request` to return None — urllib then raises `HTTPError(3xx)` rather than following). - New module-level `_PROBE_OPENER = urllib.request.build_opener(_NoRedirectHandler())`. - `probe_provider_endpoint` switches from `urlopen(req, …)` to `_PROBE_OPENER.open(req, …)`. - The existing `HTTPError` handler now categorizes 3xx as `unreachable` with a detail string mentioning 'redirect' so the user understands what happened. 3xx does NOT get its own error code in `PROBE_ERROR_CODES` — the error taxonomy contract stays the same shape (frontend i18n unchanged). Added regression test `test_probe_does_not_follow_redirects` in `tests/test_issue1499_onboarding_probe.py`. Spins up a tiny HTTP server that 302-redirects `/v1/models` to `/different-endpoint` (which would return `{'data': [{'id': 'should-not-see'}]}` if followed). Asserts the probe returns `{ok: False, error: 'unreachable', status: 302, detail: …'redirect'…}` and that the 'should-not-see' string never appears in the result. Mutation-verified: reverting `_PROBE_OPENER.open` back to `urlopen` causes the test to fail with "Probe followed a redirect — should have refused". Suite delta: 3917 → 3918 passing (+1). Reviewer-flagged in PR #1501. Per the 'reviewer-flagged-fix-in-release-not-followup' policy: <20 LOC defensive fix, regression test path obvious, ship in this release rather than punting.
2026-05-03 03:21:22 +00:00
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
def probe_provider_endpoint(
provider: str,
base_url: str,
api_key: str | None = None,
timeout: float = PROBE_TIMEOUT_SECONDS,
) -> dict:
"""Probe `<base_url>/models` for a self-hosted OpenAI-compatible provider.
Used by the onboarding wizard to validate the user's configured base URL
before persisting (#1499). Distinguishes failure modes so the frontend
can render a precise inline error instead of a generic "could not save."
Returns one of:
{"ok": True, "models": [{"id": "...", "label": "..."}, ...]}
{"ok": False, "error": "<code>", "detail": "<human string>"}
Where ``<code>`` is one of ``PROBE_ERROR_CODES``.
The probe is a single HTTP GET — no retries. The timeout is short by
design: the wizard runs the probe synchronously on the user's submit
click, and we'd rather report "timeout" quickly than block the UI for
the kernel default ~75s.
The probe response is NOT persisted. This function returns model IDs
so the wizard can populate its dropdown, but ``apply_onboarding_setup``
only writes the user's typed selection — never auto-pinning a stale
list of models to ``config.yaml``.
SSRF: ``base_url`` is whatever the user typed in the onboarding form.
The wizard is gated behind authentication (post-onboarding, the user
has already authenticated to the WebUI), and the legitimate target is
a local LM Studio / Ollama / vLLM server, so we deliberately do not
block private-IP ranges — that would make the feature useless. The
risk surface is "authenticated user crafts a probe to enumerate
internal HTTP services," which is a different threat model from
unauthenticated SSRF.
"""
base_url = _normalize_base_url(base_url)
if not base_url:
return {"ok": False, "error": "invalid_url", "detail": "base_url is required"}
parsed = urlparse(base_url)
if parsed.scheme not in {"http", "https"}:
return {
"ok": False,
"error": "invalid_url",
"detail": "base_url must start with http:// or https://",
}
if not parsed.hostname:
return {"ok": False, "error": "invalid_url", "detail": "base_url has no host"}
# Build the probe URL. OpenAI-compatible servers expose /v1/models or
# /models. Most users supply a base URL ending in /v1, so we just append
# /models to whatever they typed. Strip the trailing slash and append
# rather than urljoin to avoid eating the /v1 segment when there's no
# trailing slash.
probe_url = f"{base_url}/models"
headers = {
"Accept": "application/json",
"User-Agent": "hermes-webui-onboarding-probe",
}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
req = urllib.request.Request(probe_url, headers=headers, method="GET")
try:
fix(onboarding,probe): refuse HTTP redirects on probe path (reviewer-flagged on PR #1501) SSRF defense-in-depth: `urllib.request.urlopen` follows redirects by default, so a probe at `http://example.com/v1/models` could be redirected to `http://internal-service:8080/admin` — surfacing internal HTTP services to the authenticated user. The probe is already gated behind WebUI auth and the local-network check, so the practical attack surface is 'authenticated user enumerating internal services' (same as `curl` from their browser DevTools). Tightening the redirect default is cheap insurance. Implementation: - New module-level `_NoRedirectHandler` (subclasses `urllib.request.HTTPRedirectHandler`, overrides `redirect_request` to return None — urllib then raises `HTTPError(3xx)` rather than following). - New module-level `_PROBE_OPENER = urllib.request.build_opener(_NoRedirectHandler())`. - `probe_provider_endpoint` switches from `urlopen(req, …)` to `_PROBE_OPENER.open(req, …)`. - The existing `HTTPError` handler now categorizes 3xx as `unreachable` with a detail string mentioning 'redirect' so the user understands what happened. 3xx does NOT get its own error code in `PROBE_ERROR_CODES` — the error taxonomy contract stays the same shape (frontend i18n unchanged). Added regression test `test_probe_does_not_follow_redirects` in `tests/test_issue1499_onboarding_probe.py`. Spins up a tiny HTTP server that 302-redirects `/v1/models` to `/different-endpoint` (which would return `{'data': [{'id': 'should-not-see'}]}` if followed). Asserts the probe returns `{ok: False, error: 'unreachable', status: 302, detail: …'redirect'…}` and that the 'should-not-see' string never appears in the result. Mutation-verified: reverting `_PROBE_OPENER.open` back to `urlopen` causes the test to fail with "Probe followed a redirect — should have refused". Suite delta: 3917 → 3918 passing (+1). Reviewer-flagged in PR #1501. Per the 'reviewer-flagged-fix-in-release-not-followup' policy: <20 LOC defensive fix, regression test path obvious, ship in this release rather than punting.
2026-05-03 03:21:22 +00:00
with _PROBE_OPENER.open(req, timeout=timeout) as resp:
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
status = resp.status
body = resp.read(PROBE_MAX_BYTES + 1)
except urllib.error.HTTPError as exc:
fix(onboarding,probe): refuse HTTP redirects on probe path (reviewer-flagged on PR #1501) SSRF defense-in-depth: `urllib.request.urlopen` follows redirects by default, so a probe at `http://example.com/v1/models` could be redirected to `http://internal-service:8080/admin` — surfacing internal HTTP services to the authenticated user. The probe is already gated behind WebUI auth and the local-network check, so the practical attack surface is 'authenticated user enumerating internal services' (same as `curl` from their browser DevTools). Tightening the redirect default is cheap insurance. Implementation: - New module-level `_NoRedirectHandler` (subclasses `urllib.request.HTTPRedirectHandler`, overrides `redirect_request` to return None — urllib then raises `HTTPError(3xx)` rather than following). - New module-level `_PROBE_OPENER = urllib.request.build_opener(_NoRedirectHandler())`. - `probe_provider_endpoint` switches from `urlopen(req, …)` to `_PROBE_OPENER.open(req, …)`. - The existing `HTTPError` handler now categorizes 3xx as `unreachable` with a detail string mentioning 'redirect' so the user understands what happened. 3xx does NOT get its own error code in `PROBE_ERROR_CODES` — the error taxonomy contract stays the same shape (frontend i18n unchanged). Added regression test `test_probe_does_not_follow_redirects` in `tests/test_issue1499_onboarding_probe.py`. Spins up a tiny HTTP server that 302-redirects `/v1/models` to `/different-endpoint` (which would return `{'data': [{'id': 'should-not-see'}]}` if followed). Asserts the probe returns `{ok: False, error: 'unreachable', status: 302, detail: …'redirect'…}` and that the 'should-not-see' string never appears in the result. Mutation-verified: reverting `_PROBE_OPENER.open` back to `urlopen` causes the test to fail with "Probe followed a redirect — should have refused". Suite delta: 3917 → 3918 passing (+1). Reviewer-flagged in PR #1501. Per the 'reviewer-flagged-fix-in-release-not-followup' policy: <20 LOC defensive fix, regression test path obvious, ship in this release rather than punting.
2026-05-03 03:21:22 +00:00
# 3xx / 4xx / 5xx with a body — categorize. 3xx happens when the
# endpoint redirects (we refuse to follow on the probe path — see
# _NoRedirectHandler). Map to `unreachable` rather than introducing a
# new error code, since a self-hosted /models endpoint that 3xx-
# redirects is itself misconfigured.
if 300 <= exc.code < 400:
code = "unreachable"
detail = (
f"HTTP {exc.code} — endpoint returned a redirect "
f"(probe does not follow redirects). Point base_url at the "
f"final URL directly."
)
return {"ok": False, "error": code, "detail": detail, "status": exc.code}
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
code = "http_4xx" if 400 <= exc.code < 500 else "http_5xx"
# Try to surface a useful detail (LM Studio sometimes returns text/plain).
try:
err_body = exc.read(2048).decode("utf-8", errors="replace").strip()
except Exception:
err_body = ""
detail = f"HTTP {exc.code}"
if err_body:
err_first = err_body.splitlines()[0][:200]
detail = f"{detail}: {err_first}"
return {"ok": False, "error": code, "detail": detail, "status": exc.code}
except urllib.error.URLError as exc:
# Distinguish DNS / connect-refused / timeout / generic.
reason = exc.reason
if isinstance(reason, socket.timeout) or "timed out" in str(reason).lower():
return {"ok": False, "error": "timeout", "detail": f"connection timed out after {timeout:g}s"}
if _probe_failure_is_dns(reason, parsed.hostname):
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
return {
"ok": False,
"error": "dns",
"detail": f"could not resolve host '{parsed.hostname}'",
}
if isinstance(reason, ConnectionRefusedError) or "refused" in str(reason).lower():
port_hint = parsed.port or ("443" if parsed.scheme == "https" else "80")
return {
"ok": False,
"error": "connect_refused",
"detail": f"connection refused at {parsed.hostname}:{port_hint}",
}
return {"ok": False, "error": "unreachable", "detail": str(reason)[:200]}
except (TimeoutError, socket.timeout):
return {"ok": False, "error": "timeout", "detail": f"connection timed out after {timeout:g}s"}
except Exception as exc: # pragma: no cover — defensive net
if _probe_failure_is_dns(exc, parsed.hostname):
return {
"ok": False,
"error": "dns",
"detail": f"could not resolve host '{parsed.hostname}'",
}
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
logger.debug("probe_provider_endpoint unexpected error", exc_info=True)
return {"ok": False, "error": "unreachable", "detail": str(exc)[:200]}
# If the response was huge, refuse to parse. 256 KB cap is generous;
# anything bigger is likely the user pointed us at the wrong service.
if len(body) > PROBE_MAX_BYTES:
return {
"ok": False,
"error": "parse",
"detail": f"response exceeded {PROBE_MAX_BYTES // 1024} KB cap",
}
try:
payload = json.loads(body.decode("utf-8", errors="replace"))
except (ValueError, UnicodeDecodeError) as exc:
return {
"ok": False,
"error": "parse",
"detail": f"response is not JSON ({exc.__class__.__name__})",
}
# Accept both the OpenAI shape (`{"data": [{"id": ...}, ...]}`) and the
# bare-list shape some self-hosted servers return (`[{"id": ...}, ...]`).
if isinstance(payload, dict) and isinstance(payload.get("data"), list):
entries = payload["data"]
elif isinstance(payload, list):
entries = payload
else:
return {
"ok": False,
"error": "parse",
"detail": "response is not in OpenAI /models shape (expected {'data': [...]} or [...])",
}
models = []
for entry in entries:
if isinstance(entry, dict) and entry.get("id"):
mid = str(entry["id"]).strip()
if mid:
models.append({"id": mid, "label": mid})
elif isinstance(entry, str) and entry.strip():
models.append({"id": entry.strip(), "label": entry.strip()})
return {"ok": True, "models": models, "status": status}
def _extract_current_provider(cfg: dict) -> str:
model_cfg = cfg.get("model", {})
if isinstance(model_cfg, dict):
provider = str(model_cfg.get("provider") or "").strip().lower()
if provider:
return provider
return ""
def _extract_current_model(cfg: dict) -> str:
model_cfg = cfg.get("model", {})
if isinstance(model_cfg, str):
return model_cfg.strip()
if isinstance(model_cfg, dict):
return str(model_cfg.get("default") or "").strip()
return ""
def _extract_current_base_url(cfg: dict) -> str:
model_cfg = cfg.get("model", {})
if isinstance(model_cfg, dict):
return _normalize_base_url(str(model_cfg.get("base_url") or ""))
return ""
def _provider_api_key_present(
provider: str, cfg: dict, env_values: dict[str, str]
) -> bool:
provider = (provider or "").strip().lower()
if not provider:
return False
env_var = _SUPPORTED_PROVIDER_SETUPS.get(provider, {}).get("env_var")
if env_var and env_values.get(env_var):
return True
fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500) Addresses both #1499 (onboarding wizard never probes the configured base URL) and #1500 (cross-tool env-var name divergence between webui and agent CLI). Surfaced together because they're both LM-Studio onboarding bugs that pile on top of each other — fixing only one leaves the broken UX. #1499 — Onboarding wizard probes <base_url>/models before persisting Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed without ever fetching `<base_url>/models`. @chwps's log timeline in #1420 showed the wizard finishing in 239ms with zero outbound HTTP — onboarding silently persisted unreachable URLs and left users with empty model dropdowns they had to populate by hand-editing config.yaml. Backend: * New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)` in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep). Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}` on failure with stable error codes the frontend can switch on: invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse, unreachable. 256 KB response cap and 5s timeout keep a hostile or mis- pointed endpoint from blocking the wizard. * New `POST /api/onboarding/probe` route — thin JSON wrapper around the function above. Same local-network gate as `/api/onboarding/setup` because the body carries an `api_key` the user typed. * The probe response is NEVER persisted. Only the user's typed selection ends up in config.yaml; the probed model list just populates the wizard's dropdown. * SSRF: deliberately does NOT block private-IP ranges. The wizard is gated behind WebUI auth and the legitimate target IS a local LM Studio / Ollama / vLLM server. A "block private IPs" SSRF defense would make the feature useless for its primary use case. Frontend: * `static/onboarding.js`: - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}). - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent & cached on (provider, baseUrl, apiKey). - Debounced (400ms) on `oninput` of the base URL field. - Explicit "Test connection" button. - `nextOnboardingStep` blocks Continue at the setup step for any provider with `requires_base_url=True` until the probe succeeds. Same localized error renders inline. * `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8 — same convention as v0.50.271 #1488 voice-buttons). * `static/style.css`: probe banner + Test button styling (red-tinted error variant, green-tinted success variant, neutral probing state). Verified via manual repro on port 8789: * connect_refused → red banner, helpful "from Docker, try the host IP" hint, blocks Continue. * DNS failure → red banner, "could not resolve host '...'", blocks Continue. * Success against a mock /v1/models server → green banner, model dropdown populates from the probed list, Continue advances normally. #1500 — webui env var aligned with agent CLI (LM_API_KEY) The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in both onboarding and Settings detection. The agent CLI runtime (hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured auth on their LM Studio instance got Settings → Providers reporting has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER → 401 against the auth-enabled LM Studio server. Masked in practice for the no-auth majority. Picked Option B from the issue (defer to the agent — single source of truth) but mitigated the migration cliff by reading the legacy name as a fallback: * `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`: - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward). - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for pre-#1500 users so detection keeps working without forcing an .env rewrite). * `api/onboarding.py:_provider_api_key_present` reads aliases too. * `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`. * `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)` — new dict, used by `_provider_has_key` and `get_providers`'s key_source resolution. Drops in cleanly when other providers later rename their env vars too. Verified: ``` before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat after fix: webui writes LM_API_KEY → agent picks it up → chat works pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings → key_source='env_file' ``` Tests * `tests/test_issue1499_onboarding_probe.py` — 17 tests: 3 invalid_url variants, dns, connect_refused, success (OpenAI shape), success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse wrong-shape, api_key authorization header passthrough, "probe must not write to config.yaml or .env", PROBE_ERROR_CODES contract pin, 3 end-to-end route-level smoke tests against the live server fixture. * `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests: onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias, onboarding writes ONLY the canonical name, legacy env var still detected post-migration, canonical takes precedence when both are set, _provider_api_key_present reads aliases. * `tests/test_issue1420_lmstudio_provider_env_var.py` — updated: the original 5-test #1420 suite now pins LM_API_KEY as canonical and LMSTUDIO_API_KEY as alias. Full suite: 3879 → 3901 passing (+22), 0 failures. Out of scope (explicitly NOT addressed here) The third LM Studio onboarding sub-bug from #1420's thread — that `apply_onboarding_setup` requires a non-empty api_key for lmstudio even though most LM Studio installs run keyless — remains. The agent's `LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but the onboarding wizard rejects the empty-key case at submit. Fixing this requires a UX decision (auto-write a sentinel? loosen the required-key check for self-hosted providers?) and is left as a separate follow-up. Closes #1499 Closes #1500 Co-authored-by: chwps <106549456+chwps@users.noreply.github.com> Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
# Legacy env-var aliases (read-only fallback for env vars renamed in past
# releases — e.g. lmstudio's LM_API_KEY canonical + LMSTUDIO_API_KEY legacy
# in #1500). Canonical name is what onboarding writes going forward;
# aliases keep existing users' detection working without forcing an .env
# rewrite.
for alias in _SUPPORTED_PROVIDER_SETUPS.get(provider, {}).get("env_var_aliases", []) or []:
if alias and env_values.get(alias):
return True
model_cfg = cfg.get("model", {})
if isinstance(model_cfg, dict) and str(model_cfg.get("api_key") or "").strip():
return True
fix: harden None providers config across onboarding/providers/routes (salvage of #3967) A config.yaml with an explicit empty/null `providers:` key parses to None in PyYAML. `cfg.get("providers", {})` only returns the {} default when the key is *absent*, so an explicit null yields None instead of an empty mapping. master already guards each read with `isinstance(providers_cfg, dict)`, but the value fed to that guard is still a footgun: the natural chained form `cfg.get("providers", {}).get(...)` (already fixed once in api/config.py) crashes with AttributeError on None. Harden every providers-key read in the three remaining files to `cfg.get("providers") or {}` so an explicit null degrades to an empty dict at the source, matching the existing api/config.py convention. Sites hardened: api/onboarding.py:578 (_provider_api_key_present) api/providers.py:1051 (_provider_has_shadowed_codex_oauth_value) api/providers.py:1219 (_provider_has_key) api/providers.py:1265 (_get_provider_api_key) api/providers.py:2348 (get_providers catalog) api/providers.py:2781 (_clean_provider_key_from_config) api/routes.py:5045 (_context_length_config_api_key_for_provider) api/routes.py:5110 (_context_length_lookup_inputs_for_model) api/routes.py:15916 (_handle_live_models) Adds tests/test_none_providers_config_guard.py: per-file source-form pins (fail on master / on any single-file revert) plus behavioural checks driving the real functions with `providers: None` to prove no crash and parity with the empty-mapping config. Updates the existing test_issue3717 routes-string pin to match the hardened form. This is a focused salvage of grab-bag PR #3967 -- only the None-providers hardening is taken; the PR's unrelated changes (account-usage worker-pool refactor, function removals) are intentionally dropped. Co-authored-by: lidi1011 <lidi1011@users.noreply.github.com>
2026-06-28 05:57:56 +00:00
# ``cfg.get("providers", {})`` only returns the default when the key is
# absent; an explicit ``providers:`` (null) in config.yaml yields ``None``.
# ``... or {}`` degrades that null to an empty mapping (salvage of #3967).
providers_cfg = cfg.get("providers") or {}
if isinstance(providers_cfg, dict):
provider_cfg = providers_cfg.get(provider, {})
if (
isinstance(provider_cfg, dict)
and str(provider_cfg.get("api_key") or "").strip()
):
return True
if provider == "custom":
custom_cfg = providers_cfg.get("custom", {})
if (
isinstance(custom_cfg, dict)
and str(custom_cfg.get("api_key") or "").strip()
):
return True
# For providers not in _SUPPORTED_PROVIDER_SETUPS (e.g. minimax-cn, deepseek,
# xai, etc.), ask the hermes_cli auth registry — it knows every provider's env
# var names and can check os.environ for a valid key.
# Exclude known OAuth/token-flow providers — those are handled separately by
# _provider_oauth_authenticated() and should not be short-circuited here.
_known_oauth = {"openai-codex", "copilot", "copilot-acp", "qwen-oauth", "nous", "anthropic"}
if provider not in _SUPPORTED_PROVIDER_SETUPS and provider not in _known_oauth:
try:
from hermes_cli.auth import get_auth_status as _gas
status = _gas(provider)
if isinstance(status, dict) and status.get("logged_in"):
return True
except Exception:
pass
return False
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
def _oauth_payload_has_token(payload: dict) -> bool:
"""Return True if an auth payload contains usable token material."""
if not isinstance(payload, dict):
return False
token_fields = (
payload,
payload.get("tokens") if isinstance(payload.get("tokens"), dict) else {},
)
for candidate in token_fields:
if not isinstance(candidate, dict):
continue
if any(
str(candidate.get(key) or "").strip()
for key in ("access_token", "refresh_token", "api_key")
):
return True
return False
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
def _provider_oauth_authenticated(provider: str, hermes_home: "Path") -> bool:
"""Return True if the provider has valid OAuth credentials.
Reads the profile-scoped auth.json directly so onboarding respects the
requested Hermes home. Known OAuth providers may store auth either in the
legacy providers[provider_id] singleton state or in credential_pool entries
used by current Hermes runtime auth resolution.
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
"""
provider = (provider or "").strip().lower()
provider = {"claude": "anthropic", "claude-code": "anthropic"}.get(provider, provider)
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
if not provider:
return False
_known_oauth_providers = {"openai-codex", "copilot", "copilot-acp", "qwen-oauth", "nous", "anthropic"}
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
if provider not in _known_oauth_providers:
return False
try:
import json as _j
auth_path = hermes_home / "auth.json"
if not auth_path.exists():
return False
store = _j.loads(auth_path.read_text(encoding="utf-8"))
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
providers_store = store.get("providers")
if isinstance(providers_store, dict):
state = providers_store.get(provider)
if _oauth_payload_has_token(state):
return True
pool_store = store.get("credential_pool")
if isinstance(pool_store, dict):
entries = pool_store.get(provider)
if isinstance(entries, list):
for entry in entries:
if _oauth_payload_has_token(entry):
return True
if (
provider == "anthropic"
and isinstance(entry, dict)
and entry.get("auth_type") == "oauth"
and entry.get("source") == "claude_code_linked"
):
return True
return False
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
except Exception:
return False
def _status_from_runtime(cfg: dict, imports_ok: bool) -> dict:
provider = _extract_current_provider(cfg)
model = _extract_current_model(cfg)
base_url = _extract_current_base_url(cfg)
env_values = _load_env_file(_get_active_hermes_home() / ".env")
provider_configured = bool(provider and model)
provider_ready = False
if provider_configured:
fix(onboarding): allow keyless setup for self-hosted providers (#1499 third sub-bug) Pre-fix, the wizard rejected an empty api_key for every provider in _SUPPORTED_PROVIDER_SETUPS — including lmstudio, ollama, and custom, which run keyless on the vast majority of local installs. The agent's LMSTUDIO_NOAUTH_PLACEHOLDER substitution at chat-time was the workaround for the no-auth case, but the wizard side rejected the empty input first. Users had to type random gibberish into the API key field to clear the form — the third sub-bug from #1420 that the prior commit's PR description explicitly punted to a follow-up. Surfaced by Nathan during PR review: "I think it's too weird for users to have to type a string into the API key field, right?" Yes — and the probe (#1499) makes the cleanest fix strictly better: we accept empty keys, and the probe gives instant feedback ("Connected. 2 model(s) available." for keyless servers, "401" for auth-required servers). Backend changes --------------- * `api/onboarding.py` — `_SUPPORTED_PROVIDER_SETUPS` gains `key_optional: True` for `lmstudio`, `ollama`, `custom`. Cloud providers (openrouter, anthropic, openai, gemini, deepseek, …) remain key_required. * `apply_onboarding_setup` skips the "{env_var} is required" check when `key_optional` is set AND no key is supplied. No write to .env for the empty-key case (no `LM_API_KEY=*** placeholder lying in the user's .env`). * `_status_from_runtime` reports `provider_ready=True` for key_optional providers based on `requires_base_url` alone, so the wizard doesn't refire on the next page load just because there's no api_key. Cloud providers still need a key for provider_ready=True. * `_build_setup_catalog` exposes the `key_optional` flag to the frontend. Frontend changes ---------------- * `static/onboarding.js` — new `_renderOnboardingApiKeyField()` helper. For key_optional providers: - Label: "API key (optional)" - Placeholder: "Leave blank for keyless servers" - Inline italic muted help: "Most LM Studio / Ollama / vLLM installs run keyless — leave this blank if your server doesn't require authentication. Use the Test connection button to verify." For cloud providers: unchanged (label "API key", standard placeholder, no help block). * The api-key input also now triggers `_scheduleOnboardingProbe()` on oninput, so changing the key re-runs the probe — handles "the server rejected my empty key with 401, let me add one and retry." * `static/i18n.js` — 3 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8). * `static/style.css` — `.onboarding-api-key-help` rule for the muted italic helper paragraph. Verified end-to-end on port 8789 -------------------------------- Spun up an isolated test server + a mock LM Studio at `127.0.0.1:11234/v1/models`. Stepped through the wizard: * Picked LM Studio → field label flipped to "API key (optional)", placeholder showed "Leave blank for keyless servers", help text rendered in italic muted gray below. * Switched to Anthropic → label reverted to "API key", help text disappeared. Visual hierarchy correct. * Left api_key blank, set base_url to the mock, clicked Test connection → green "Connected. 2 model(s) available." banner. Probe-discovered models populated the workspace-step dropdown. * Continued through to the finish step. config.yaml written with provider/model/base_url. **`.env` does NOT exist** — no placeholder string written. `chat_ready: true`, `state: ready`. * Vision tool confirmed the visual hierarchy: subtle italic help reads as documentation, prominent green banner pops as status. Tests ----- `tests/test_issue1499_keyless_onboarding.py` — 16 tests in 3 classes: TestKeyOptionalProviderSchema (5) - lmstudio / ollama / custom declare key_optional=True - openrouter / anthropic / openai do NOT (regression defense) - setup catalog exposes the flag TestKeylessOnboarding (6) - lmstudio / ollama / custom: empty api_key accepted, no .env write - openrouter / anthropic: empty api_key still rejected - lmstudio with explicit key still writes .env (regression defense) TestKeylessChatReady (5) - lmstudio / ollama: provider_ready=True with no key - custom: provider_ready=True with key+base_url, False without base_url - openrouter: provider_ready=False with no key (regression defense) - End-to-end get_onboarding_status reports chat_ready=True Full suite: 3901 → 3917 passing (+16 from this commit; +22 cumulative from the PR's earlier commit). 0 failures. Closes #1499 (all three sub-bugs from #1420 now addressed)
2026-05-03 03:07:07 +00:00
meta = _SUPPORTED_PROVIDER_SETUPS.get(provider, {})
if provider in _SUPPORTED_PROVIDER_SETUPS:
# key_optional providers (lmstudio, ollama, custom) are ready as
# soon as the user has saved a provider+model+base_url; an api_key
# is allowed but not required. The agent runtime substitutes a
# placeholder for keyless local servers (LMSTUDIO_NOAUTH_PLACEHOLDER
# for lmstudio, equivalent paths for ollama / custom). See #1499
# third sub-bug from #1420.
if meta.get("key_optional"):
if meta.get("requires_base_url"):
provider_ready = bool(base_url)
else:
provider_ready = True
else:
# Standard wizard provider (openrouter, anthropic, openai, gemini,
# deepseek, zai, …) — needs an api_key. Custom historically also
# took this branch, but is now key_optional via the meta flag.
if meta.get("requires_base_url"):
provider_ready = bool(
base_url
and _provider_api_key_present(provider, cfg, env_values)
)
else:
provider_ready = _provider_api_key_present(provider, cfg, env_values)
if not provider_ready and meta.get("oauth_provider"):
provider_ready = _provider_oauth_authenticated(
str(meta.get("oauth_provider")), _get_active_hermes_home()
)
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
else:
# Unknown provider — may be an OAuth flow (openai-codex, copilot, etc.)
# OR an API-key provider not in the quick-setup list (minimax-cn, deepseek,
# xai, etc.). Check both: api key presence first (covers the majority of
# third-party providers), then OAuth auth.json.
provider_ready = (
_provider_api_key_present(provider, cfg, env_values)
or _provider_oauth_authenticated(provider, _get_active_hermes_home())
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
)
chat_ready = bool(_HERMES_FOUND and imports_ok and provider_ready)
note_args: list[str] = []
if not _HERMES_FOUND or not imports_ok:
state = "agent_unavailable"
note_key = "onboarding_notice_system_unavailable"
note = (
"Hermes is not fully importable from the Web UI yet. Finish bootstrap or fix the "
"agent install before provider setup will work."
)
elif chat_ready:
state = "ready"
note_key = "onboarding_notice_system_ready"
provider_name = _PROVIDER_DISPLAY.get(
provider, provider.title() if provider else "Hermes"
)
note = f"Hermes is minimally configured and ready to chat via {provider_name}."
elif provider_configured:
state = "provider_incomplete"
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
if provider == "custom" and not base_url:
note_key = "onboarding_notice_custom_base_url_required"
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
note = (
"Hermes has a saved provider/model selection, but the custom "
"provider still needs a base URL. Add the API key too if that "
"server requires one."
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
)
elif provider not in _SUPPORTED_PROVIDER_SETUPS:
note_key = "onboarding_notice_provider_auth_required"
note_args = [provider]
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
# OAuth / unsupported provider: avoid misleading "API key" wording.
note = (
f"Provider '{provider}' is configured but not yet authenticated. "
"Run 'hermes auth' or 'hermes model' in a terminal to complete "
"setup, then reload the Web UI."
)
else:
note_key = "onboarding_notice_provider_api_key_required"
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
note = (
"Hermes has a saved provider/model selection but still needs the "
"API key required to chat."
)
else:
state = "needs_provider"
note_key = "onboarding_notice_provider_choice_required"
note = "Hermes is installed, but you still need to choose a provider and save working credentials."
return {
"provider_configured": provider_configured,
"provider_ready": provider_ready,
"chat_ready": chat_ready,
"setup_state": state,
"provider_note": note,
"provider_note_key": note_key,
"provider_note_args": note_args,
"current_provider": provider or None,
"current_model": model or None,
"current_base_url": base_url or None,
"env_path": str(_get_active_hermes_home() / ".env"),
}
def _build_setup_catalog(cfg: dict) -> dict:
current_provider = _extract_current_provider(cfg) or "openrouter"
current_model = _extract_current_model(cfg)
current_base_url = _extract_current_base_url(cfg)
providers = []
for provider_id, meta in _SUPPORTED_PROVIDER_SETUPS.items():
providers.append(
{
"id": provider_id,
"label": meta["label"],
"env_var": meta["env_var"],
"default_model": meta["default_model"],
"default_base_url": meta.get("default_base_url") or "",
"requires_base_url": bool(meta.get("requires_base_url")),
fix(onboarding): allow keyless setup for self-hosted providers (#1499 third sub-bug) Pre-fix, the wizard rejected an empty api_key for every provider in _SUPPORTED_PROVIDER_SETUPS — including lmstudio, ollama, and custom, which run keyless on the vast majority of local installs. The agent's LMSTUDIO_NOAUTH_PLACEHOLDER substitution at chat-time was the workaround for the no-auth case, but the wizard side rejected the empty input first. Users had to type random gibberish into the API key field to clear the form — the third sub-bug from #1420 that the prior commit's PR description explicitly punted to a follow-up. Surfaced by Nathan during PR review: "I think it's too weird for users to have to type a string into the API key field, right?" Yes — and the probe (#1499) makes the cleanest fix strictly better: we accept empty keys, and the probe gives instant feedback ("Connected. 2 model(s) available." for keyless servers, "401" for auth-required servers). Backend changes --------------- * `api/onboarding.py` — `_SUPPORTED_PROVIDER_SETUPS` gains `key_optional: True` for `lmstudio`, `ollama`, `custom`. Cloud providers (openrouter, anthropic, openai, gemini, deepseek, …) remain key_required. * `apply_onboarding_setup` skips the "{env_var} is required" check when `key_optional` is set AND no key is supplied. No write to .env for the empty-key case (no `LM_API_KEY=*** placeholder lying in the user's .env`). * `_status_from_runtime` reports `provider_ready=True` for key_optional providers based on `requires_base_url` alone, so the wizard doesn't refire on the next page load just because there's no api_key. Cloud providers still need a key for provider_ready=True. * `_build_setup_catalog` exposes the `key_optional` flag to the frontend. Frontend changes ---------------- * `static/onboarding.js` — new `_renderOnboardingApiKeyField()` helper. For key_optional providers: - Label: "API key (optional)" - Placeholder: "Leave blank for keyless servers" - Inline italic muted help: "Most LM Studio / Ollama / vLLM installs run keyless — leave this blank if your server doesn't require authentication. Use the Test connection button to verify." For cloud providers: unchanged (label "API key", standard placeholder, no help block). * The api-key input also now triggers `_scheduleOnboardingProbe()` on oninput, so changing the key re-runs the probe — handles "the server rejected my empty key with 401, let me add one and retry." * `static/i18n.js` — 3 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8). * `static/style.css` — `.onboarding-api-key-help` rule for the muted italic helper paragraph. Verified end-to-end on port 8789 -------------------------------- Spun up an isolated test server + a mock LM Studio at `127.0.0.1:11234/v1/models`. Stepped through the wizard: * Picked LM Studio → field label flipped to "API key (optional)", placeholder showed "Leave blank for keyless servers", help text rendered in italic muted gray below. * Switched to Anthropic → label reverted to "API key", help text disappeared. Visual hierarchy correct. * Left api_key blank, set base_url to the mock, clicked Test connection → green "Connected. 2 model(s) available." banner. Probe-discovered models populated the workspace-step dropdown. * Continued through to the finish step. config.yaml written with provider/model/base_url. **`.env` does NOT exist** — no placeholder string written. `chat_ready: true`, `state: ready`. * Vision tool confirmed the visual hierarchy: subtle italic help reads as documentation, prominent green banner pops as status. Tests ----- `tests/test_issue1499_keyless_onboarding.py` — 16 tests in 3 classes: TestKeyOptionalProviderSchema (5) - lmstudio / ollama / custom declare key_optional=True - openrouter / anthropic / openai do NOT (regression defense) - setup catalog exposes the flag TestKeylessOnboarding (6) - lmstudio / ollama / custom: empty api_key accepted, no .env write - openrouter / anthropic: empty api_key still rejected - lmstudio with explicit key still writes .env (regression defense) TestKeylessChatReady (5) - lmstudio / ollama: provider_ready=True with no key - custom: provider_ready=True with key+base_url, False without base_url - openrouter: provider_ready=False with no key (regression defense) - End-to-end get_onboarding_status reports chat_ready=True Full suite: 3901 → 3917 passing (+16 from this commit; +22 cumulative from the PR's earlier commit). 0 failures. Closes #1499 (all three sub-bugs from #1420 now addressed)
2026-05-03 03:07:07 +00:00
# #1499 (third sub-bug from #1420) — providers that may run
# keyless (lmstudio, ollama, custom). Frontend uses this to
# show a "(optional)" hint and allow Continue without a key.
"key_optional": bool(meta.get("key_optional")),
"models": list(meta.get("models", [])),
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"category": meta.get("category", "easy_start"),
"quick": meta.get("quick", False),
"oauth_provider": meta.get("oauth_provider") or "",
"oauth_label": meta.get("oauth_label") or "",
}
)
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
# Sort providers by category order, then alphabetically within each category.
cat_order = {c["id"]: c["order"] for c in _PROVIDER_CATEGORIES}
providers.sort(key=lambda p: (cat_order.get(p["category"], 99), p["label"]))
# Group providers by category for the frontend.
categories = []
for cat in sorted(_PROVIDER_CATEGORIES, key=lambda c: c["order"]):
categories.append({
"id": cat["id"],
"label": cat["label"],
"providers": [p["id"] for p in providers if p["category"] == cat["id"]],
})
# Flag whether the currently-configured provider is OAuth-based (not in the
# API-key flow). The frontend uses this to show a confirmation card instead
# of a key input when the user has already authenticated via 'hermes auth'.
current_is_oauth = (
current_provider not in _SUPPORTED_PROVIDER_SETUPS and bool(current_provider)
) or _provider_oauth_authenticated(current_provider, _get_active_hermes_home())
return {
"providers": providers,
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
"categories": categories,
"unsupported_note": _UNSUPPORTED_PROVIDER_NOTE,
"current_is_oauth": current_is_oauth,
"current": {
"provider": current_provider,
"model": current_model
or _SUPPORTED_PROVIDER_SETUPS.get(current_provider, {}).get(
"default_model", ""
),
"base_url": current_base_url,
},
}
def get_onboarding_status() -> dict:
settings = load_settings()
cfg = get_config()
imports_ok, missing, errors = verify_hermes_imports()
runtime = _status_from_runtime(cfg, imports_ok)
workspaces = load_workspaces()
last_workspace = get_last_workspace()
available_models = get_available_models()
# HERMES_WEBUI_SKIP_ONBOARDING=1 lets hosting providers (e.g. Agent37) ship
# a pre-configured instance without the wizard blocking the first load.
# This is an operator-level override and is honoured unconditionally —
# the operator knows their deployment is configured; we must not second-guess
# it by requiring chat_ready to also be true.
skip_env = os.environ.get("HERMES_WEBUI_SKIP_ONBOARDING", "").strip()
skip_requested = skip_env in {"1", "true", "yes"}
auto_completed = skip_requested # unconditional: operator says skip, we skip
# Auto-complete for existing Hermes users: if config.yaml already exists
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056) * feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS catalog so they appear in the model picker for the openai, openai-codex, and copilot providers. Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent * fix(models): add gpt-5.5-mini to copilot provider catalog * fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044) Mermaid's built-in 'dark' and 'default' themes inject an @import for fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src only allows cdn.jsdelivr.net, so this request is blocked on every diagram render, filling the console with CSP errors. Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables block of mermaid.initialize() in renderMermaidBlocks(). This suppresses Mermaid's external font import and uses the page's existing font stack. Avoids adding fonts.googleapis.com to the CSP — no new external dependency, no font FOUT, consistent with the rest of the UI typography. 3 regression tests added in tests/test_1044_mermaid_csp_font.py. 2215/2215 tests passing. * fix(onboarding): non-standard provider/path cluster (#1029) * fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045) The pageshow handler added for #822 only cleared the session search filter and re-rendered the session list. This left the rest of the layout chrome (topbar, rail icons, workspace panel, resize handles, gateway SSE) in the stale bfcache DOM state, causing a broken layout (oversized search icon, uninitialized rail) that required a hard refresh to fix. Fix: extend the pageshow handler to re-run the full set of layout sync calls that the boot IIFE runs on a fresh page load: syncTopbar() — restores model chip, title, topbar state syncWorkspacePanelState() — restores workspace panel open/closed _initResizePanels() — reattaches panel resize drag listeners startGatewaySSE() — reconnects the gateway SSE watcher (bfcache-persisted connections are dead) All four calls are typeof-guarded for safe degradation if a helper is not yet defined. The existing #822 fixes (sessionSearch clear + renderSessionListFromCache) are preserved unchanged. loadSession() is intentionally NOT re-called — it would cause message flicker; the sync calls above are sufficient to restore visual state. 7 regression tests added in tests/test_1045_bfcache_layout_restore.py. 2219/2219 tests passing. * fix(bfcache): also close open dropdowns on bfcache restore (#1045) Additional symptom noted in issue #1045: bfcache freezes the DOM including any open dropdown/popover state. The thinking-level selector (and other composer dropdowns) left open when navigating away would appear open without user interaction on tab restore. Extend the pageshow handler to call all four named close functions before the layout sync: closeModelDropdown() — composer model selector closeReasoningDropdown() — thinking/reasoning effort selector closeWsDropdown() — workspace chip dropdown closeProfileDropdown() — profile switcher dropdown All calls are typeof-guarded, matching the style of the layout sync calls already in the handler. 2 new tests (9 total in test_1045_bfcache_layout_restore.py): - pageshow closes all four named dropdowns - dropdown closes appear before layout sync calls (clean state first) 2221/2221 tests passing. * fix(bfcache): remove _initResizePanels() — bfcache preserves listeners * fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test * fix(sessions): use cron job name as session title when available (#1032) * fix(test): add id column to messages table in cron title test fixture * fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block * fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038) When an auth session expires, the server returns a 302→/login for page requests. In a normal browser this works fine, but in an iOS PWA running in standalone mode the redirect navigates out of the PWA shell into Safari, leaving the app permanently stuck on 'Authentication required' with no recovery path. Fix: intercept 401 responses client-side before surfacing any error. - workspace.js api(): check res.status===401 first; call window.location.href='/login' and return immediately (no throw) - ui.js: add _redirectIfUnauth() helper; wire into all direct fetch() calls that bypass api() — api/models, api/models/live, api/upload All fetch paths that could receive a 401 now redirect cleanly within the PWA frame rather than opening Safari. 6 regression tests added in tests/test_1038_pwa_auth_redirect.py. 2175/2175 tests passing. * fix(pwa): preserve current URL in ?next= param on 401 redirect * fix(test): update 401-redirect assertion to accept ?next= URL format * feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by the login success handler (always redirected to ./). _safeNextPath() validates and returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs, // protocol-relative URLs, backslash variants, and control characters. 4 new regression tests added. * Implement session agent cache for AIAgent reuse Added session agent cache to reuse AIAgent across messages. * Implement agent caching for session management * Implement session agent eviction on session deletion Added session agent eviction to prevent turn count leakage in recycled sessions. * docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27) * docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210 Three entries in the [Unreleased] section are duplicates of items now listed under v0.50.210: - Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP - bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore - iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL The original drafts landed in [Unreleased] when individual PRs (#1047, #1048, #1043) were approved; the v0.50.210 release-notes commit then added the same items under the version section without removing the [Unreleased] copies. Drop the duplicates so users reading the CHANGELOG don't see the same fix listed twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: qxxaa <mrhanoi@outlook.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:44 +00:00
# AND the provider is configured (or the system is chat_ready), treat onboarding
# as done. These users configured Hermes via the CLI before the Web UI existed;
# they must never be shown the first-run wizard — it would silently overwrite their
# config. We use provider_configured (not chat_ready) so that users with
# non-wizard providers (ollama-cloud, deepseek, xai, kimi, etc.) are not forced
# through the wizard just because their provider doesn't have a detectable API key
# — the wizard cannot represent their provider and would overwrite their config
# with whichever wizard-supported provider they accidentally select.
config_exists = Path(_get_config_path()).exists()
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056) * feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS catalog so they appear in the model picker for the openai, openai-codex, and copilot providers. Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent * fix(models): add gpt-5.5-mini to copilot provider catalog * fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044) Mermaid's built-in 'dark' and 'default' themes inject an @import for fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src only allows cdn.jsdelivr.net, so this request is blocked on every diagram render, filling the console with CSP errors. Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables block of mermaid.initialize() in renderMermaidBlocks(). This suppresses Mermaid's external font import and uses the page's existing font stack. Avoids adding fonts.googleapis.com to the CSP — no new external dependency, no font FOUT, consistent with the rest of the UI typography. 3 regression tests added in tests/test_1044_mermaid_csp_font.py. 2215/2215 tests passing. * fix(onboarding): non-standard provider/path cluster (#1029) * fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045) The pageshow handler added for #822 only cleared the session search filter and re-rendered the session list. This left the rest of the layout chrome (topbar, rail icons, workspace panel, resize handles, gateway SSE) in the stale bfcache DOM state, causing a broken layout (oversized search icon, uninitialized rail) that required a hard refresh to fix. Fix: extend the pageshow handler to re-run the full set of layout sync calls that the boot IIFE runs on a fresh page load: syncTopbar() — restores model chip, title, topbar state syncWorkspacePanelState() — restores workspace panel open/closed _initResizePanels() — reattaches panel resize drag listeners startGatewaySSE() — reconnects the gateway SSE watcher (bfcache-persisted connections are dead) All four calls are typeof-guarded for safe degradation if a helper is not yet defined. The existing #822 fixes (sessionSearch clear + renderSessionListFromCache) are preserved unchanged. loadSession() is intentionally NOT re-called — it would cause message flicker; the sync calls above are sufficient to restore visual state. 7 regression tests added in tests/test_1045_bfcache_layout_restore.py. 2219/2219 tests passing. * fix(bfcache): also close open dropdowns on bfcache restore (#1045) Additional symptom noted in issue #1045: bfcache freezes the DOM including any open dropdown/popover state. The thinking-level selector (and other composer dropdowns) left open when navigating away would appear open without user interaction on tab restore. Extend the pageshow handler to call all four named close functions before the layout sync: closeModelDropdown() — composer model selector closeReasoningDropdown() — thinking/reasoning effort selector closeWsDropdown() — workspace chip dropdown closeProfileDropdown() — profile switcher dropdown All calls are typeof-guarded, matching the style of the layout sync calls already in the handler. 2 new tests (9 total in test_1045_bfcache_layout_restore.py): - pageshow closes all four named dropdowns - dropdown closes appear before layout sync calls (clean state first) 2221/2221 tests passing. * fix(bfcache): remove _initResizePanels() — bfcache preserves listeners * fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test * fix(sessions): use cron job name as session title when available (#1032) * fix(test): add id column to messages table in cron title test fixture * fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block * fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038) When an auth session expires, the server returns a 302→/login for page requests. In a normal browser this works fine, but in an iOS PWA running in standalone mode the redirect navigates out of the PWA shell into Safari, leaving the app permanently stuck on 'Authentication required' with no recovery path. Fix: intercept 401 responses client-side before surfacing any error. - workspace.js api(): check res.status===401 first; call window.location.href='/login' and return immediately (no throw) - ui.js: add _redirectIfUnauth() helper; wire into all direct fetch() calls that bypass api() — api/models, api/models/live, api/upload All fetch paths that could receive a 401 now redirect cleanly within the PWA frame rather than opening Safari. 6 regression tests added in tests/test_1038_pwa_auth_redirect.py. 2175/2175 tests passing. * fix(pwa): preserve current URL in ?next= param on 401 redirect * fix(test): update 401-redirect assertion to accept ?next= URL format * feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by the login success handler (always redirected to ./). _safeNextPath() validates and returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs, // protocol-relative URLs, backslash variants, and control characters. 4 new regression tests added. * Implement session agent cache for AIAgent reuse Added session agent cache to reuse AIAgent across messages. * Implement agent caching for session management * Implement session agent eviction on session deletion Added session agent eviction to prevent turn count leakage in recycled sessions. * docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27) * docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210 Three entries in the [Unreleased] section are duplicates of items now listed under v0.50.210: - Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP - bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore - iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL The original drafts landed in [Unreleased] when individual PRs (#1047, #1048, #1043) were approved; the v0.50.210 release-notes commit then added the same items under the version section without removing the [Unreleased] copies. Drop the duplicates so users reading the CHANGELOG don't see the same fix listed twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: qxxaa <mrhanoi@outlook.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:44 +00:00
# For providers not in the wizard's quick-setup list (e.g. ollama-cloud, deepseek,
# xai, kimi-k2.6), the wizard can never help — it only knows how to configure
# openrouter/anthropic/openai/google/custom. If such a user has a configured
# provider + model in config.yaml, showing the wizard would only confuse them
# (or worse, let them accidentally overwrite their config with gpt-5.4-mini).
_current_provider = str(
(cfg.get("model", {}) or {}).get("provider", "") if isinstance(cfg.get("model"), dict)
else ""
).strip().lower()
_is_non_wizard_provider = bool(
_current_provider and _current_provider not in _SUPPORTED_PROVIDER_SETUPS
)
config_auto_completed = config_exists and (
bool(runtime.get("chat_ready"))
or (_is_non_wizard_provider and bool(runtime.get("provider_configured")))
)
# Persist the flag so it survives future transient import failures (e.g. after
# a git branch switch in the hermes-agent repo). Without this, a CLI-configured
# user who never ran the wizard has no onboarding_completed flag — any momentary
# imports_ok=False during restart makes chat_ready=False, config_auto_completed=False,
# and the wizard reappears with a broken dropdown that clobbers their config.
#
# Best-effort: if save_settings raises (read-only FS, disk full, permission error),
# log and continue. The `config_auto_completed` branch of `completed=` below still
# returns True for this request, so the user sees the correct state — only the
# persistence-across-restart guarantee is degraded. Raising here would turn every
# /api/onboarding/status call into a 500 until disk was writable, which is worse UX
# than losing the next-restart protection.
if config_auto_completed and not settings.get("onboarding_completed"):
try:
save_settings({"onboarding_completed": True})
settings["onboarding_completed"] = True
except Exception:
logger.debug("Failed to persist onboarding_completed", exc_info=True)
return {
"completed": bool(settings.get("onboarding_completed")) or auto_completed or config_auto_completed,
"settings": {
"default_model": settings.get("default_model") or DEFAULT_MODEL,
"default_workspace": settings.get("default_workspace")
or str(DEFAULT_WORKSPACE),
"password_enabled": is_auth_enabled(),
"bot_name": settings.get("bot_name") or "Hermes",
},
"system": {
"hermes_found": bool(_HERMES_FOUND),
"imports_ok": bool(imports_ok),
"missing_modules": missing,
"import_errors": errors,
"config_path": str(_get_config_path()),
"config_exists": Path(_get_config_path()).exists(),
**runtime,
},
"setup": _build_setup_catalog(cfg),
"workspaces": {
"items": workspaces,
"last": last_workspace,
},
"models": available_models,
}
def apply_onboarding_setup(body: dict) -> dict:
# Hard guard: if the operator set SKIP_ONBOARDING, the wizard should never
# have appeared. Even if the frontend somehow calls this endpoint anyway
# (e.g. a stale JS bundle or a curious user), we must not overwrite the
# operator's config.yaml or .env files. Just mark onboarding complete and
# return the current status — no file writes.
skip_env = os.environ.get("HERMES_WEBUI_SKIP_ONBOARDING", "").strip()
if skip_env in {"1", "true", "yes"}:
save_settings({"onboarding_completed": True})
return get_onboarding_status()
provider = str(body.get("provider") or "").strip().lower()
model = str(body.get("model") or "").strip()
api_key = str(body.get("api_key") or "").strip()
base_url = _normalize_base_url(str(body.get("base_url") or ""))
if provider not in _SUPPORTED_PROVIDER_SETUPS:
# Unsupported providers (openai-codex, copilot, nous, etc.) are already
# configured via the CLI. Just mark onboarding as complete and let the
# user through — the agent is already set up, no further setup needed.
save_settings({"onboarding_completed": True})
return get_onboarding_status()
if not model:
raise ValueError("model is required")
provider_meta = _SUPPORTED_PROVIDER_SETUPS[provider]
if provider_meta.get("requires_base_url"):
if not base_url:
raise ValueError("base_url is required for custom endpoints")
parsed = urlparse(base_url)
if parsed.scheme not in {"http", "https"}:
raise ValueError("base_url must start with http:// or https://")
config_path = _get_config_path()
# Guard: if config.yaml already exists and the caller did not explicitly
# acknowledge the overwrite, refuse to proceed. The frontend must pass
# confirm_overwrite=True after showing the user a confirmation step.
if Path(config_path).exists() and not body.get("confirm_overwrite"):
return {
"error": "config_exists",
"message": (
"Hermes is already configured (config.yaml exists). "
"Pass confirm_overwrite=true to overwrite it."
),
"requires_confirm": True,
}
cfg = _load_yaml_config(config_path)
env_path = _get_active_hermes_home() / ".env"
env_values = _load_env_file(env_path)
if not api_key and not _provider_api_key_present(provider, cfg, env_values):
fix(onboarding): allow keyless setup for self-hosted providers (#1499 third sub-bug) Pre-fix, the wizard rejected an empty api_key for every provider in _SUPPORTED_PROVIDER_SETUPS — including lmstudio, ollama, and custom, which run keyless on the vast majority of local installs. The agent's LMSTUDIO_NOAUTH_PLACEHOLDER substitution at chat-time was the workaround for the no-auth case, but the wizard side rejected the empty input first. Users had to type random gibberish into the API key field to clear the form — the third sub-bug from #1420 that the prior commit's PR description explicitly punted to a follow-up. Surfaced by Nathan during PR review: "I think it's too weird for users to have to type a string into the API key field, right?" Yes — and the probe (#1499) makes the cleanest fix strictly better: we accept empty keys, and the probe gives instant feedback ("Connected. 2 model(s) available." for keyless servers, "401" for auth-required servers). Backend changes --------------- * `api/onboarding.py` — `_SUPPORTED_PROVIDER_SETUPS` gains `key_optional: True` for `lmstudio`, `ollama`, `custom`. Cloud providers (openrouter, anthropic, openai, gemini, deepseek, …) remain key_required. * `apply_onboarding_setup` skips the "{env_var} is required" check when `key_optional` is set AND no key is supplied. No write to .env for the empty-key case (no `LM_API_KEY=*** placeholder lying in the user's .env`). * `_status_from_runtime` reports `provider_ready=True` for key_optional providers based on `requires_base_url` alone, so the wizard doesn't refire on the next page load just because there's no api_key. Cloud providers still need a key for provider_ready=True. * `_build_setup_catalog` exposes the `key_optional` flag to the frontend. Frontend changes ---------------- * `static/onboarding.js` — new `_renderOnboardingApiKeyField()` helper. For key_optional providers: - Label: "API key (optional)" - Placeholder: "Leave blank for keyless servers" - Inline italic muted help: "Most LM Studio / Ollama / vLLM installs run keyless — leave this blank if your server doesn't require authentication. Use the Test connection button to verify." For cloud providers: unchanged (label "API key", standard placeholder, no help block). * The api-key input also now triggers `_scheduleOnboardingProbe()` on oninput, so changing the key re-runs the probe — handles "the server rejected my empty key with 401, let me add one and retry." * `static/i18n.js` — 3 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8). * `static/style.css` — `.onboarding-api-key-help` rule for the muted italic helper paragraph. Verified end-to-end on port 8789 -------------------------------- Spun up an isolated test server + a mock LM Studio at `127.0.0.1:11234/v1/models`. Stepped through the wizard: * Picked LM Studio → field label flipped to "API key (optional)", placeholder showed "Leave blank for keyless servers", help text rendered in italic muted gray below. * Switched to Anthropic → label reverted to "API key", help text disappeared. Visual hierarchy correct. * Left api_key blank, set base_url to the mock, clicked Test connection → green "Connected. 2 model(s) available." banner. Probe-discovered models populated the workspace-step dropdown. * Continued through to the finish step. config.yaml written with provider/model/base_url. **`.env` does NOT exist** — no placeholder string written. `chat_ready: true`, `state: ready`. * Vision tool confirmed the visual hierarchy: subtle italic help reads as documentation, prominent green banner pops as status. Tests ----- `tests/test_issue1499_keyless_onboarding.py` — 16 tests in 3 classes: TestKeyOptionalProviderSchema (5) - lmstudio / ollama / custom declare key_optional=True - openrouter / anthropic / openai do NOT (regression defense) - setup catalog exposes the flag TestKeylessOnboarding (6) - lmstudio / ollama / custom: empty api_key accepted, no .env write - openrouter / anthropic: empty api_key still rejected - lmstudio with explicit key still writes .env (regression defense) TestKeylessChatReady (5) - lmstudio / ollama: provider_ready=True with no key - custom: provider_ready=True with key+base_url, False without base_url - openrouter: provider_ready=False with no key (regression defense) - End-to-end get_onboarding_status reports chat_ready=True Full suite: 3901 → 3917 passing (+16 from this commit; +22 cumulative from the PR's earlier commit). 0 failures. Closes #1499 (all three sub-bugs from #1420 now addressed)
2026-05-03 03:07:07 +00:00
# Providers that may run keyless (lmstudio, ollama, custom — gated by
# `key_optional` in _SUPPORTED_PROVIDER_SETUPS) are allowed to onboard
# with no api_key. OAuth-capable wizard providers (currently Anthropic
# via Claude Code) are also allowed once their server-side OAuth/link
# marker is present.
oauth_ready = bool(provider_meta.get("oauth_provider")) and _provider_oauth_authenticated(
str(provider_meta.get("oauth_provider")), _get_active_hermes_home()
)
if not provider_meta.get("key_optional") and not oauth_ready:
fix(onboarding): allow keyless setup for self-hosted providers (#1499 third sub-bug) Pre-fix, the wizard rejected an empty api_key for every provider in _SUPPORTED_PROVIDER_SETUPS — including lmstudio, ollama, and custom, which run keyless on the vast majority of local installs. The agent's LMSTUDIO_NOAUTH_PLACEHOLDER substitution at chat-time was the workaround for the no-auth case, but the wizard side rejected the empty input first. Users had to type random gibberish into the API key field to clear the form — the third sub-bug from #1420 that the prior commit's PR description explicitly punted to a follow-up. Surfaced by Nathan during PR review: "I think it's too weird for users to have to type a string into the API key field, right?" Yes — and the probe (#1499) makes the cleanest fix strictly better: we accept empty keys, and the probe gives instant feedback ("Connected. 2 model(s) available." for keyless servers, "401" for auth-required servers). Backend changes --------------- * `api/onboarding.py` — `_SUPPORTED_PROVIDER_SETUPS` gains `key_optional: True` for `lmstudio`, `ollama`, `custom`. Cloud providers (openrouter, anthropic, openai, gemini, deepseek, …) remain key_required. * `apply_onboarding_setup` skips the "{env_var} is required" check when `key_optional` is set AND no key is supplied. No write to .env for the empty-key case (no `LM_API_KEY=*** placeholder lying in the user's .env`). * `_status_from_runtime` reports `provider_ready=True` for key_optional providers based on `requires_base_url` alone, so the wizard doesn't refire on the next page load just because there's no api_key. Cloud providers still need a key for provider_ready=True. * `_build_setup_catalog` exposes the `key_optional` flag to the frontend. Frontend changes ---------------- * `static/onboarding.js` — new `_renderOnboardingApiKeyField()` helper. For key_optional providers: - Label: "API key (optional)" - Placeholder: "Leave blank for keyless servers" - Inline italic muted help: "Most LM Studio / Ollama / vLLM installs run keyless — leave this blank if your server doesn't require authentication. Use the Test connection button to verify." For cloud providers: unchanged (label "API key", standard placeholder, no help block). * The api-key input also now triggers `_scheduleOnboardingProbe()` on oninput, so changing the key re-runs the probe — handles "the server rejected my empty key with 401, let me add one and retry." * `static/i18n.js` — 3 new keys × 9 locales (canonical English in `en`, English fallback with `// TODO: translate` markers in the other 8). * `static/style.css` — `.onboarding-api-key-help` rule for the muted italic helper paragraph. Verified end-to-end on port 8789 -------------------------------- Spun up an isolated test server + a mock LM Studio at `127.0.0.1:11234/v1/models`. Stepped through the wizard: * Picked LM Studio → field label flipped to "API key (optional)", placeholder showed "Leave blank for keyless servers", help text rendered in italic muted gray below. * Switched to Anthropic → label reverted to "API key", help text disappeared. Visual hierarchy correct. * Left api_key blank, set base_url to the mock, clicked Test connection → green "Connected. 2 model(s) available." banner. Probe-discovered models populated the workspace-step dropdown. * Continued through to the finish step. config.yaml written with provider/model/base_url. **`.env` does NOT exist** — no placeholder string written. `chat_ready: true`, `state: ready`. * Vision tool confirmed the visual hierarchy: subtle italic help reads as documentation, prominent green banner pops as status. Tests ----- `tests/test_issue1499_keyless_onboarding.py` — 16 tests in 3 classes: TestKeyOptionalProviderSchema (5) - lmstudio / ollama / custom declare key_optional=True - openrouter / anthropic / openai do NOT (regression defense) - setup catalog exposes the flag TestKeylessOnboarding (6) - lmstudio / ollama / custom: empty api_key accepted, no .env write - openrouter / anthropic: empty api_key still rejected - lmstudio with explicit key still writes .env (regression defense) TestKeylessChatReady (5) - lmstudio / ollama: provider_ready=True with no key - custom: provider_ready=True with key+base_url, False without base_url - openrouter: provider_ready=False with no key (regression defense) - End-to-end get_onboarding_status reports chat_ready=True Full suite: 3901 → 3917 passing (+16 from this commit; +22 cumulative from the PR's earlier commit). 0 failures. Closes #1499 (all three sub-bugs from #1420 now addressed)
2026-05-03 03:07:07 +00:00
raise ValueError(f"{provider_meta['env_var']} is required")
model_cfg = cfg.get("model", {})
if not isinstance(model_cfg, dict):
model_cfg = {}
model_cfg["provider"] = provider
model_cfg["default"] = _normalize_model_for_provider(provider, model)
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
if provider_meta.get("requires_base_url"):
model_cfg["base_url"] = base_url
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042) * feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix (80cdaee) stripped filesystem paths from e.message but still displayed the JS exception message to users. Per reviewer feedback and project convention (NEVER expose raw e.message in UI), replace with: - A generic user-facing i18n key (settings_update_check_failed) as default - Fallback to API response body error if available (structured, not raw) - Full error logged via console.warn for debugging - Button disable-during-check already confirmed working (try/finally pattern) - settings_update_check_failed key added in all 6 locales * fix(#785): align HTML selectors with CSS and add regression tests - Wrap update button in div#checkUpdatesBlock so CSS selectors apply - Change button class from sm-btn to btn-tiny (matching stylesheet) - Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny) - Move spinner sizing to CSS class .spinner-xs - Add 4 static tests in test_update_banner_fixes.py: checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales * feat: 'Keep workspace panel open' toggle in Appearance settings (#999) * feat: categorize providers in setup wizard (#603) - Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok), Ollama, LM Studio to the onboarding quick-setup catalog - Group providers into 3 categories: Easy start, Open/self-hosted, Specialized — rendered as <optgroup> in the provider dropdown - Generic base_url save logic (requires_base_url + default_base_url) instead of hardcoded provider checks - i18n keys for category labels in en, ru, es, zh, zh-Hant * ci: re-run tests * fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644 The test helper _available_models_with_cfg patches cfg in-memory but get_available_models() calls reload_config() when the config file's mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0 and _cfg_mtime starts at 0.0, triggering a reload that overwrites the test's mock with on-disk content. Fix: freeze _cfg_mtime to the current config file mtime inside the helper, so reload_config() is not triggered during the test. * fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests - gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview - x-ai: grok-4.20 → grok-3 - deepseek: deepseek-chat-v3-0324 → deepseek-chat - Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for gemini, deepseek, mistral, and x-ai through apply_onboarding_setup * test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai * fix(onboarding): correct stale model defaults for specialized providers Three issues in the new specialized provider catalog (#1027 hold reason): 1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog has the 3.1 family. Updated to `gemini-3.1-pro-preview`. 2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`. Updated. 3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")` which returns []. The catalog in api/config.py is keyed under "google" (even though the agent's alias map normalizes google -> gemini). Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces the actual 5-model list. Also forward-compatible lookup for x-ai (xai or x-ai key). Without these fixes, users picking gemini or x-ai in the wizard would see no model dropdown and the default_model written to config.yaml would 404 on first chat. deepseek default_model bumped from `deepseek-chat` to `deepseek-chat-v3-0324` to match the test fixture's expectation and the agent catalog's pinned version. Added two regression tests: - test_gemini_model_list_is_populated: pins the catalog-key correctness - test_specialized_default_models_match_catalog: pins the version prefixes (3.x for gemini, 4.x for grok) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: inline HTML preview in workspace panel (#779) Render .html/.htm files as live previews in a sandboxed iframe instead of showing raw source code. Adds an 'Open in browser' button to open the file in a new tab. Changes: - workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing in openFile(), and openInBrowser() function - index.html: add sandboxed iframe element and 'Open in browser' button in preview toolbar (visible only for HTML files) - i18n.js: add 'open_in_browser' key in all 6 locales The iframe uses sandbox='allow-scripts' for security. Download button remains available alongside the new preview. * docs: document sandbox security tradeoff for HTML preview Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work. Added code comment explaining the deliberate sandbox=allow-scripts choice: scripts are needed for most HTML documents but the iframe is still origin- isolated and cannot access parent cookies/data. * fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading routes.py: add inline_preview param — bypasses Content-Disposition:attachment for text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe. workspace.js: add &inline=1 to the iframe src URL. test: add 5 static regression tests for the inline HTML preview. * fix(security): CSP sandbox header for inline HTML preview The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only applies when HTML is loaded INSIDE that iframe. A user tricked into opening /api/file/raw?path=evil.html&inline=1 directly in a top-level tab (e.g. via a chat link) would render the HTML in the WebUI's origin without any sandbox, giving the page full access to cookies and localStorage. Server-side Content-Security-Policy: sandbox allow-scripts mirrors the iframe sandbox exactly: scripts run, but the document is treated as a unique opaque origin (no allow-same-origin) and cannot read WebUI cookies, localStorage, or postMessage to the parent regardless of how the URL is accessed. Added test_inline_html_response_sets_csp_sandbox to pin the header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43) * docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209 The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in without a corresponding CHANGELOG entry. Without this fix, the queue feature ships silently and the bundled Cloudflare CSP relaxation in api/helpers.py is also undocumented. Adds two entries: - Added: queue flyout (#1040) under v0.50.209 - Changed: CSP allowlist for Cloudflare Access deployments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
elif provider_meta.get("default_base_url"):
model_cfg["base_url"] = provider_meta["default_base_url"]
else:
model_cfg.pop("base_url", None)
cfg["model"] = model_cfg
_save_yaml_config(config_path, cfg)
if api_key:
_write_env_file(env_path, {provider_meta["env_var"]: api_key})
# Reload the hermes_cli provider/config cache so the next streaming call
# picks up the new key without requiring a server restart.
try:
from api.profiles import _reload_dotenv
_reload_dotenv(_get_active_hermes_home())
except Exception:
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) * security: fix bandit security issues (B310, B324) - Add usedforsecurity=False to MD5 hash in gateway_watcher.py - Add URL scheme validation to prevent file:// access in config.py - Add URL validation to bootstrap.py health check - Add nosec comments where runtime validation exists * fix: handle ConnectionResetError gracefully and add debug logging - Add QuietHTTPServer class to suppress noisy connection reset errors caused by clients disconnecting abruptly (fixes log spam from 'ConnectionResetError: [Errno 54] Connection reset by peer') - Replace silent 'pass' statements with logger.debug() calls across api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py, and api/onboarding.py for better observability during troubleshooting - All tests pass (25 passed in test_regressions.py) * chore: add debug logging to profiles and routes modules - Replace silent 'pass' statements with logger.debug() calls in api/profiles.py for better error visibility during profile switching and module patching - Add logger initialization to api/routes.py * security: fix B110 bare except/pass issues (bandit security scan) - Replace bare except/pass patterns with logger.debug() calls - Fixes CWE-703 (improper check/handling of exceptional conditions) - Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py - All tests pass successfully * security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) - api/gateway_watcher.py: MD5 usedforsecurity=False (B324) - api/config.py, bootstrap.py: URL scheme validation before urlopen (B310) - 12 files: replace bare except/pass with logger.debug() (B110) - server.py: QuietHTTPServer suppresses client disconnect log noise - server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail) - tests/test_sprint43.py: 19 new tests covering all security fixes - CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822) --------- Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger.debug("Failed to reload dotenv")
# Belt-and-braces: set directly on os.environ AFTER _reload_dotenv so the
# value survives even if _reload_dotenv cleared it (e.g. when _write_env_file
# wrote to disk but the profile isolation tracking hasn't seen it yet).
if api_key:
os.environ[provider_meta["env_var"]] = api_key
try:
# hermes_cli may cache config at import time; ask it to reload if possible.
from hermes_cli.config import reload as _cli_reload
_cli_reload()
except Exception:
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) * security: fix bandit security issues (B310, B324) - Add usedforsecurity=False to MD5 hash in gateway_watcher.py - Add URL scheme validation to prevent file:// access in config.py - Add URL validation to bootstrap.py health check - Add nosec comments where runtime validation exists * fix: handle ConnectionResetError gracefully and add debug logging - Add QuietHTTPServer class to suppress noisy connection reset errors caused by clients disconnecting abruptly (fixes log spam from 'ConnectionResetError: [Errno 54] Connection reset by peer') - Replace silent 'pass' statements with logger.debug() calls across api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py, and api/onboarding.py for better observability during troubleshooting - All tests pass (25 passed in test_regressions.py) * chore: add debug logging to profiles and routes modules - Replace silent 'pass' statements with logger.debug() calls in api/profiles.py for better error visibility during profile switching and module patching - Add logger initialization to api/routes.py * security: fix B110 bare except/pass issues (bandit security scan) - Replace bare except/pass patterns with logger.debug() calls - Fixes CWE-703 (improper check/handling of exceptional conditions) - Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py - All tests pass successfully * security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) - api/gateway_watcher.py: MD5 usedforsecurity=False (B324) - api/config.py, bootstrap.py: URL scheme validation before urlopen (B310) - 12 files: replace bare except/pass with logger.debug() (B110) - server.py: QuietHTTPServer suppresses client disconnect log noise - server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail) - tests/test_sprint43.py: 19 new tests covering all security fixes - CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822) --------- Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger.debug("Failed to reload hermes_cli config")
reload_config()
return get_onboarding_status()
def apply_self_hosted_provider_setup(body: dict) -> dict:
provider = str(body.get("provider") or "").strip().lower()
model = str(body.get("model") or "").strip()
api_key = str(body.get("api_key") or "").strip()
base_url = _normalize_base_url(str(body.get("base_url") or ""))
activate = body.get("activate")
do_activate = activate is None or bool(activate)
if provider not in {"ollama", "lmstudio"}:
raise ValueError(f"unsupported self-hosted provider: {provider}")
if not model:
raise ValueError("model is required")
provider_meta = _SUPPORTED_PROVIDER_SETUPS.get(provider, {})
if provider_meta.get("requires_base_url"):
if not base_url:
raise ValueError("base_url is required for this provider")
parsed = urlparse(base_url)
if parsed.scheme not in {"http", "https"}:
raise ValueError("base_url must start with http:// or https://")
config_path = _get_config_path()
cfg = _load_yaml_config(config_path)
providers_cfg = cfg.setdefault("providers", {})
if not isinstance(providers_cfg, dict):
providers_cfg = {}
cfg["providers"] = providers_cfg
provider_cfg = providers_cfg.setdefault(provider, {})
if not isinstance(provider_cfg, dict):
provider_cfg = {}
providers_cfg[provider] = provider_cfg
provider_cfg["base_url"] = base_url
model_cfg = cfg.get("model", {})
if not isinstance(model_cfg, dict):
model_cfg = {}
original_model_cfg = dict(model_cfg)
env_var = provider_meta.get("env_var")
if do_activate:
model_cfg["provider"] = provider
model_cfg["default"] = _normalize_model_for_provider(provider, model)
model_cfg["base_url"] = base_url
cfg["model"] = model_cfg
elif "model" in cfg:
cfg["model"] = original_model_cfg
_save_yaml_config(config_path, cfg)
if api_key and env_var:
_write_env_file(_get_active_hermes_home() / ".env", {env_var: api_key})
os.environ[env_var] = api_key
try:
from api.profiles import _reload_dotenv
_reload_dotenv(_get_active_hermes_home())
except Exception:
logger.debug("Failed to reload dotenv")
try:
# hermes_cli may cache config at import time; ask it to reload if possible.
from hermes_cli.config import reload as _cli_reload
_cli_reload()
except Exception:
logger.debug("Failed to reload hermes_cli config")
invalidate_models_cache()
result = {"ok": True, "provider": provider, "base_url": base_url}
if do_activate:
result["model"] = model_cfg.get("default")
return result
def complete_onboarding() -> dict:
save_settings({"onboarding_completed": True})
return get_onboarding_status()