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:
+21
-14
@@ -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,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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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,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")
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
Reference in New Issue
Block a user