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.
This commit is contained in:
@@ -2399,7 +2399,8 @@ def save_config_value(key_path: str, value: any) -> bool:
|
|||||||
config_path = get_hermes_home() / 'config.yaml'
|
config_path = get_hermes_home() / 'config.yaml'
|
||||||
|
|
||||||
try:
|
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
|
from utils import atomic_roundtrip_yaml_update
|
||||||
atomic_roundtrip_yaml_update(config_path, key_path, value)
|
atomic_roundtrip_yaml_update(config_path, key_path, value)
|
||||||
try: # owner-only: config files contain API keys
|
try: # owner-only: config files contain API keys
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ def _get_flush_dir():
|
|||||||
"""Return the pending-messages flush directory under the active HERMES_HOME."""
|
"""Return the pending-messages flush directory under the active HERMES_HOME."""
|
||||||
from hermes_constants import get_hermes_home
|
from hermes_constants import get_hermes_home
|
||||||
flush_dir = get_hermes_home() / "pending_messages"
|
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)
|
flush_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||||
if os.name == "posix":
|
if os.name == "posix":
|
||||||
os.chmod(flush_dir, 0o700)
|
os.chmod(flush_dir, 0o700)
|
||||||
|
|||||||
+2
-1
@@ -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:
|
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
|
"""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."""
|
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)
|
secure_parent_dir(target)
|
||||||
atomic_json_write(target, data, mode=0o600, fsync_dir=fsync_dir, **dump_kwargs)
|
atomic_json_write(target, data, mode=0o600, fsync_dir=fsync_dir, **dump_kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ def _persist_oauth_heal_clean_mark(provider_id: str, fingerprint: tuple) -> None
|
|||||||
if marks.get(provider_id) == new_mark:
|
if marks.get(provider_id) == new_mark:
|
||||||
return # already recorded; skip the rewrite
|
return # already recorded; skip the rewrite
|
||||||
marks[provider_id] = new_mark
|
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.
|
# 0o600 like the MCP schema cache: this names credential-store paths.
|
||||||
atomic_json_write(path, marks, mode=0o600)
|
atomic_json_write(path, marks, mode=0o600)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ def read_or_create_install_id(root: Path | None = None) -> Optional[str]:
|
|||||||
if not mint:
|
if not mint:
|
||||||
return existing
|
return existing
|
||||||
try:
|
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
|
# 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.
|
# 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):
|
with _INSTALL_ID_PUBLICATION_LOCK, _install_id_file_lock(root):
|
||||||
|
|||||||
@@ -224,7 +224,8 @@ class LlamaServerSupervisor:
|
|||||||
"executable": proc.exe(), "owner_pid": os.getpid(),
|
"executable": proc.exe(), "owner_pid": os.getpid(),
|
||||||
"owner_create_time": psutil.Process().create_time()}
|
"owner_create_time": psutil.Process().create_time()}
|
||||||
path = state_path()
|
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)
|
atomic_json_write(path, self._state, mode=0o600)
|
||||||
|
|
||||||
def _wait_health(self, timeout_s: int) -> None:
|
def _wait_health(self, timeout_s: int) -> None:
|
||||||
|
|||||||
@@ -134,7 +134,8 @@ def persist_personality(value: Any) -> bool:
|
|||||||
from utils import atomic_roundtrip_yaml_update
|
from utils import atomic_roundtrip_yaml_update
|
||||||
|
|
||||||
config_path = get_hermes_home() / "config.yaml"
|
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)
|
atomic_roundtrip_yaml_update(config_path, "display.personality", name)
|
||||||
try:
|
try:
|
||||||
os.chmod(config_path, 0o600)
|
os.chmod(config_path, 0o600)
|
||||||
|
|||||||
@@ -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)
|
logger.debug("Plugin catalog: unreadable live cache %s: %s", cache, exc)
|
||||||
try:
|
try:
|
||||||
import httpx
|
import httpx
|
||||||
|
from hermes_constants import mkdir_under_hermes_home
|
||||||
|
|
||||||
resp = httpx.get(LIVE_CATALOG_URL, timeout=_REQUEST_TIMEOUT, follow_redirects=True)
|
resp = httpx.get(LIVE_CATALOG_URL, timeout=_REQUEST_TIMEOUT, follow_redirects=True)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
if len(resp.content) > _MAX_LIVE_BYTES:
|
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()
|
data = resp.json()
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("entries"), list):
|
if not isinstance(data, dict) or not isinstance(data.get("entries"), list):
|
||||||
raise ValueError("unexpected live catalog payload")
|
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")
|
cache.write_text(json.dumps(data), encoding="utf-8")
|
||||||
return data
|
return data
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -266,7 +266,8 @@ def _append_entry(entry: LedgerEntry) -> bool:
|
|||||||
]
|
]
|
||||||
pruned.append(asdict(entry))
|
pruned.append(asdict(entry))
|
||||||
try:
|
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
|
# 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).
|
# utf-8 text handle from raising UnicodeEncodeError (a ValueError, not an OSError).
|
||||||
atomic_json_write(path, pruned, mode=0o600, ensure_ascii=True)
|
atomic_json_write(path, pruned, mode=0o600, ensure_ascii=True)
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ def write_breadcrumb(session_id: str, cwd: Optional[str] = None) -> None:
|
|||||||
if not terminal_id:
|
if not terminal_id:
|
||||||
return
|
return
|
||||||
directory = _breadcrumbs_dir()
|
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()
|
now = time.time()
|
||||||
payload = {"session_id": session_id, "cwd": cwd or os.getcwd(), "ts": now}
|
payload = {"session_id": session_id, "cwd": cwd or os.getcwd(), "ts": now}
|
||||||
atomic_json_write(directory / terminal_id, payload, indent=None)
|
atomic_json_write(directory / terminal_id, payload, indent=None)
|
||||||
|
|||||||
@@ -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:
|
def _write_json_0600(path: Path, data: Dict[str, Any]) -> None:
|
||||||
from utils import atomic_json_write
|
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)
|
atomic_json_write(path, data, mode=0o600)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ def _write_config(cfg: dict, path: Path | None = None) -> None:
|
|||||||
out = _apply_edits(cfg.snapshot, cfg, disk)
|
out = _apply_edits(cfg.snapshot, cfg, disk)
|
||||||
elif path.exists():
|
elif path.exists():
|
||||||
out = _apply_edits(cfg.snapshot, cfg, _overlay_local(cfg.snapshot, disk))
|
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)
|
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
|
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
|
cfg.snapshot, cfg.path = copy.deepcopy(dict(cfg)), path
|
||||||
|
|||||||
@@ -2216,7 +2216,8 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|||||||
logger.debug("Could not safely mark OpenViking session %s pending without a run lock", sid)
|
logger.debug("Could not safely mark OpenViking session %s pending without a run lock", sid)
|
||||||
return
|
return
|
||||||
try:
|
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)
|
atomic_json_write(path, {"session_id": sid, "owner_run_id": self._run_id}, mode=0o600)
|
||||||
self._pending_marked_sids.add(sid)
|
self._pending_marked_sids.add(sid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -321,7 +321,8 @@ def _mirror_manual_config_to_openviking_store(*, prompt, select, cancelled, valu
|
|||||||
return _SETUP_CANCELLED
|
return _SETUP_CANCELLED
|
||||||
if replace is False:
|
if replace is False:
|
||||||
continue
|
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
|
# 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.
|
# 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)
|
ov.atomic_json_write(path, ov._ovcli_data_from_connection_values(values), mode=0o600)
|
||||||
|
|||||||
@@ -3449,7 +3449,8 @@ class FeishuAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
def _persist_seen_message_ids(self) -> None:
|
def _persist_seen_message_ids(self) -> None:
|
||||||
try:
|
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:
|
with self._dedup_lock:
|
||||||
recent = self._seen_message_order[-self._dedup_cache_size:]
|
recent = self._seen_message_order[-self._dedup_cache_size:]
|
||||||
# Save as {msg_id: timestamp} so TTL filtering works across restarts.
|
# Save as {msg_id: timestamp} so TTL filtering works across restarts.
|
||||||
|
|||||||
@@ -195,7 +195,8 @@ def _chmod_quiet(path: Path, mode: int) -> None:
|
|||||||
|
|
||||||
def _write_private_json(path: Path, data: Any) -> None:
|
def _write_private_json(path: Path, data: Any) -> None:
|
||||||
"""Atomically write JSON with 0o600 permissions (0o700 parent) where supported."""
|
"""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)
|
_chmod_quiet(path.parent, 0o700)
|
||||||
# mkstemp's 0o600 temp + atomic rename never exposes the token at process umask.
|
# 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)
|
atomic_write_text(path, json.dumps(data, indent=2, ensure_ascii=False), create_mode=0o600)
|
||||||
|
|||||||
+2
-1
@@ -86,7 +86,8 @@ def relay_root(root: Path | str) -> Path:
|
|||||||
def _ensure_dirs(root: Path | str) -> Path:
|
def _ensure_dirs(root: Path | str) -> Path:
|
||||||
base = relay_root(root)
|
base = relay_root(root)
|
||||||
for sub in (OUTBOX_DIR, CLAIMED_DIR, REPLIES_DIR):
|
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
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ def save_completed_result(session) -> None:
|
|||||||
record["command"] = redact_sensitive_text(record["command"], code_file=True, force=True)
|
record["command"] = redact_sensitive_text(record["command"], code_file=True, force=True)
|
||||||
directory = get_hermes_home() / "logs" / "process-results"
|
directory = get_hermes_home() / "logs" / "process-results"
|
||||||
try:
|
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)
|
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
atomic_json_write(directory / f"{session.id}.json", record, mode=0o600)
|
atomic_json_write(directory / f"{session.id}.json", record, mode=0o600)
|
||||||
_result_paths()
|
_result_paths()
|
||||||
|
|||||||
+2
-1
@@ -170,7 +170,8 @@ def _save_discovery_cache(cache: Dict[str, list]) -> None:
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
from utils import atomic_json_write # stdlib+yaml only; no cycle
|
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)
|
atomic_json_write(path, cache, indent=0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Could not write tool discovery cache %s: %s", path, e)
|
logger.debug("Could not write tool discovery cache %s: %s", path, e)
|
||||||
|
|||||||
@@ -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):
|
if read_guard := _background_review_read_before_write_guard(name, target, action, label):
|
||||||
return read_guard
|
return read_guard
|
||||||
original = target.read_text(encoding="utf-8")
|
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)
|
atomic_write_text(target, content, preserve_mode=True, create_mode=0o644)
|
||||||
scan_error = _security_scan_skill(skill_dir)
|
scan_error = _security_scan_skill(skill_dir)
|
||||||
if not scan_error:
|
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):
|
if existing := _find_skill(name):
|
||||||
return _err(f"A skill named '{name}' already exists at {existing['path']}.")
|
return _err(f"A skill named '{name}' already exists at {existing['path']}.")
|
||||||
skill_dir = _resolve_skill_dir(name, category)
|
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"
|
skill_md = skill_dir / "SKILL.md"
|
||||||
atomic_write_text(skill_md, content, preserve_mode=True, create_mode=0o644)
|
atomic_write_text(skill_md, content, preserve_mode=True, create_mode=0o644)
|
||||||
if scan_error := _security_scan_skill(skill_dir):
|
if scan_error := _security_scan_skill(skill_dir):
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ def _read_suppressed_names() -> set:
|
|||||||
|
|
||||||
def _write_manifest(entries: Dict[str, str]):
|
def _write_manifest(entries: Dict[str, str]):
|
||||||
"""Atomic v2 write, preserving an existing file's mode/owner (not mkstemp's 0600)."""
|
"""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:
|
try:
|
||||||
data = "".join(f"{n}:{h}\n" for n, h in sorted(entries.items()))
|
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)
|
atomic_write_text(_manifest_file(), data, tmp_prefix=".bundled_manifest_", preserve_mode=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user