hermes-hub/src/antigravity_provider/paths.py
Hermes Team 6cb0d7c636 fix(profiles): пустые слоты появлялись сами, потому что запрос пути создавал каталог
Владелец спросил, откуда 25 каталогов профилей, когда заводил единицы.
Проверено исполнением: обращение к get_profile_dir создавало каталог. Спросили
четыре пути — появились четыре каталога.

В коде зашит список «стандартных» слотов — ag-orch-primary, ag-orch-fallback,
ag-1..ag-20, ag-w1..ag-w10 — в двух местах: agy_subprocess и
model_discovery_service. Любой обход этого списка материализовал их все. Отсюда
ag-w1..ag-w4, ag-cold-*, ag-spare-* — владелец их не создавал.

Спросить, где профиль жил бы, и завести его — разные действия. Создание теперь
запрашивается явно, create=True, и это делают только те, кто действительно
заводит профиль.

Тесты и сценарий проверки, опиравшиеся на побочное создание, приведены к новому
поведению: они проверяют изоляцию пути, а не то, что каталог возник сам.

669 passed, 2 skipped; ruff чисто; релизный гейт пройден.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 15:27:33 +07:00

176 lines
6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Single Source of Truth for Hermes Hub Filesystem Paths.
Ensures zero hardcoded absolute developer directories or usernames.
Fully respects HERMES_HOME for complete hermetic test isolation.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import Optional
def get_hermes_home() -> Path:
"""Return base hermes home directory, respecting HERMES_HOME environment variable."""
env_home = os.environ.get("HERMES_HOME")
if env_home:
p = Path(env_home).resolve()
p.mkdir(parents=True, exist_ok=True)
return p
local_app = os.environ.get("LOCALAPPDATA", "")
if local_app:
p = Path(local_app) / "hermes"
else:
p = Path.home() / ".hermes"
p.mkdir(parents=True, exist_ok=True)
return p
def get_repo_root() -> Path:
"""Find repository / installation root containing assets and launcher."""
cur = Path(__file__).resolve()
for parent in [cur.parents[3], cur.parents[2], cur.parents[1]]:
if (parent / "assets").exists() or (parent / "pyproject.toml").exists():
return parent
# Fallback to plugin directory in hermes home
plugin_dir = get_hermes_home() / "plugins" / "antigravity-provider"
if plugin_dir.exists():
return plugin_dir
return cur.parents[2]
def get_assets_dir() -> Path:
return get_repo_root() / "assets"
def get_branding_dir() -> Path:
return get_assets_dir() / "branding"
def get_providers_assets_dir() -> Path:
return get_assets_dir() / "providers"
def get_logs_dir() -> Path:
d = get_hermes_home() / "logs"
d.mkdir(parents=True, exist_ok=True)
return d
def get_log_file() -> Path:
return get_logs_dir() / "hermes-hub.log"
def get_startup_log_file() -> Path:
return get_logs_dir() / "startup.log"
def get_config_dir() -> Path:
d = get_hermes_home()
d.mkdir(parents=True, exist_ok=True)
return d
def get_router_profiles_path() -> Path:
return get_config_dir() / "router_profiles.yaml"
def get_router_state_path() -> Path:
return get_config_dir() / "router_state.json"
def get_router_active_profile_path() -> Path:
return get_config_dir() / "router_active_profile.json"
def get_workflow_state_path() -> Path:
"""Return the persisted agent/workflow state sidecar.
Logical roles and their execution routes remain canonical in
``router_profiles.yaml``. This file stores only the extra agent metadata,
graph layout and execution checkpoints which do not belong to routing.
"""
return get_config_dir() / "workflow_state.json"
def get_workflow_run_state_path() -> Path:
"""Return the persisted active workflow run state file."""
return get_config_dir() / "workflow_run_state.json"
def get_agent_files_dir() -> Path:
"""Return the user-editable directory containing real Agent Files."""
directory = get_hermes_home() / "agents"
directory.mkdir(parents=True, exist_ok=True)
return directory
def get_compatibility_path() -> Path:
return get_config_dir() / "compatibility.json"
def get_profile_dir(
profile_id: str, provider: Optional[str] = None, create: bool = False
) -> Path:
"""Вернуть каталог профиля. По умолчанию НИЧЕГО не создаёт.
Раньше эта функция создавала каталог при каждом обращении. В коде зашит
список «стандартных» слотов — ag-orch-primary, ag-orch-fallback, ag-1..ag-20,
ag-w1..ag-w10, — и любой обход этого списка материализовал их на диске. У
владельца накопилось 25 каталогов, из которых он заводил единицы: остальные
появились сами, просто потому что кто-то спросил путь.
Спрашивать, где профиль жил бы, и создавать его — разные действия. Создание
теперь запрашивается явно через create=True: так делают те, кто
действительно заводит профиль.
Accepts both (profile_id) and (provider, profile_id) or (profile_id, provider) gracefully.
"""
# If first argument looks like a provider or swapped, resolve cleanly
p_id = profile_id
prov = provider
if prov is None:
p_lower = p_id.lower()
if p_lower.startswith("ag-") or "antigravity" in p_lower:
folder_prefix = "agy_profiles"
elif p_lower.startswith("codex-") or "codex" in p_lower:
folder_prefix = "codex_profiles"
elif p_lower.startswith("opengo-") or "opencode" in p_lower:
folder_prefix = "opengo_profiles"
elif p_lower.startswith("claude-") or "claude" in p_lower or "anthropic" in p_lower:
folder_prefix = "claude_profiles"
elif p_lower.startswith("grok-") or "grok" in p_lower or "xai" in p_lower:
folder_prefix = "grok_profiles"
else:
folder_prefix = f"{p_lower}_profiles"
else:
# Provider explicitly passed
prov_lower = prov.lower()
if "antigravity" in prov_lower or "agy" in prov_lower:
folder_prefix = "agy_profiles"
elif "codex" in prov_lower or "openai" in prov_lower:
folder_prefix = "codex_profiles"
elif "opencode" in prov_lower or "opengo" in prov_lower:
folder_prefix = "opengo_profiles"
elif "claude" in prov_lower or "anthropic" in prov_lower:
folder_prefix = "claude_profiles"
elif "grok" in prov_lower or "xai" in prov_lower:
folder_prefix = "grok_profiles"
else:
folder_prefix = f"{prov_lower}_profiles"
d = get_hermes_home() / folder_prefix / p_id
if create:
d.mkdir(parents=True, exist_ok=True)
return d
def get_hermes_agent_dir() -> Path:
return get_hermes_home() / "hermes-agent"
def get_hermes_agent_venv() -> Path:
return get_hermes_agent_dir() / "venv"