simplify(compat): web_server — drop 221 re-exports (config/status/shutil/run_in_threadpool, lifecycle, 13 web_server_<concern> blocks, 47 route-handler legacy re-exports); web_deps.late()/LateState() take an owning-module arg; concern modules import each other directly (62 lazy sites)

This commit is contained in:
Teknium
2026-09-03 14:21:32 -07:00
parent da7ee6353e
commit 5f1feb5344
16 changed files with 60 additions and 261 deletions
+21 -14
View File
@@ -1,30 +1,35 @@
"""Late-binding dependency seam for extracted dashboard routers.
``web_server`` owns all dashboard state/helpers; routers under ``web_routers/`` cannot
import it at import time (web_server imports them to mount them — a cycle) and must not
copy its state (tests ``monkeypatch.setattr(web_server, ...)`` and expect that to win).
``late(name)`` / ``LateState(name)`` resolve ``web_server.<name>`` *at call time*.
``web_server`` owns the dashboard app and its process state; routers under
``web_routers/`` cannot import it at import time (web_server imports them to mount them —
a cycle) and must not copy its state (tests ``monkeypatch.setattr(web_server, ...)`` and
expect that to win). ``late(name)`` / ``LateState(name)`` resolve ``<module>.<name>`` *at
call time*; ``module`` defaults to ``web_server`` and may name a ``web_server_<concern>``
module whose helper tests monkeypatch there.
"""
from __future__ import annotations
import importlib
import sys
from typing import Any
WEB_SERVER = "hermes_cli.web_server"
def _server():
"""Return the live ``hermes_cli.web_server`` module (imported on demand)."""
mod = sys.modules.get("hermes_cli.web_server")
def _server(module: str = WEB_SERVER):
"""Return the live ``module`` (imported on demand)."""
mod = sys.modules.get(module)
if mod is None: # pragma: no cover - routers are only mounted by web_server
import hermes_cli.web_server as mod # type: ignore[no-redef]
mod = importlib.import_module(module)
return mod
def late(name: str):
"""Late-binding proxy for a callable defined on ``web_server``."""
def late(name: str, module: str = WEB_SERVER):
"""Late-binding proxy for a callable defined on ``module``."""
def _proxy(*args: Any, **kwargs: Any):
return getattr(_server(), name)(*args, **kwargs)
return getattr(_server(module), name)(*args, **kwargs)
_proxy.__name__ = name
_proxy.__qualname__ = name
@@ -39,13 +44,15 @@ class LateState:
defined *after* the router's ``include_router`` point, so a late import would miss it.
"""
__slots__ = ("_name",)
__slots__ = ("_name", "_module")
def __init__(self, name: str) -> None:
def __init__(self, name: str, module: str = WEB_SERVER) -> None:
object.__setattr__(self, "_name", name)
object.__setattr__(self, "_module", module)
def _target(self) -> Any:
return getattr(_server(), object.__getattribute__(self, "_name"))
return getattr(_server(object.__getattribute__(self, "_module")),
object.__getattribute__(self, "_name"))
def __getattr__(self, attr: str) -> Any:
return getattr(self._target(), attr)
+1 -5
View File
@@ -1,8 +1,4 @@
"""Pydantic request/response models for the Hermes dashboard web server.
``web_server`` re-exports every name here, so existing imports like
``from hermes_cli.web_server import ConfigUpdate`` keep working.
"""
"""Pydantic request/response models for the Hermes dashboard web server."""
from __future__ import annotations
+14 -143
View File
@@ -15,7 +15,6 @@ import logging
import os
import re
import secrets
import shutil # noqa: F401 — tests monkeypatch web_server.shutil.which
import subprocess
import sys
import sysconfig
@@ -34,29 +33,12 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from hermes_cli import __version__
from hermes_cli.config import ( # noqa: F401 — late-bound by extracted routers/modules; tests monkeypatch web_server.<name>
cfg_get,
check_config_version,
detect_install_method,
get_hermes_home,
load_config,
load_env,
remove_env_value,
save_config,
save_env_value,
)
from gateway.status import ( # noqa: F401 — late-bound by web_routers/status + tests monkeypatch web_server.<name>
get_running_pid,
get_running_pid_cached,
get_runtime_status_running_pid,
read_runtime_status,
)
from hermes_cli.config import load_config
try:
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from starlette.concurrency import run_in_threadpool # noqa: F401 — late-bound by web_server_cron/routers; tests patch web_server.run_in_threadpool
except ImportError:
# First try lazy-installing the dashboard extras. Only the user actually
# running `hermes dashboard` needs fastapi+uvicorn; lazy install keeps
@@ -67,7 +49,6 @@ except ImportError:
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from starlette.concurrency import run_in_threadpool # noqa: F401
except Exception:
raise SystemExit(
"Web UI requires fastapi and uvicorn.\n"
@@ -78,19 +59,15 @@ WEB_DIST = Path(os.environ["HERMES_WEB_DIST"]) if "HERMES_WEB_DIST" in os.enviro
_log = logging.getLogger(__name__)
from hermes_cli.web_server_lifecycle import ( # noqa: E402,F401 — re-exported; routers/tests reach these via web_server.<name>
from hermes_cli.web_server_lifecycle import ( # noqa: E402
PORT_IN_USE_EXIT_CODE,
_dashboard_forwarded_allow_ips,
_eager_reconcile_own_session_db,
_is_addr_in_use_error,
_is_serve_orphaned,
_maybe_open_browser,
_port_bind_conflict,
_read_bound_port,
_report_port_in_use,
_resolve_restart_drain_timeout,
_start_parent_death_watchdog,
_valid_parent_start_marker,
_warm_gateway_module,
_write_dashboard_ready_file,
_write_machine_sentinel_line,
@@ -779,123 +756,17 @@ async def _dashboard_selftest_loop() -> None:
await _dashboard_selftest_once()
# Helpers extracted into web_server_<concern> modules, re-exported so
# ``web_server.<name>`` stays the late-binding seam (web_deps.late) routers use
# and tests monkeypatch. Every name here has a live web_server.<name> reference.
from hermes_cli.web_server_config import ( # noqa: E402,F401
CONFIG_SCHEMA, _AUX_TASK_SLOTS, _apply_main_model_assignment, _apply_model_assignment_sync,
_dashboard_code_skew_guard, _denormalize_config_from_web, _memory_provider_options,
_normalize_config_for_web, _normalize_main_model_assignment,
_schema_with_dynamic_provider_options, _timezone_options,
)
from hermes_cli.web_models import ( # noqa: E402,F401
ConfigUpdate, WhatsAppOnboardingStart, WhatsAppOnboardingApply, MoaModelSlot, MoaPresetPayload,
MoaConfigPayload, BulkDeleteSessions, CronJobCreate, CronJobUpdate,
AutomationBlueprintInstantiate, MCPServerCreate,
)
from hermes_cli.web_server_gateway import ( # noqa: E402,F401
_ACTION_COMMANDS, _ACTION_IDS, _ACTION_LOG_DIR, _ACTION_LOG_FILES, _ACTION_PROCS,
_ACTION_RESULTS, _TOPOLOGY_CACHE, _TOPOLOGY_CACHE_TTL, _collect_profile_gateway_topology,
_collect_profile_gateway_topology_cached, _dashboard_spawn_executable, _display_system_platform,
_gateway_subcommand, _load_configured_gateway_platforms, _probe_gateway_health,
_profile_gateway_writer_identity, _profile_platform_ports, _restart_gateway_after,
_spawn_hermes_action, _split_text_for_speak_stream, _strip_session_list_rows,
_terminate_desktop_managed_gateway,
)
from hermes_cli.web_server_files import ( # noqa: E402,F401
_dashboard_local_update_managed_externally, _fs_path, _managed_file_entry,
_managed_response_meta, _path_is_under, _resolve_managed_path,
)
from hermes_cli.web_server_memory import ( # noqa: E402,F401
_coerce_bool, _dependency_importable, _discover_memory_provider_statuses, _field_default,
_field_is_set, _field_value, _field_visible, _load_memory_provider, _memory_provider_manifest,
_memory_provider_setup_info, _memory_provider_setup_manifest, _normalize_memory_provider_name,
_normalize_memory_provider_schema, _read_memory_provider_existing_values,
_require_memory_provider_ready, _run_setup_command,
)
from hermes_cli.web_server_profiles import ( # noqa: E402,F401
_profile_cli_args, _hub_action_name, _installed_hub_identifiers, _SKILLS_PROFILE_LOCK,
_TERMINAL_BACKENDS, _approval_mode_of, _aux_task_summary, _aux_usage_rows,
_broadcast_gateway_session_info, _config_profile_scope, _fallback_profile_dicts,
_is_other_profile, _merge_aux_into_by_model, _parse_model_ids, _plugin_terminal_backend_rows,
_profile_scope, _resolve_profile_dir, _write_profile_mcp_servers,
)
from hermes_cli.web_server_messaging import ( # noqa: E402,F401
_MESSAGING_KEYS_PAGE_KEYS, _TelegramOnboardingPairing, _WhatsAppOnboardingSession,
_build_catalog_entry, _channel_managed_env_keys, _messaging_platform_catalog,
_restart_gateway_after_whatsapp_onboarding, _telegram_onboarding_error_message,
_telegram_onboarding_lock, _telegram_onboarding_pairings, _telegram_onboarding_request_sync,
_whatsapp_onboarding_payload, _whatsapp_onboarding_sessions, _whatsapp_session_path,
_write_platform_enabled,
)
from hermes_cli.web_server_oauth import ( # noqa: E402,F401
_OAUTH_PROVIDER_CATALOG, _external_process_cli_command, _minimax_poller, _nous_poller,
_oauth_profile_name, _oauth_sessions, _oauth_sessions_lock, _truncate_token, _xai_device_poller,
)
from hermes_cli.web_server_sessions import ( # noqa: E402,F401
_auto_archive_ticker_loop, _last_auto_archive_check, _maybe_auto_archive_for_profile,
_open_session_db_at_path, _open_session_db_for_profile, _session_db_heal_exhausted,
_session_db_heal_warned, _session_db_read_probe_statements, _session_latest_descendant,
)
from hermes_cli.web_server_cron import ( # noqa: E402,F401
_call_cron_for_profile, _create_cron_job_sync, _cron_default_profile, _cron_optional_text,
_cron_profile_dicts, _cron_profile_home, _cron_string_list, _find_cron_job_profile,
_fire_cron_job_for_profile, _forward_cron_fire_to_gateway, _gateway_fire_endpoint,
_gateway_intentionally_stopped, _mutate_cron_for_profile, _normalize_dashboard_cron_script,
_notify_cron_provider_for_profile, _raise_if_cron_registration_error, _run_cron_dashboard_io,
_validate_dashboard_cron_context_from, _validate_dashboard_cron_effective_job,
)
from hermes_cli.web_server_mcp import ( # noqa: E402,F401
_mcp_oauth_flows, _mcp_server_summary, _normalize_mcp_server_create, _run_dashboard_mcp_oauth,
)
from hermes_cli.web_server_chat import ( # noqa: E402,F401
PTY_REGISTRY, PtyBridge, PtyUnavailableError, _GATEWAY_WS_PROTOCOL,
_GATEWAY_WS_TICKET_PROTOCOL_PREFIX, _LOOPBACK_HOSTS, _PTY_BRIDGE_AVAILABLE, _RESIZE_RE,
_active_session_file_for_channel, _build_gateway_ws_url, _build_sidecar_url,
_get_console_executor, _legacy_pump, _resolve_chat_argv, _resolve_chat_argv_async,
_resolve_client_ws_host, _ws_auth_ok, _ws_auth_reason, _ws_client_is_allowed, _ws_client_reason,
_ws_host_origin_is_allowed, _ws_host_origin_reason, _ws_request_is_allowed,
)
from hermes_cli.web_server_dashboard import ( # noqa: E402,F401
_BUILTIN_DASHBOARD_THEMES, _discover_dashboard_plugins, _discover_user_themes,
_invalidate_plugins_hub_cache, _merged_plugins_hub, _mount_plugin_api_routes,
_normalise_theme_definition, _render_active_theme_bootstrap_css, _safe_plugin_api_relpath,
_schedule_check_fn_probe, mount_spa,
)
# Legacy re-exports of route handlers; tests call these via web_server.<name>.
from hermes_cli.web_routers.files import upload_managed_file_stream # noqa: E402,F401
from hermes_cli.web_routers.status import get_status, run_dump # noqa: E402,F401
from hermes_cli.web_routers.sessions import search_sessions # noqa: E402,F401
from hermes_cli.web_routers.models import ( # noqa: E402,F401
get_model_options, get_recommended_default_model, set_moa_models,
# Action registries/spawner are owned by web_server_gateway; routers and tests reach them
# there, so this module reads them through the module too (one patch seam).
from hermes_cli import web_server_gateway as _gateway_mod # noqa: E402
from hermes_cli.web_server_gateway import _ACTION_LOG_FILES, _terminate_desktop_managed_gateway # noqa: E402
from hermes_cli.web_server_sessions import _auto_archive_ticker_loop # noqa: E402
from hermes_cli.web_server_chat import PTY_REGISTRY # noqa: E402
from hermes_cli.web_server_dashboard import ( # noqa: E402
_discover_dashboard_plugins, _mount_plugin_api_routes, mount_spa,
)
from hermes_cli.web_routers.messaging import ( # noqa: E402,F401
apply_whatsapp_onboarding, start_whatsapp_onboarding,
)
from hermes_cli.web_routers.oauth import ( # noqa: E402,F401
_codex_full_login_worker, _new_oauth_session, _resolve_provider_status,
)
from hermes_cli.web_routers.sessions import ( # noqa: E402,F401
bulk_delete_sessions_endpoint, count_empty_sessions_endpoint, delete_empty_sessions_endpoint,
get_session_latest_descendant, get_session_messages, delete_session_endpoint,
export_session_endpoint, prune_sessions_endpoint,
)
from hermes_cli.web_routers.cron import ( # noqa: E402,F401
list_cron_jobs, create_cron_job, update_cron_job, pause_cron_job, resume_cron_job,
trigger_cron_job, delete_cron_job, instantiate_blueprint, _normalize_dashboard_cron_updates,
)
from hermes_cli.web_routers.ops import ( # noqa: E402,F401
list_credential_pool, run_doctor, run_import,
)
from hermes_cli.web_routers.analytics import ( # noqa: E402,F401
get_models_analytics, get_usage_analytics,
)
from hermes_cli.web_routers.chat_ws import ( # noqa: E402,F401
_broadcast_event, _get_event_state, pty_ws,
)
from hermes_cli.web_routers.dashboard_ui import post_agent_plugin_install # noqa: E402,F401
_GATEWAY_HEALTH_URL = os.getenv("GATEWAY_HEALTH_URL")
@@ -978,10 +849,10 @@ def _spawn_gateway_restart(profile: Optional[str] = None) -> Tuple[subprocess.Po
global _LAST_GATEWAY_RESTART
subcommand = _gateway_subcommand(profile, "restart")
existing = _ACTION_PROCS.get("gateway-restart")
subcommand = _gateway_mod._gateway_subcommand(profile, "restart")
existing = _gateway_mod._ACTION_PROCS.get("gateway-restart")
if existing is not None and existing.poll() is None:
existing_command = _ACTION_COMMANDS.get("gateway-restart")
existing_command = _gateway_mod._ACTION_COMMANDS.get("gateway-restart")
if existing_command is None or existing_command == tuple(subcommand):
return existing, True
raise RuntimeError("gateway restart already in progress for another profile")
@@ -1000,7 +871,7 @@ def _spawn_gateway_restart(profile: Optional[str] = None) -> Tuple[subprocess.Po
)
return recent_proc, True
proc = _spawn_hermes_action(subcommand, "gateway-restart")
proc = _gateway_mod._spawn_hermes_action(subcommand, "gateway-restart")
_LAST_GATEWAY_RESTART = (time.monotonic(), proc, tuple(subcommand))
return proc, False
+5 -11
View File
@@ -1,9 +1,5 @@
"""Chat/terminal WebSocket plumbing: PTY bridge selection and registry, WS
client/origin/auth gates, chat argv resolution, gateway/sidecar URL building.
Split out of ``hermes_cli.web_server``; every externally used name is re-imported
there, so ``web_server.<name>`` keeps resolving (and monkeypatching) as before.
Helpers that tests patch on ``web_server`` are reached lazily through it.
"""
import logging
@@ -184,7 +180,6 @@ def _ws_host_origin_reason(ws: "WebSocket") -> Optional[str]:
def _ws_host_origin_is_allowed(ws: "WebSocket") -> bool:
"""True when the upgrade passes the dashboard Host/Origin guard."""
from hermes_cli.web_server import _ws_host_origin_reason
return _ws_host_origin_reason(ws) is None
@@ -286,7 +281,6 @@ def _ws_auth_reason(ws: "WebSocket") -> tuple[Optional[str], str]:
def _ws_auth_ok(ws: "WebSocket") -> bool:
"""True when the WS-upgrade credential is accepted. See _ws_auth_reason."""
from hermes_cli.web_server import _ws_auth_reason
return _ws_auth_reason(ws)[0] is None
@@ -303,10 +297,10 @@ def _resolve_chat_argv(
``profile`` scopes the ENTIRE chat by pointing ``HERMES_HOME`` at the profile
dir, the same propagation ``hermes -p <name>`` performs.
"""
from hermes_cli.web_server import (
_config_profile_scope, _open_session_db_for_profile, _resolve_profile_dir,
_session_latest_descendant)
from hermes_cli.main import PROJECT_ROOT, _apply_tui_python_env, _make_tui_argv
from hermes_cli.web_server_profiles import _config_profile_scope, _resolve_profile_dir
from hermes_cli.web_server_sessions import _open_session_db_for_profile, _session_latest_descendant
from hermes_cli.main import PROJECT_ROOT
from hermes_cli.main_tui_launch import _apply_tui_python_env, _make_tui_argv
profile_dir: Optional[Path] = None
requested = (profile or "").strip()
@@ -428,7 +422,7 @@ async def _resolve_chat_argv_async(
active_session_file: Optional[str] = None) -> tuple[list[str], Optional[str], Optional[dict]]:
"""Resolve chat argv off the event loop (it may run ``npm run build``); the
async lock keeps one-build-at-a-time without parking worker threads."""
from hermes_cli.web_server import _get_chat_argv_lock, _resolve_chat_argv, app
from hermes_cli.web_server import _get_chat_argv_lock, app
kwargs = {"resume": resume, "sidecar_url": sidecar_url, "profile": profile}
if active_session_file is not None:
kwargs["active_session_file"] = active_session_file
-4
View File
@@ -1,8 +1,4 @@
"""Dashboard config schema and model-assignment logic: CONFIG_SCHEMA construction, dynamic provider options, web<->config normalisation, main/aux model assignment.
Split out of ``hermes_cli.web_server``; every externally used name is re-imported there so
``web_server.<name>`` keeps resolving (and monkeypatching). Helpers that tests patch on
``web_server`` are reached lazily through it.
"""
import logging
+3 -17
View File
@@ -1,9 +1,5 @@
"""Dashboard cron helpers: per-profile scheduler I/O, job validation/normalisation, cron fire and
gateway forwarding.
Split out of ``hermes_cli.web_server``; every externally used name is re-imported there, so
``web_server.<name>`` keeps resolving (and monkeypatching). Helpers that tests patch on
``web_server`` are reached lazily through it.
"""
import contextlib
@@ -72,7 +68,6 @@ def _validate_dashboard_cron_effective_job(job: Dict[str, Any]) -> None:
def _validate_dashboard_cron_context_from(refs: Optional[List[str]], profile_name: str) -> None:
from hermes_cli.web_server import _call_cron_for_profile
for ref in refs or ():
# "self" (the continuity toggle) resolves to the job's own id at run time — it can't be
# validated against the store (create precedes the job's existence).
@@ -87,7 +82,7 @@ def _validate_dashboard_cron_context_from(refs: Optional[List[str]], profile_nam
def _cron_profile_dicts() -> List[Dict[str, Any]]:
"""Minimal profile records (callers only consume ``name``); avoids ``list_profiles()``,
whose config parsing, gateway probes and skill counts are GIL pressure on large pools."""
from hermes_cli.web_server import _fallback_profile_dicts
from hermes_cli.web_server_profiles import _fallback_profile_dicts
from hermes_cli import profiles as profiles_mod
try:
return [
@@ -116,7 +111,6 @@ def _cron_default_profile() -> str:
def _cron_profile_home(profile: Optional[str]) -> Tuple[str, Path]:
"""Resolve a profile query value to (profile_name, HERMES_HOME)."""
from hermes_cli.web_server import _cron_default_profile
from hermes_cli import profiles as profiles_mod
raw = (profile or _cron_default_profile()).strip() or "default"
try:
@@ -157,7 +151,6 @@ def _cron_store_scope(home: Path):
def _call_cron_for_profile(target_profile: Optional[str], func_name: str, *args, **kwargs):
"""Run a cron.jobs helper against the selected profile's cron directory."""
from hermes_cli.web_server import _cron_profile_home
profile_name, home = _cron_profile_home(target_profile)
with _cron_store_scope(home) as cron_jobs:
if func_name == "create_job":
@@ -182,7 +175,6 @@ def _notify_cron_provider_for_profile(target_profile: Optional[str]) -> None:
affected profile re-arms on its next fire/start (idempotent via dedup_key). The built-in
provider re-reads jobs.json each tick and stays a no-op here.
"""
from hermes_cli.web_server import _cron_profile_dicts, _cron_profile_home
try:
_profile_name, home = _cron_profile_home(target_profile)
from cron.scheduler_provider import InProcessCronScheduler, resolve_cron_scheduler
@@ -205,7 +197,6 @@ def _notify_cron_provider_for_profile(target_profile: Optional[str]) -> None:
def _mutate_cron_for_profile(target_profile: Optional[str], func_name: str, *args, **kwargs):
"""Apply a cron store mutation and reconcile its scheduler provider."""
from hermes_cli.web_server import _call_cron_for_profile, _notify_cron_provider_for_profile
result = _call_cron_for_profile(target_profile, func_name, *args, **kwargs)
if result:
_notify_cron_provider_for_profile(target_profile)
@@ -213,7 +204,6 @@ def _mutate_cron_for_profile(target_profile: Optional[str], func_name: str, *arg
def _find_cron_job_profile(job_id: str) -> Optional[str]:
from hermes_cli.web_server import _call_cron_for_profile, _cron_profile_dicts
for profile in _cron_profile_dicts():
name = str(profile.get("name") or "")
if not name:
@@ -226,7 +216,7 @@ def _find_cron_job_profile(job_id: str) -> Optional[str]:
async def _run_cron_dashboard_io(func, *args, **kwargs):
"""Run cron dashboard profile/job I/O outside the FastAPI event loop."""
from hermes_cli.web_server import run_in_threadpool
from starlette.concurrency import run_in_threadpool
if inspect.iscoroutinefunction(func):
raise TypeError("_run_cron_dashboard_io only accepts sync callables")
result = await run_in_threadpool(func, *args, **kwargs)
@@ -244,7 +234,6 @@ def _raise_if_cron_registration_error(e: Exception) -> None:
def _create_cron_job_sync(body: CronJobCreate, profile: Optional[str] = None):
from hermes_cli.web_server import _cron_profile_home
try:
profile_name, profile_home = _cron_profile_home(profile)
script = _normalize_dashboard_cron_script(body.script, profile_home)
@@ -286,7 +275,6 @@ def _fire_cron_job_for_profile(profile: str, job_id: str, *, force: bool = False
cannot serve relay-fronted platforms or E2EE rooms). Retained for the dashboard trigger path
and external callers on the web_deps late-binding seam; do not add new uses.
"""
from hermes_cli.web_server import _cron_profile_home
_profile_name, home = _cron_profile_home(profile)
from cron.scheduler_provider import provider_supports_force_fire, resolve_cron_scheduler
with _cron_store_scope(home):
@@ -332,7 +320,7 @@ def _gateway_fire_endpoint(profile: str, home: Path) -> str:
mirrors under ``/p/<profile>/…``, so a non-default profile's port must be read from the
default home (a secondary's own API_SERVER_PORT is a port nothing listens on).
"""
from hermes_cli.web_server import _cron_default_profile, load_config
from hermes_cli.config import load_config
import os as _os
multiplex = False
try:
@@ -396,7 +384,6 @@ async def _forward_cron_fire_to_gateway(
de-dupes a double fire) — unless :func:`_gateway_intentionally_stopped`, in which case it
drops the fire with 200: retrying into an operator-stopped gateway can never succeed.
"""
from hermes_cli.web_server import _cron_profile_home
_profile_name, home = _cron_profile_home(profile)
url = _gateway_fire_endpoint(_profile_name, home)
import httpx
@@ -425,7 +412,6 @@ def _gateway_intentionally_stopped(profile: Optional[str]) -> bool:
``gateway_state`` runtime field: a legacy/crashed file must stay on the retryable-503 path.
Any resolution or parse failure returns False (fail open toward retry).
"""
from hermes_cli.web_server import _cron_profile_home
import json as _json
try:
data = _json.loads((_cron_profile_home(profile)[1] / "gateway_state.json").read_text(encoding="utf-8"))
+4 -13
View File
@@ -1,8 +1,4 @@
"""Dashboard UI assets: SPA mount, theme normalisation/bootstrap CSS, dashboard-plugin discovery and the plugins-hub merge.
Split out of ``hermes_cli.web_server``; every externally used name is re-imported there so
``web_server.<name>`` keeps resolving (and monkeypatching). Helpers that tests patch on
``web_server`` are reached lazily through it.
"""
import logging
@@ -47,7 +43,7 @@ def _render_active_theme_bootstrap_css() -> str:
references the variables rather than literals so runtime theme switches stay live:
``applyTheme()`` writes inline styles on ``documentElement`` which outrank this block.
"""
from hermes_cli.web_server import load_config
from hermes_cli.config import load_config
try:
active = cfg_get(load_config(), "dashboard", "theme", default="default")
if not active or not isinstance(active, str):
@@ -621,7 +617,6 @@ def _plugin_auth_hint(name: str, provides_tools: list) -> tuple:
A missing cache entry is "unknown": schedule a background probe rather than probing
inline (which would starve the root event loop), so the short hub TTL picks it up.
"""
from hermes_cli.web_server import _schedule_check_fn_probe
try:
from tools.registry import get_cached_check_fn_result, registry
for tname in provides_tools:
@@ -646,13 +641,9 @@ def _merged_plugins_hub(force_refresh: bool = False) -> Dict[str, Any]:
event loop). Only cached availability is consumed and the payload is memoized briefly to
collapse the dashboard's bursty duplicate fetches.
"""
from hermes_cli.web_server import (
_discover_memory_provider_statuses,
_get_dashboard_plugins,
_normalize_memory_provider_name,
get_hermes_home,
load_config,
)
from hermes_cli.web_server_memory import _discover_memory_provider_statuses, _normalize_memory_provider_name
from hermes_cli.web_server import _get_dashboard_plugins
from hermes_cli.config import get_hermes_home, load_config
global _plugins_hub_cache, _plugins_hub_cache_expires_at
now = time.monotonic()
if not force_refresh:
+2 -4
View File
@@ -1,7 +1,4 @@
"""Managed-files policy for the dashboard file browser: root resolution, path containment, entry metadata.
Split out of ``hermes_cli.web_server``, which re-imports every externally used
name so ``web_server.<name>`` keeps resolving (and monkeypatching) as before.
"""
import mimetypes
@@ -99,7 +96,8 @@ def _dashboard_local_update_managed_externally() -> bool:
the update button is the correct path. pip stays blocked in containers: its
apply path mutates the running container filesystem.
"""
from hermes_cli.web_server import PROJECT_ROOT, detect_install_method
from hermes_cli.web_server import PROJECT_ROOT
from hermes_cli.config import detect_install_method
if _default_hermes_root_is_opt_data():
return True
try:
+2 -11
View File
@@ -1,9 +1,5 @@
"""Gateway/process helpers for the dashboard: per-profile gateway topology (+cache), action
subprocess spawning, gateway restart plumbing, system platform display.
Split out of ``hermes_cli.web_server``; every externally used name is re-imported there, so
``web_server.<name>`` keeps resolving (and monkeypatching). Helpers that tests patch on
``web_server`` are reached lazily through it.
"""
import logging
@@ -149,7 +145,6 @@ def _collect_profile_gateway_topology() -> Dict[str, Any]:
(multiplex / single / multiple / none) and ``profile_platforms`` — ownership-filtered runtime
platform maps per live gateway, an internal aggregation input never exposed directly.
"""
from hermes_cli.web_server import _profile_gateway_writer_identity
try:
from hermes_cli.profiles import _check_gateway_running, profiles_to_serve
from gateway.status import read_runtime_status
@@ -213,7 +208,6 @@ def _topology_cache_get(fn: Any) -> Optional[Dict[str, Any]]:
def _collect_profile_gateway_topology_cached() -> Dict[str, Any]:
from hermes_cli.web_server import _collect_profile_gateway_topology
fn = _collect_profile_gateway_topology
cached = _topology_cache_get(fn)
if cached is not None:
@@ -286,7 +280,6 @@ _ACTION_RESULTS: Dict[str, Dict[str, Any]] = {}
def _terminate_desktop_managed_gateway() -> None:
"""Stop a live gateway restart child when its Desktop backend shuts down."""
from hermes_cli.web_server import _ACTION_PROCS
proc = _ACTION_PROCS.get("gateway-restart")
if proc is None:
return
@@ -333,9 +326,7 @@ def _spawn_hermes_action(
subcommand: List[str], name: str, *, env_overrides: Optional[Dict[str, str]] = None
) -> subprocess.Popen:
"""Spawn ``hermes <subcommand>`` detached (via ``hermes_cli.main``) and record the handle."""
from hermes_cli.web_server import (
PROJECT_ROOT, _ACTION_COMMANDS, _ACTION_IDS, _ACTION_LOG_DIR, _ACTION_PROCS, _ACTION_RESULTS,
)
from hermes_cli.web_server import PROJECT_ROOT
_ACTION_LOG_DIR.mkdir(parents=True, exist_ok=True)
log_file = open(_ACTION_LOG_DIR / _ACTION_LOG_FILES[name], "ab", buffering=0)
log_file.write(f"\n=== {name} started {time.strftime('%Y-%m-%d %H:%M:%S')} ===\n".encode())
@@ -365,7 +356,7 @@ def _spawn_hermes_action(
def _gateway_subcommand(profile: Optional[str], verb: str) -> List[str]:
from hermes_cli.web_server import _profile_cli_args
from hermes_cli.web_server_profiles import _profile_cli_args
return _profile_cli_args(profile) + ["gateway", verb]
-4
View File
@@ -1,7 +1,4 @@
"""Serve-process lifecycle: parent death watchdog, port-conflict preflight, READY announcement, browser open, trusted proxies.
Split out of ``hermes_cli.web_server``, which re-imports every externally used
name so ``web_server.<name>`` keeps resolving (and monkeypatching) as before.
"""
import logging
@@ -380,7 +377,6 @@ def _write_machine_sentinel_line(line: str) -> None:
def _report_port_in_use(host: str, port: int) -> None:
"""Print the machine sentinel + a human hint naming likely holders."""
from hermes_cli.web_server import _write_machine_sentinel_line
_write_machine_sentinel_line(_PORT_IN_USE_SENTINEL.format(port=port))
print(
f" Port {port} on {host} is already in use — likely another "
-2
View File
@@ -1,7 +1,5 @@
"""MCP dashboard helpers: create-payload normalisation, env redaction/summary, dashboard-driven MCP OAuth worker.
Split out of ``hermes_cli.web_server``, which re-imports every externally used
name so ``web_server.<name>`` keeps resolving (and monkeypatching) as before.
Wraps the same config layer the CLI uses (hermes_cli.mcp_config); stdio ``env``
secrets are redacted on read.
"""
+3 -9
View File
@@ -1,8 +1,4 @@
"""Memory-provider dashboard helpers: manifest/schema loading, setup-env and dependency probes, configured-status discovery.
Split out of ``hermes_cli.web_server``; every externally used name is re-imported there so
``web_server.<name>`` keeps resolving (and monkeypatching). Helpers that tests patch on
``web_server`` are reached lazily through it.
"""
import logging
@@ -140,7 +136,6 @@ def _run_setup_command(
def _memory_provider_dependencies_installed(setup: Dict[str, Any]) -> bool:
from hermes_cli.web_server import _dependency_importable
pip_ok = all(_dependency_importable(dep) for dep in _string_list(setup.get("pip_dependencies")))
external_ok = True
for dep in setup.get("external_dependencies") or []:
@@ -226,7 +221,7 @@ def _read_json_file(path: Path) -> Dict[str, Any]:
def _read_memory_provider_existing_values(name: str) -> Dict[str, Any]:
"""Best-effort read of existing provider config across legacy/native stores."""
from hermes_cli.web_server import get_hermes_home, load_config
from hermes_cli.config import get_hermes_home, load_config
hermes_home = get_hermes_home()
values: Dict[str, Any] = {}
@@ -259,7 +254,7 @@ def _read_memory_provider_existing_values(name: str) -> Dict[str, Any]:
def _env_lookup(env_key: Optional[str]) -> str:
from hermes_cli.web_server import load_env
from hermes_cli.config import load_env
if not env_key:
return ""
return str(load_env().get(env_key) or os.environ.get(env_key) or "")
@@ -348,7 +343,7 @@ def _memory_provider_status(row: Dict[str, Any], setup: Dict[str, Any], configur
def _discover_memory_provider_statuses() -> List[Dict[str, Any]]:
from hermes_cli.web_server import load_config
from hermes_cli.config import load_config
discovered: Dict[str, Dict[str, Any]] = {}
try:
from plugins.memory import discover_memory_providers
@@ -393,7 +388,6 @@ def _discover_memory_provider_statuses() -> List[Dict[str, Any]]:
def _require_memory_provider_ready(name: str) -> None:
from hermes_cli.web_server import _discover_memory_provider_statuses
if not name:
return
row = next((r for r in _discover_memory_provider_statuses() if r["name"] == name), None)
+1 -5
View File
@@ -1,9 +1,5 @@
"""Messaging-platform catalog and onboarding helpers: platform overrides/env discovery, WhatsApp
and Telegram onboarding state.
Split out of ``hermes_cli.web_server``; every externally used name is re-imported there, so
``web_server.<name>`` keeps resolving (and monkeypatching). Helpers that tests patch on
``web_server`` are reached lazily through it.
"""
import logging
@@ -380,7 +376,7 @@ def _whatsapp_onboarding_payload(pairing_id: str, record: _WhatsAppOnboardingSes
def _restart_gateway_after_whatsapp_onboarding(profile: Optional[str] = None) -> dict[str, Any]:
from hermes_cli.web_server import _restart_gateway_after
from hermes_cli.web_server_gateway import _restart_gateway_after
return _restart_gateway_after(profile, what="WhatsApp onboarding", label="WhatsApp onboarding")
+3 -7
View File
@@ -1,9 +1,5 @@
"""Dashboard OAuth/login-status helpers: provider catalog, per-provider device pollers,
Anthropic/Copilot/Claude-Code status probes.
Split out of ``hermes_cli.web_server``; every externally used name is re-imported there, so
``web_server.<name>`` keeps resolving (and monkeypatching). Helpers that tests patch on
``web_server`` are reached lazily through it.
"""
import logging
@@ -216,7 +212,7 @@ def _oauth_poller(label: str):
@_oauth_poller("nous")
def _nous_poller(session_id: str, sess: Dict[str, Any]) -> None:
"""Background poller that drives a Nous device-code flow to completion."""
from hermes_cli.web_server import _profile_scope
from hermes_cli.web_server_profiles import _profile_scope
from hermes_cli.auth import _poll_for_token, persist_nous_credentials, refresh_nous_oauth_from_state
import httpx
portal_base_url, client_id = sess["portal_base_url"], sess["client_id"]
@@ -255,7 +251,7 @@ def _minimax_poller(session_id: str, sess: Dict[str, Any]) -> None:
``device_code``. Builds the same auth_state as the CLI's ``_minimax_oauth_login`` and persists
via ``_minimax_save_auth_state`` so the system ends up as after ``hermes auth add minimax-oauth``.
Region is fixed to "global" here; cn-region operators use the CLI's ``--region cn``."""
from hermes_cli.web_server import _profile_scope
from hermes_cli.web_server_profiles import _profile_scope
from hermes_cli.auth import (
_minimax_poll_token, _minimax_resolve_token_expiry_unix, _minimax_save_auth_state,
MINIMAX_OAUTH_GLOBAL_INFERENCE, MINIMAX_OAUTH_SCOPE,
@@ -294,7 +290,7 @@ def _minimax_poller(session_id: str, sess: Dict[str, Any]) -> None:
@_oauth_poller("xai")
def _xai_device_poller(session_id: str, sess: Dict[str, Any]) -> None:
"""Background poller for xAI's OAuth device-code flow."""
from hermes_cli.web_server import _profile_scope
from hermes_cli.web_server_profiles import _profile_scope
import httpx
from hermes_cli.auth import (
_save_xai_oauth_tokens, _xai_oauth_discovery, _xai_oauth_poll_device_token,
-4
View File
@@ -1,9 +1,5 @@
"""Profile-scoped helpers: profile discovery fallback, profile dir/MCP-server writes,
the profile/config scope context managers, skills-hub and tools/analytics catalog helpers.
Split out of ``hermes_cli.web_server``; every externally used name is re-imported
there, so ``web_server.<name>`` keeps resolving (and monkeypatching) as before.
Helpers that tests patch on ``web_server`` are reached lazily through it.
"""
import logging
+1 -8
View File
@@ -1,9 +1,5 @@
"""Session-DB access for the dashboard: per-profile SessionDB opening with schema
heal, latest-descendant lookup and the auto-archive ticker.
Split out of ``hermes_cli.web_server``, which re-imports every external name so
``web_server.<name>`` keeps resolving (and monkeypatching); helpers tests patch
there are reached lazily through it.
"""
import logging
@@ -111,8 +107,6 @@ def _open_session_db_at_path(db_path: Path, *, read_only: bool):
healthy read path never takes a write lock. Tables outside SCHEMA_SQL
(telemetry ``tel_*``, FTS shadow tables) are outside both probe and heal.
"""
from hermes_cli.web_server import (
_session_db_heal_exhausted, _session_db_heal_warned, _session_db_read_probe_statements)
import sqlite3
from hermes_state import SessionDB, is_malformed_schema_error
@@ -188,7 +182,7 @@ def _open_session_db_for_profile(profile: Optional[str], *, read_only: bool):
Access-mode semantics: see :func:`_open_session_db_at_path`.
"""
from hermes_cli.web_server import _cron_profile_home
from hermes_cli.web_server_cron import _cron_profile_home
from hermes_state import _default_db_path
if profile:
@@ -210,7 +204,6 @@ def _maybe_auto_archive_for_profile(profile: Optional[str]) -> None:
"""Config-gated stale-session auto-archive for ``profile``; never raises.
``hermes serve`` runs neither CLI nor gateway startup hooks, so this
session-list trigger is what makes ``sessions.auto_archive`` work there."""
from hermes_cli.web_server import _open_session_db_for_profile
try:
key = profile or ""
now = time.monotonic()