Keep externally managed directory links and permissions intact during home initialization. Refuse missing targets rather than creating directories on an unmounted volume's underlying filesystem. Report link, target, mount and access guidance through doctor while preserving config.yaml. Extract the home initialization phase into config_home, and memoize successful resolved aliases so plugin discovery cannot repeat chmod after losing the symlink spelling. Live Linux doctor PTY A/B verified directory and root links, plain paths, missing targets, mount-style missing paths and file conflicts. Targeted invariant tests are queued under the campaign's shared serial lock; this checkpoint is not a unit-suite or merge-readiness claim. Inspired by #104774 and #103735; deliberately does not auto-create external targets or silently ignore an unavailable sessions directory. Co-authored-by: ca-shrimp <320556551+ca-shrimp@users.noreply.github.com> Co-authored-by: Craig Richardson <craigrichardson@Craigs-Mac-mini.local>
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
"""Directory initialization and storage diagnostics for the active Hermes home."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
class HomeInitializationError(RuntimeError):
|
|
"""The home skeleton is unavailable, not an invalid YAML document."""
|
|
|
|
|
|
def _directory_links(path: Path) -> list[Path]:
|
|
return [part for part in (*reversed(path.parents), path) if part.is_symlink()]
|
|
|
|
|
|
def _ensure_directory(path: Path, *, create: bool, secure: bool) -> None:
|
|
from hermes_cli.config import _secure_dir
|
|
|
|
detail = ""
|
|
try:
|
|
links = _directory_links(path)
|
|
detail = "; ".join(f"{link} -> {link.readlink()}" for link in links)
|
|
# Never materialize a missing mount's target on the underlying disk.
|
|
for link in links:
|
|
if not link.is_dir():
|
|
raise FileNotFoundError(f"Directory link is unavailable: {link}")
|
|
if create:
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
elif not path.is_dir():
|
|
raise FileNotFoundError(f"Required directory does not exist: {path}")
|
|
# The operator owns permissions beyond a link, including logs/curator.
|
|
if secure and not links:
|
|
_secure_dir(path)
|
|
except OSError as exc:
|
|
raise HomeInitializationError(
|
|
f"Cannot initialize Hermes directory {path}: {exc}. "
|
|
+ (f"Directory links: {detail}. " if detail else "")
|
|
+ "Check the directory/link target, mount availability and access permissions; "
|
|
"restore the mount or repair the link before retrying. "
|
|
"Hermes has not replaced the link or created its missing target."
|
|
) from exc
|
|
|
|
|
|
def initialize_home(home: Path, subdirs: tuple[str, ...], ensured: set[str]) -> None:
|
|
from hermes_cli.config import _ensure_default_soul_md, is_managed
|
|
|
|
managed = is_managed()
|
|
old_umask = os.umask(0o007) if managed else None
|
|
try:
|
|
_ensure_directory(home, create=not managed, secure=not managed)
|
|
required = ("cron", "sessions", "logs", "memories") if managed else subdirs
|
|
for subdir in required:
|
|
_ensure_directory(home / subdir, create=not managed, secure=not managed)
|
|
if managed:
|
|
_ensure_directory(home / "logs" / "curator", create=True, secure=False)
|
|
try:
|
|
_ensure_default_soul_md(home)
|
|
except OSError as exc:
|
|
raise HomeInitializationError(
|
|
f"Cannot initialize Hermes home {home}: {exc}. "
|
|
"Check storage availability and access permissions."
|
|
) from exc
|
|
finally:
|
|
if old_umask is not None:
|
|
os.umask(old_umask)
|
|
# Plugin discovery rebinds the resolved home. Do not repeat chmod through
|
|
# that spelling after the operator-owned symlink boundary has been lost.
|
|
ensured.update((str(home), str(home.resolve())))
|
|
|
|
|
|
def config_load_issue(exc: Exception):
|
|
from hermes_cli.config import ConfigIssue
|
|
|
|
if isinstance(exc, (HomeInitializationError, OSError)):
|
|
return ConfigIssue(
|
|
"error", f"Hermes storage is unavailable: {exc}",
|
|
"Check the reported path, link target, mount and permissions; keep config.yaml unchanged.",
|
|
)
|
|
return ConfigIssue("error", "Could not load config.yaml", "Run 'hermes setup' to create a valid config")
|