From e53b74c39450d85d210ba06e69be5022278eb974 Mon Sep 17 00:00:00 2001 From: islam666 Date: Wed, 3 Jun 2026 08:58:58 +0000 Subject: [PATCH] fix(dist): stop USER_OWNED_EXCLUDE from filtering nested directories The copytree ignore lambda in _copy_dist_payload applied USER_OWNED_EXCLUDE recursively at every directory depth. This caused nested directories whose names matched exclude entries (bin, logs, cache, etc.) to be silently dropped during distribution install/update. Fix: only apply USER_OWNED_EXCLUDE filtering at the root of the staged tree, matching the two-tier pattern used by _clone_all_copytree_ignore and _default_export_ignore in profiles.py. Add 5 tests covering nested bin/logs/cache preservation and top-level filtering still working. Fixes #37954 --- hermes_cli/profile_distribution.py | 7 +- tests/hermes_cli/test_profile_distribution.py | 71 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/hermes_cli/profile_distribution.py b/hermes_cli/profile_distribution.py index a667b5a1e07..c981015d4b0 100644 --- a/hermes_cli/profile_distribution.py +++ b/hermes_cli/profile_distribution.py @@ -573,10 +573,15 @@ def _copy_dist_payload( if entry.is_dir(): if dest.exists(): shutil.rmtree(dest) + staged_resolved = staged.resolve() shutil.copytree( entry, dest, - ignore=lambda d, names: [n for n in names if n in USER_OWNED_EXCLUDE], + ignore=lambda d, names: ( + [n for n in names if n in USER_OWNED_EXCLUDE] + if Path(d).resolve() == staged_resolved + else [] + ), ) else: shutil.copy2(entry, dest) diff --git a/tests/hermes_cli/test_profile_distribution.py b/tests/hermes_cli/test_profile_distribution.py index 235316bd843..82dd1de5bd2 100644 --- a/tests/hermes_cli/test_profile_distribution.py +++ b/tests/hermes_cli/test_profile_distribution.py @@ -497,6 +497,77 @@ class TestSecurity: assert not (target / "skills" / "demo" / "leak.txt").exists() +# =========================================================================== +# Nested directories whose names match USER_OWNED_EXCLUDE must survive install +# =========================================================================== + + +class TestNestedUserOwnedExcludeNotFiltered: + + def test_nested_bin_dir_is_preserved(self, profile_env): + """"A distribution shipping tools/bin/ must not have tools/bin/ dropped + during install even though 'bin' is in USER_OWNED_EXCLUDE.""" + staged = _make_staging_dir(profile_env, "src") + (staged / "tools" / "bin").mkdir(parents=True) + (staged / "tools" / "bin" / "tool.py").write_text("# tool\n") + + plan = install_distribution(str(staged), name="nested_bin") + assert (plan.target_dir / "tools" / "bin").is_dir(), "nested bin/ was dropped" + assert (plan.target_dir / "tools" / "bin" / "tool.py").exists() + + def test_nested_logs_dir_is_preserved(self, profile_env): + staged = _make_staging_dir(profile_env, "src") + (staged / "scripts" / "logs").mkdir(parents=True) + (staged / "scripts" / "logs" / "run.log").write_text("ok\n") + + plan = install_distribution(str(staged), name="nested_logs") + assert (plan.target_dir / "scripts" / "logs").is_dir() + assert (plan.target_dir / "scripts" / "logs" / "run.log").read_text() == "ok\n" + + def test_nested_cache_dir_is_preserved(self, profile_env): + staged = _make_staging_dir(profile_env, "src") + (staged / "control-plane" / "cache").mkdir(parents=True) + (staged / "control-plane" / "cache" / "data.json").write_text("{}\n") + + plan = install_distribution(str(staged), name="nested_cache") + assert (plan.target_dir / "control-plane" / "cache").is_dir() + assert (plan.target_dir / "control-plane" / "cache" / "data.json").exists() + + def test_top_level_user_owned_still_skipped(self, profile_env): + """Top-level entries in USER_OWNED_EXCLUDE must still be skipped — + only nested (deeper) directories should be preserved. + + Note: _bootstrap_user_dirs creates some of these (logs/, sessions/, + memories/) in every fresh profile, so we check that the *staged content* + did not leak through rather than asserting the directory doesn't exist.""" + staged = _make_staging_dir(profile_env, "src") + # Add top-level excluded entries alongside the legit ones + (staged / "bin").mkdir(exist_ok=True) + (staged / "bin" / "shipped_binary").write_text("x") + (staged / "logs").mkdir(exist_ok=True) + (staged / "logs" / "shipped.log").write_text("y\n") + + plan = install_distribution(str(staged), name="top_filter") + # bin/ is not created by _bootstrap_user_dirs so absence means filtered + assert not (plan.target_dir / "bin").exists(), "top-level bin/ should be filtered" + # logs/ is created by _bootstrap_user_dirs even on a clean profile, + # so check that the staged file did NOT land there. + assert not (plan.target_dir / "logs" / "shipped.log").exists(), \ + "staged logs/ content should not leak into target" + + def test_both_nested_and_top_level_coexist(self, profile_env): + """Top-level bin/ filtered, but tools/bin/ kept.""" + staged = _make_staging_dir(profile_env, "src") + (staged / "bin").mkdir(exist_ok=True) + (staged / "bin" / "top.sh").write_text("# top\n") + (staged / "tools" / "bin").mkdir(parents=True) + (staged / "tools" / "bin" / "helper.py").write_text("# helper\n") + + plan = install_distribution(str(staged), name="coexist") + assert not (plan.target_dir / "bin").exists() + assert (plan.target_dir / "tools" / "bin" / "helper.py").exists() + + # =========================================================================== # Install-time metadata (installed_at stamp) # ===========================================================================