Files
hermes-agent/hermes_cli/install_identity.py
Sora-bluesky 9085ef967c fix(profiles): sweep the remaining pre-write mkdirs under the deleted-profile guard
A long-lived serve process keeps a deleted profile as the context home of threads
that outlive the delete. A bare `mkdir(parents=True)` right before an atomic write
brings `profiles/<name>/` back after `hermes profile delete` has written the
tombstone and removed the tree.

The writers in `utils` and the seven callers named in #112592 are guarded by the
preceding commits; this one applies the same `mkdir_under_hermes_home` idiom to the
other pre-write directory creations found by the same mechanical rule (auth,
personality, plugin catalog, skills sync, tool discovery cache, platform adapters,
memory plugins, local runtime supervisor, process identity, breadcrumbs). The two
sites that pass `mode=` keep their mkdir behind `assert_named_profile_home_live`.
The guard is a no-op unless the target has a provable `profiles/<name>` ancestor.

Salvaged from #112596 (30-file sweep) on top of #112594 / #112601; the overlapping
files were resolved to the already-landed versions.
2026-09-16 00:32:15 -07:00

110 lines
3.9 KiB
Python

"""Stable opaque identity shared by every profile in one Hermes install."""
from __future__ import annotations
import contextlib
import os
from pathlib import Path
import re
import threading
from typing import Optional
import uuid
from hermes_constants import get_default_hermes_root
from utils import atomic_write_text
_INSTALL_ID_FILENAME = "install_id"
_INSTALL_ID_RE = re.compile(r"^[0-9a-f]{32}$")
_INSTALL_ID_CACHE: dict[str, Optional[str]] = {"root": None, "value": None}
_INSTALL_ID_LOCK, _INSTALL_ID_PUBLICATION_LOCK = threading.Lock(), threading.Lock()
@contextlib.contextmanager
def _install_id_file_lock(root: Path):
"""Serialize identity publication across processes on POSIX and Windows."""
fd = os.open(root / ".install_id.lock", os.O_RDWR | os.O_CREAT, 0o600)
windows = os.name == "nt"
try:
if windows:
import msvcrt
if os.fstat(fd).st_size == 0:
os.write(fd, b"\0")
os.fsync(fd)
os.lseek(fd, 0, os.SEEK_SET)
msvcrt.locking(fd, msvcrt.LK_LOCK, 1)
else:
import fcntl
fcntl.flock(fd, fcntl.LOCK_EX)
yield
finally:
try:
if windows:
os.lseek(fd, 0, os.SEEK_SET)
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
else:
fcntl.flock(fd, fcntl.LOCK_UN)
finally:
os.close(fd)
def _read_existing(path: Path) -> tuple[Optional[str], bool]:
"""``(valid id or None, mint?)`` — mint on a missing or malformed file, never on a read failure."""
try:
existing = path.read_text(encoding="utf-8").strip().lower()
except FileNotFoundError:
return None, True
except (OSError, UnicodeDecodeError):
return None, False
return (existing, False) if _INSTALL_ID_RE.fullmatch(existing) else (None, True)
def read_or_create_install_id(root: Path | None = None) -> Optional[str]:
"""Read or atomically mint the opaque id for the physical install.
``None`` = neither readable nor persistable; an ephemeral id would violate the authority/registry contract.
"""
root = get_default_hermes_root() if root is None else root
path = root / _INSTALL_ID_FILENAME
existing, mint = _read_existing(path)
if not mint:
return existing
try:
from hermes_constants import mkdir_under_hermes_home
mkdir_under_hermes_home(root)
# Windows byte-range locks can report a same-process conflict instead of waiting for another
# thread: serialize threads here, then keep the file lock as the cross-process publication fence.
with _INSTALL_ID_PUBLICATION_LOCK, _install_id_file_lock(root):
existing, mint = _read_existing(path)
if not mint:
return existing
atomic_write_text(path, uuid.uuid4().hex + "\n", tmp_prefix=".install_id-", fsync_dir=True, mode=0o600)
committed = path.read_text(encoding="utf-8").strip().lower()
return committed if _INSTALL_ID_RE.fullmatch(committed) else None
except OSError:
return None
def get_install_id(*, cache: dict[str, Optional[str]] | None = None) -> Optional[str]:
"""Return the process-cached stable id for the active Hermes root."""
root = get_default_hermes_root()
root_key = str(root)
target_cache = _INSTALL_ID_CACHE if cache is None else cache
def _cached() -> Optional[str]:
cached = target_cache.get("value")
return cached if cached and target_cache.get("root") in (None, root_key) else None
if value := _cached():
return value
with _INSTALL_ID_LOCK:
if value := _cached():
return value
value = read_or_create_install_id(root)
if value:
target_cache["root"] = root_key
target_cache["value"] = value
return value
__all__ = ["get_install_id", "read_or_create_install_id"]