diff --git a/loopx/capabilities/benchmark_toolkit/README.md b/loopx/capabilities/benchmark_toolkit/README.md index 93cf6ac9f..176e6e4bd 100644 --- a/loopx/capabilities/benchmark_toolkit/README.md +++ b/loopx/capabilities/benchmark_toolkit/README.md @@ -261,7 +261,14 @@ release-snapshot CLI, requires the `codex_app_ssh_goal` profile and interface bu and proves that the returned body names that installed CLI. For an isolated case it also replaces the generic global-registry token with the explicit case registry. Keep app-server on `native_codex_profile_environment`; it supplies only the -formal profile's `HOME`, `CODEX_HOME`, and `PATH`. The upstream provider value +formal profile's `HOME`, `CODEX_HOME`, `PATH`, and home-scoped temporary directory +(`TMPDIR`, `TMP`, `TEMP`). Installation and later profile calls use that same +temporary scope, so equal-source profiles do not share runtime locators or +shutdown ownership. Stop profile callers, then invoke +`doctor --installation-only --restart-runtime` through its installed CLI with +`native_codex_profile_environment(profile)` before removing the profile; require +`stopped` or `not_running`, leaving incomplete shutdown visible. +The upstream provider value must remain in `serve_runner_owned_provider_gateway`, while app-server receives only the loopback gateway URL and a fixed non-secret sentinel. On Linux, place app-server inside `native_codex_isolation` so its fresh PID namespace and diff --git a/loopx/capabilities/benchmark_toolkit/native_codex_profile.py b/loopx/capabilities/benchmark_toolkit/native_codex_profile.py index d853e50a5..d499179c1 100644 --- a/loopx/capabilities/benchmark_toolkit/native_codex_profile.py +++ b/loopx/capabilities/benchmark_toolkit/native_codex_profile.py @@ -202,6 +202,9 @@ def _formal_install_environment( env.update( { "HOME": str(paths["home"]), + "TMPDIR": str(paths["home"]), + "TMP": str(paths["home"]), + "TEMP": str(paths["home"]), "SHELL": "/bin/sh", "CODEX_HOME": str(paths["codex_home"]), "LOOPX_PYTHON": python_executable, @@ -240,6 +243,11 @@ def native_codex_profile_environment( env.update( { "HOME": str(profile.home), + # The Effect runtime locator is temp-scoped and content-addressed. + # Equal-source profiles must not share its writer or stop owner. + "TMPDIR": str(profile.home), + "TMP": str(profile.home), + "TEMP": str(profile.home), "CODEX_HOME": str(profile.codex_home), "PATH": f"{profile.bin_dir}{os.pathsep}{inherited_path}", } diff --git a/tests/capabilities/test_native_codex_profile_skill_versions.py b/tests/capabilities/test_native_codex_profile_skill_versions.py index 9da0b58d6..4889bc532 100644 --- a/tests/capabilities/test_native_codex_profile_skill_versions.py +++ b/tests/capabilities/test_native_codex_profile_skill_versions.py @@ -2,6 +2,8 @@ from __future__ import annotations import sys +import json +import subprocess from pathlib import Path import pytest @@ -11,6 +13,7 @@ NativeCodexProfileError, inspect_native_codex_profile, install_native_codex_profile, + native_codex_profile_environment, ) @@ -37,3 +40,41 @@ def test_profile_compares_skills_with_its_own_runtime(tmp_path, monkeypatch): ) with pytest.raises(NativeCodexProfileError, match="loopx_version_mismatch"): inspect_native_codex_profile(profile.root, require_clean_source=False) + + +@pytest.mark.skipif(sys.platform == "win32", reason="POSIX release profile installer") +def test_equal_source_profiles_have_independent_runtime_shutdown(tmp_path): + source = Path(__file__).resolve().parents[2] + profiles = [ + install_native_codex_profile( + source, tmp_path / name, require_clean_source=False, + ) + for name in ("first", "second") + ] + + def doctor(profile, *args): + completed = subprocess.run( + [str(profile.cli_bin), "--format", "json", "doctor", + "--installation-only", *args], + cwd=profile.root, + env=native_codex_profile_environment(profile), + capture_output=True, text=True, timeout=30, check=True, + ) + payload = json.loads(completed.stdout) + assert payload["ok"] + return payload + + try: + assert profiles[0].source_revision == profiles[1].source_revision + for profile in profiles: + assert doctor(profile, "--deep")["typescript_control_plane"]["ready"] + stopped = doctor(profiles[0], "--restart-runtime")["effect_runtime_restart"] + assert stopped["status"] == "stopped" + assert Path(stopped["info_path"]).is_relative_to(profiles[0].home) + other = doctor(profiles[1])["typescript_control_plane"] + assert other["runtime_lifecycle"]["state"] == "running" + finally: + for profile in profiles: + assert doctor(profile, "--restart-runtime")["effect_runtime_restart"][ + "status" + ] in {"stopped", "not_running"}