From 8f2359b635859f5d2806c7980842be41e67680e3 Mon Sep 17 00:00:00 2001 From: jenkins Date: Sun, 13 Sep 2026 16:52:44 -0500 Subject: [PATCH] hermes: preserve legacy provider session directories --- .../hermes/scripts/execution_pool_worker.py | 23 +++++++++++++++++++ .../test_hermes_execution_pool_worker_v2.py | 19 +++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/services/hermes/scripts/execution_pool_worker.py b/services/hermes/scripts/execution_pool_worker.py index 6a5d7779..3c9423fa 100644 --- a/services/hermes/scripts/execution_pool_worker.py +++ b/services/hermes/scripts/execution_pool_worker.py @@ -93,6 +93,27 @@ def _state_path(assignment: dict[str, Any]) -> Path: return path +def _preserve_runtime_directory(runtime: Path) -> None: + """Move one legacy provider directory aside before installing our symlink.""" + parent = runtime.parent + if parent.is_symlink() or not parent.is_dir() or runtime.is_symlink() or not runtime.is_dir(): + raise ProtocolError(f"provider session path is not a safe directory: {runtime.name}") + preserved = parent / f".hermes-legacy-{runtime.name}" + if preserved.exists() or preserved.is_symlink(): + raise ProtocolError(f"provider session migration conflicts: {runtime.name}") + try: + os.rename(runtime, preserved) + descriptor = os.open(parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + except OSError as error: + raise ProtocolError(f"provider session migration failed: {runtime.name}") from error + if preserved.is_symlink() or not preserved.is_dir(): + raise ProtocolError(f"provider session migration is unsafe: {runtime.name}") + + def _bind_provider_sessions(assignment: dict[str, Any]) -> None: """Attach provider session directories to this exact durable task/run.""" parts = tuple(str(assignment[name]) for name in ("board", "task_id", "run_id")) @@ -143,6 +164,8 @@ def _bind_provider_sessions(assignment: dict[str, Any]) -> None: current.resolve().relative_to(provider_root) if runtime.is_symlink(): runtime.unlink() + elif runtime.is_dir(): + _preserve_runtime_directory(runtime) elif runtime.exists(): raise ProtocolError(f"provider session path is not a symlink: {runtime.name}") runtime.symlink_to(durable) diff --git a/testing/tests/test_hermes_execution_pool_worker_v2.py b/testing/tests/test_hermes_execution_pool_worker_v2.py index d06fc239..b821fd5b 100644 --- a/testing/tests/test_hermes_execution_pool_worker_v2.py +++ b/testing/tests/test_hermes_execution_pool_worker_v2.py @@ -143,6 +143,25 @@ def test_provider_session_binding_replaces_only_owned_symlinks(tmp_path, monkeyp worker._bind_provider_sessions(assignment()) +def test_provider_session_binding_preserves_legacy_runtime_directories(tmp_path, monkeypatch): + _worker_root, _data_root, _codex, claude = prepare_provider_roots(tmp_path, monkeypatch) + legacy = claude / "projects" + (legacy / "prior-session").mkdir(parents=True) + (legacy / "prior-session" / "record").write_text("preserved") + + worker._bind_provider_sessions(assignment()) + + assert legacy.is_symlink() + preserved = claude / ".hermes-legacy-projects/prior-session/record" + assert preserved.read_text() == "preserved" + worker._bind_provider_sessions(assignment()) + legacy.unlink() + legacy.mkdir() + with pytest.raises(protocol.ProtocolError, match="migration conflicts"): + worker._bind_provider_sessions(assignment()) + assert preserved.read_text() == "preserved" + + def test_provider_session_binding_rejects_durable_and_runtime_tampering( tmp_path, monkeypatch ):