Files
hermes-agent/hermes_cli/_startup_fast.py
ethernet 7d2b3b767d merge: integrate upstream/main into ethie/pm-clean
Merge upstream b1f003e186 while preserving PM runtime ownership and
Python 3.14 worker startup, Windows signing, and macOS wait recovery.

Keep retired runtime modules deleted. Port upstream updater preflight
checks into the checkout strategy and preserve live build logging.
Carry checkpoint filename handling and process recovery into the current
module layout. Regenerate locks and adapt incoming platform test markers.

Focused Python and JavaScript tests, desktop and root-test typechecks,
conflict-path lint checks, lock validation, and retired-import checks pass.
The full test suite and packaged release builds were not run.
2026-09-08 19:17:39 -04:00

210 lines
8.4 KiB
Python

"""Pre-import startup fast paths — THE canonical lightweight helpers.
This module is imported by ``hermes_cli/main.py`` BEFORE its heavy import
wall (config, argparse tree, logging, providers). Everything here must stay
**stdlib-only and cheap** (os/sys file probes; no yaml, no hermes_cli.config,
no argparse). A guard test (``test_startup_fast_import_weight``) subprocess-
imports this module and fails if any heavy module sneaks into sys.modules.
Why this module exists (the bug class it kills): version-printing kept being
reimplemented as ``*_fast()`` copies at the top of main.py, each duplicating
canonical logic — project-root resolution, container detection, profile
detection. The copies drifted: eb4040242 changed the canonical output and
referenced ``PROJECT_ROOT`` inside the fast function, which doesn't exist
yet on the fast path → the fast path NameError'd on --version and nobody
noticed. One implementation, imported by both the fast path and the module
constants, makes that drift structurally impossible; the parity guard test
would have caught eb4040242 the day it landed.
``hermes_cli/config.py``'s ``get_container_exec_info()`` reads the same
``.container-mode`` file; keep the file-format assumptions here and there in
sync (this module deliberately only PROBES existence/typos cheaply and errs
toward the slow path, which then does the authoritative parse).
"""
from __future__ import annotations
import os
import sys
__all__ = [
"project_root_str",
"ensure_project_root_on_path",
"is_global_fast_version_argv",
"is_container_startup_environment",
"active_profile_may_override_home",
"container_mode_may_be_active",
"read_openai_version",
"read_install_method",
"print_fast_version_info",
"try_fast_version",
]
def _read_text(path: str) -> str | None:
"""Read a small text file, or None when it is missing/unreadable."""
try:
with open(path, encoding="utf-8-sig") as handle:
return handle.read()
except (OSError, UnicodeDecodeError):
return None
def project_root_str() -> str:
"""Repo root as a str — the single source for main.py's PROJECT_ROOT."""
return os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir))
def ensure_project_root_on_path() -> None:
"""Put the project root at sys.path[0], deduping realpath-equivalents."""
project_root = project_root_str()
normalized_root = os.path.normcase(os.path.realpath(project_root))
sys.path[:] = [entry for entry in sys.path
if not entry or os.path.normcase(os.path.realpath(entry)) != normalized_root]
sys.path.insert(0, project_root)
def is_global_fast_version_argv(argv: list[str]) -> bool:
return argv in (["--version"], ["-V"])
def is_container_startup_environment() -> bool:
"""True when we're already INSIDE a container (fast path is then safe)."""
if os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv"):
return True
cgroup = _read_text("/proc/1/cgroup") or ""
return "docker" in cgroup or "podman" in cgroup or "/lxc/" in cgroup
def active_profile_may_override_home(hermes_root: str) -> bool:
"""Cheap probe: does an active non-default profile redirect HERMES_HOME?"""
active = (_read_text(os.path.join(hermes_root, "active_profile")) or "").strip()
return bool(active and active != "default")
def _default_home() -> str:
return os.path.join(os.path.expanduser("~"), ".hermes")
def _resolved_home() -> str:
return os.environ.get("HERMES_HOME", "").strip() or _default_home()
def container_mode_may_be_active() -> bool:
"""Conservative probe for NixOS container-mode routing.
False positives are fine (the slow path does the authoritative check). False negatives are NOT —
they'd print the host's version instead of the container's — so any profile ambiguity means "may
be active".
"""
if os.environ.get("HERMES_DEV") == "1" or is_container_startup_environment():
return False
hermes_home = os.environ.get("HERMES_HOME", "").strip()
if hermes_home:
if os.path.exists(os.path.join(hermes_home, ".container-mode")):
return True
parent_name = os.path.basename(os.path.dirname(os.path.normpath(hermes_home)))
return parent_name != "profiles" and active_profile_may_override_home(hermes_home)
default_home = _default_home()
return active_profile_may_override_home(default_home) or os.path.exists(
os.path.join(default_home, ".container-mode"))
def read_openai_version() -> str | None:
"""Read OpenAI SDK version without importing ``importlib.metadata``."""
for base in sys.path:
version_file = os.path.join(base or os.getcwd(), "openai", "_version.py")
try:
with open(version_file, encoding="utf-8-sig") as handle:
for line in handle:
stripped = line.strip()
if not stripped.startswith("__version__"):
continue
_key, _sep, value = stripped.partition("=")
value = value.split("#", 1)[0].strip().strip("\"'")
return value or None
except OSError:
continue
return None
def read_install_method() -> str | None:
"""The installer's ``.install_method`` stamp, if present.
Only the stamp (step 1 of ``config.detect_install_method``'s resolution order) — the
managed/git/pip fallbacks need heavier imports and stay on the slow path.
"""
method = _read_text(os.path.join(_resolved_home(), ".install_method"))
return (method or "").strip().lower() or None
def print_fast_version_info(*, check_updates: bool = True) -> None:
"""THE canonical ``hermes --version`` output (also used by /version).
Every lazy block degrades gracefully — a broken/heavy import can never take the basic version
output down.
"""
# Registry-owned banner label (includes "· upstream <sha>" for git installs); banner.py keeps
# rich/prompt_toolkit lazy, so this import is light.
try:
from hermes_cli.banner import format_banner_version_label
print(format_banner_version_label())
except Exception:
from hermes_cli import __release_date__, __version__
print(f"Hermes Agent v{__version__} ({__release_date__})")
print(f"Install directory: {project_root_str()}")
# Authoritative resolver first (code-scoped stamp → managed → nix → git → pip; also self-heals
# poisoned shared-home 'docker' stamps); cheap stdlib stamp probe only if it fails.
try:
from pathlib import Path
from hermes_cli.config import detect_install_method
install_method = detect_install_method(Path(project_root_str()))
except Exception:
install_method = read_install_method()
if install_method:
print(f"Install method: {install_method}")
print(f"Python: {sys.version.split()[0]}")
openai_version = read_openai_version()
print(f"OpenAI SDK: {openai_version}" if openai_version else "OpenAI SDK: Not installed")
if not check_updates:
return
# Synchronous update status — bounded by check_for_updates' own subprocess/network timeouts
# and its 6-hour cache; any failure prints nothing.
try:
from hermes_cli.banner import UPDATE_AVAILABLE_NO_COUNT, check_for_updates
from hermes_cli.config import recommended_update_command
behind = check_for_updates(passive=True)
if behind == UPDATE_AVAILABLE_NO_COUNT:
print(f"Update available — run '{recommended_update_command()}'")
elif behind and behind > 0:
commits_word = "commit" if behind == 1 else "commits"
print(f"Update available: {behind} {commits_word} behind — run '{recommended_update_command()}'")
elif behind == 0:
print("Up to date")
except Exception:
pass
def try_fast_version(argv: list[str] | None = None) -> bool:
"""Handle ``hermes --version`` before the heavy import wall.
Only ``--version``/``-V`` (the ``version`` subcommand was removed —
``--version`` now carries the full output incl. update status), and
never when container mode may need to route the command into the
container.
"""
if argv is None:
argv = sys.argv[1:]
if not is_global_fast_version_argv(argv):
return False
if container_mode_may_be_active():
return False
print_fast_version_info()
return True