diff --git a/cli.py b/cli.py index 9a9b6ce0bb..aa037e649a 100644 --- a/cli.py +++ b/cli.py @@ -2399,7 +2399,8 @@ def save_config_value(key_path: str, value: any) -> bool: config_path = get_hermes_home() / 'config.yaml' try: - config_path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(config_path.parent) from utils import atomic_roundtrip_yaml_update atomic_roundtrip_yaml_update(config_path, key_path, value) try: # owner-only: config files contain API keys diff --git a/gateway/shutdown_flush.py b/gateway/shutdown_flush.py index 7c9d141a2a..b0af857d89 100644 --- a/gateway/shutdown_flush.py +++ b/gateway/shutdown_flush.py @@ -35,6 +35,8 @@ def _get_flush_dir(): """Return the pending-messages flush directory under the active HERMES_HOME.""" from hermes_constants import get_hermes_home flush_dir = get_hermes_home() / "pending_messages" + from hermes_constants import assert_named_profile_home_live + assert_named_profile_home_live(flush_dir) flush_dir.mkdir(parents=True, exist_ok=True, mode=0o700) if os.name == "posix": os.chmod(flush_dir, 0o700) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 79522c3c3d..3931be3395 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -698,7 +698,8 @@ def _load_auth_store(auth_file: Optional[Path] = None) -> Dict[str, Any]: def _save_private_json(target: Path, data: Any, *, fsync_dir: bool = False, **dump_kwargs: Any) -> None: """0600 credential JSON under a 0700 parent (``secure_parent_dir`` refuses ``/``, top-level dirs and the install tree). ``atomic_json_write`` creates the temp file 0600 before any byte lands.""" - target.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(target.parent) secure_parent_dir(target) atomic_json_write(target, data, mode=0o600, fsync_dir=fsync_dir, **dump_kwargs) diff --git a/hermes_cli/auth_oauth_grants.py b/hermes_cli/auth_oauth_grants.py index 87a60e1fd3..11913ef471 100644 --- a/hermes_cli/auth_oauth_grants.py +++ b/hermes_cli/auth_oauth_grants.py @@ -207,7 +207,8 @@ def _persist_oauth_heal_clean_mark(provider_id: str, fingerprint: tuple) -> None if marks.get(provider_id) == new_mark: return # already recorded; skip the rewrite marks[provider_id] = new_mark - path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(path.parent) # 0o600 like the MCP schema cache: this names credential-store paths. atomic_json_write(path, marks, mode=0o600) except Exception: diff --git a/hermes_cli/install_identity.py b/hermes_cli/install_identity.py index 2f25436359..46e9c8b191 100644 --- a/hermes_cli/install_identity.py +++ b/hermes_cli/install_identity.py @@ -69,7 +69,8 @@ def read_or_create_install_id(root: Path | None = None) -> Optional[str]: if not mint: return existing try: - root.mkdir(parents=True, exist_ok=True) + 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): diff --git a/hermes_cli/local_runtime/supervisor.py b/hermes_cli/local_runtime/supervisor.py index 0688d3debc..6870efaca9 100644 --- a/hermes_cli/local_runtime/supervisor.py +++ b/hermes_cli/local_runtime/supervisor.py @@ -224,7 +224,8 @@ class LlamaServerSupervisor: "executable": proc.exe(), "owner_pid": os.getpid(), "owner_create_time": psutil.Process().create_time()} path = state_path() - path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(path.parent) atomic_json_write(path, self._state, mode=0o600) def _wait_health(self, timeout_s: int) -> None: diff --git a/hermes_cli/personality.py b/hermes_cli/personality.py index e2dc163125..22cf01f39b 100644 --- a/hermes_cli/personality.py +++ b/hermes_cli/personality.py @@ -134,7 +134,8 @@ def persist_personality(value: Any) -> bool: from utils import atomic_roundtrip_yaml_update config_path = get_hermes_home() / "config.yaml" - config_path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(config_path.parent) atomic_roundtrip_yaml_update(config_path, "display.personality", name) try: os.chmod(config_path, 0o600) diff --git a/hermes_cli/plugin_catalog.py b/hermes_cli/plugin_catalog.py index dad4d8ac91..abc8a5ab52 100644 --- a/hermes_cli/plugin_catalog.py +++ b/hermes_cli/plugin_catalog.py @@ -238,6 +238,8 @@ def fetch_live_catalog(*, force: bool = False) -> Optional[Dict[str, Any]]: logger.debug("Plugin catalog: unreadable live cache %s: %s", cache, exc) try: import httpx + from hermes_constants import mkdir_under_hermes_home + resp = httpx.get(LIVE_CATALOG_URL, timeout=_REQUEST_TIMEOUT, follow_redirects=True) resp.raise_for_status() if len(resp.content) > _MAX_LIVE_BYTES: @@ -245,7 +247,7 @@ def fetch_live_catalog(*, force: bool = False) -> Optional[Dict[str, Any]]: data = resp.json() if not isinstance(data, dict) or not isinstance(data.get("entries"), list): raise ValueError("unexpected live catalog payload") - cache.parent.mkdir(parents=True, exist_ok=True) + mkdir_under_hermes_home(cache.parent) cache.write_text(json.dumps(data), encoding="utf-8") return data except Exception as exc: diff --git a/hermes_cli/process_identity.py b/hermes_cli/process_identity.py index d2ad83aad6..b71a066215 100644 --- a/hermes_cli/process_identity.py +++ b/hermes_cli/process_identity.py @@ -266,7 +266,8 @@ def _append_entry(entry: LedgerEntry) -> bool: ] pruned.append(asdict(entry)) try: - path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(path.parent) # argv may carry surrogate-escaped bytes (non-UTF-8 paths); ensure_ascii keeps the # utf-8 text handle from raising UnicodeEncodeError (a ValueError, not an OSError). atomic_json_write(path, pruned, mode=0o600, ensure_ascii=True) diff --git a/hermes_cli/terminal_breadcrumbs.py b/hermes_cli/terminal_breadcrumbs.py index 786f5949f3..9dc8ca4ce5 100644 --- a/hermes_cli/terminal_breadcrumbs.py +++ b/hermes_cli/terminal_breadcrumbs.py @@ -84,7 +84,8 @@ def write_breadcrumb(session_id: str, cwd: Optional[str] = None) -> None: if not terminal_id: return directory = _breadcrumbs_dir() - directory.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(directory) now = time.time() payload = {"session_id": session_id, "cwd": cwd or os.getcwd(), "ts": now} atomic_json_write(directory / terminal_id, payload, indent=None) diff --git a/hermes_cli/web_routers/memory_providers.py b/hermes_cli/web_routers/memory_providers.py index 777326cae8..1364d31a58 100644 --- a/hermes_cli/web_routers/memory_providers.py +++ b/hermes_cli/web_routers/memory_providers.py @@ -164,7 +164,8 @@ def _apply_field_values(provider: ProviderConfigSchema, values: Dict[str, str], def _write_json_0600(path: Path, data: Dict[str, Any]) -> None: from utils import atomic_json_write - path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(path.parent) atomic_json_write(path, data, mode=0o600) diff --git a/plugins/memory/honcho/cli.py b/plugins/memory/honcho/cli.py index 537b60e15d..8efacbf50c 100644 --- a/plugins/memory/honcho/cli.py +++ b/plugins/memory/honcho/cli.py @@ -149,7 +149,8 @@ def _write_config(cfg: dict, path: Path | None = None) -> None: out = _apply_edits(cfg.snapshot, cfg, disk) elif path.exists(): out = _apply_edits(cfg.snapshot, cfg, _overlay_local(cfg.snapshot, disk)) - path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(path.parent) atomic_json_write(path, out, mode=0o600) if isinstance(cfg, _ReadConfig): # a later write on the same object applies only edits made after this one cfg.snapshot, cfg.path = copy.deepcopy(dict(cfg)), path diff --git a/plugins/memory/openviking/__init__.py b/plugins/memory/openviking/__init__.py index 25353cd2af..ad8a962561 100644 --- a/plugins/memory/openviking/__init__.py +++ b/plugins/memory/openviking/__init__.py @@ -2216,7 +2216,8 @@ class OpenVikingMemoryProvider(MemoryProvider): logger.debug("Could not safely mark OpenViking session %s pending without a run lock", sid) return try: - path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(path.parent) atomic_json_write(path, {"session_id": sid, "owner_run_id": self._run_id}, mode=0o600) self._pending_marked_sids.add(sid) except Exception as e: diff --git a/plugins/memory/openviking/_setup.py b/plugins/memory/openviking/_setup.py index 488889337a..130220f516 100644 --- a/plugins/memory/openviking/_setup.py +++ b/plugins/memory/openviking/_setup.py @@ -321,7 +321,8 @@ def _mirror_manual_config_to_openviking_store(*, prompt, select, cancelled, valu return _SETUP_CANCELLED if replace is False: continue - path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(path.parent) # atomic_json_write creates the temp file 0600 and os.replace()s it: no # half-written config on crash, no chmod-after-write window for the keys. ov.atomic_json_write(path, ov._ovcli_data_from_connection_values(values), mode=0o600) diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index e1106b2cbc..3d14cd64a8 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -3449,7 +3449,8 @@ class FeishuAdapter(BasePlatformAdapter): def _persist_seen_message_ids(self) -> None: try: - self._dedup_state_path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(self._dedup_state_path.parent) with self._dedup_lock: recent = self._seen_message_order[-self._dedup_cache_size:] # Save as {msg_id: timestamp} so TTL filtering works across restarts. diff --git a/plugins/platforms/google_chat/oauth.py b/plugins/platforms/google_chat/oauth.py index 623f3e7839..b0dfe0fa35 100644 --- a/plugins/platforms/google_chat/oauth.py +++ b/plugins/platforms/google_chat/oauth.py @@ -195,7 +195,8 @@ def _chmod_quiet(path: Path, mode: int) -> None: def _write_private_json(path: Path, data: Any) -> None: """Atomically write JSON with 0o600 permissions (0o700 parent) where supported.""" - path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(path.parent) _chmod_quiet(path.parent, 0o700) # mkstemp's 0o600 temp + atomic rename never exposes the token at process umask. atomic_write_text(path, json.dumps(data, indent=2, ensure_ascii=False), create_mode=0o600) diff --git a/tools/bot_relay.py b/tools/bot_relay.py index 8544e06400..e65cdcdb5b 100644 --- a/tools/bot_relay.py +++ b/tools/bot_relay.py @@ -86,7 +86,8 @@ def relay_root(root: Path | str) -> Path: def _ensure_dirs(root: Path | str) -> Path: base = relay_root(root) for sub in (OUTBOX_DIR, CLAIMED_DIR, REPLIES_DIR): - (base / sub).mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(base / sub) return base diff --git a/tools/process_registry_results.py b/tools/process_registry_results.py index b2cd175978..eddc9eba28 100644 --- a/tools/process_registry_results.py +++ b/tools/process_registry_results.py @@ -57,6 +57,8 @@ def save_completed_result(session) -> None: record["command"] = redact_sensitive_text(record["command"], code_file=True, force=True) directory = get_hermes_home() / "logs" / "process-results" try: + from hermes_constants import assert_named_profile_home_live + assert_named_profile_home_live(directory) directory.mkdir(mode=0o700, parents=True, exist_ok=True) atomic_json_write(directory / f"{session.id}.json", record, mode=0o600) _result_paths() diff --git a/tools/registry.py b/tools/registry.py index 28564b024e..aca8baa4c4 100644 --- a/tools/registry.py +++ b/tools/registry.py @@ -170,7 +170,8 @@ def _save_discovery_cache(cache: Dict[str, list]) -> None: return try: from utils import atomic_json_write # stdlib+yaml only; no cycle - path.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(path.parent) atomic_json_write(path, cache, indent=0) except Exception as e: logger.debug("Could not write tool discovery cache %s: %s", path, e) diff --git a/tools/skill_manager_tool.py b/tools/skill_manager_tool.py index db87bee81a..8595840e09 100644 --- a/tools/skill_manager_tool.py +++ b/tools/skill_manager_tool.py @@ -364,7 +364,8 @@ def _guarded_write(name: str, skill_dir: Path, target: Path, action: str, label: if read_guard := _background_review_read_before_write_guard(name, target, action, label): return read_guard original = target.read_text(encoding="utf-8") - target.parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(target.parent) atomic_write_text(target, content, preserve_mode=True, create_mode=0o644) scan_error = _security_scan_skill(skill_dir) if not scan_error: @@ -421,7 +422,8 @@ def _create_skill(name: str, content: str, category: str = None) -> Dict[str, An if existing := _find_skill(name): return _err(f"A skill named '{name}' already exists at {existing['path']}.") skill_dir = _resolve_skill_dir(name, category) - skill_dir.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(skill_dir) skill_md = skill_dir / "SKILL.md" atomic_write_text(skill_md, content, preserve_mode=True, create_mode=0o644) if scan_error := _security_scan_skill(skill_dir): diff --git a/tools/skills_sync.py b/tools/skills_sync.py index ca774820be..812200e255 100644 --- a/tools/skills_sync.py +++ b/tools/skills_sync.py @@ -126,7 +126,8 @@ def _read_suppressed_names() -> set: def _write_manifest(entries: Dict[str, str]): """Atomic v2 write, preserving an existing file's mode/owner (not mkstemp's 0600).""" - _manifest_file().parent.mkdir(parents=True, exist_ok=True) + from hermes_constants import mkdir_under_hermes_home + mkdir_under_hermes_home(_manifest_file().parent) try: data = "".join(f"{n}:{h}\n" for n, h in sorted(entries.items())) atomic_write_text(_manifest_file(), data, tmp_prefix=".bundled_manifest_", preserve_mode=True)