fix(credential_files): apply the same exclusions to the symlink-safe mount copy

_safe_skills_path() is the sibling of iter_skills_files(): when a symlink in
skills/ forces a sanitized copy for mount-based backends (Docker/Singularity),
it rglob-copied the whole tree — .hub, .curator_backups, node_modules and all.
Prune EXCLUDED_SKILL_DIRS before descending, same rule as the sync generator,
so the mounted copy never carries (or walks) the bookkeeping trees either.
This commit is contained in:
kshitijk4poor
2026-09-03 01:05:00 +05:30
committed by kshitij
parent 1d06ef3a5d
commit ff7233b815
2 changed files with 41 additions and 10 deletions
+28
View File
@@ -112,6 +112,34 @@ class TestSkillsDirectoryMount:
# Symlink should NOT be present
assert not (safe_path / "evil_link").exists()
def test_sanitized_copy_skips_bookkeeping_dirs(self, tmp_path):
"""The symlink-safe copy is what gets mounted, so it must apply the
same EXCLUDED_SKILL_DIRS rule as the per-file sync path."""
hermes_home = tmp_path / ".hermes"
skills_dir = hermes_home / "skills"
(skills_dir / "cat" / "myskill" / "references").mkdir(parents=True)
(skills_dir / "cat" / "myskill" / "SKILL.md").write_text("# skill")
(skills_dir / "cat" / "myskill" / "references" / "api.md").write_text("ref")
for excluded in (".hub", ".curator_backups", "node_modules"):
junk = skills_dir / excluded / "vendored"
junk.mkdir(parents=True)
(junk / "blob.bin").write_bytes(b"\0" * 64)
# Force the sanitizing copy path.
secret = tmp_path / "secret.txt"
secret.write_text("TOP SECRET")
(skills_dir / "evil_link").symlink_to(secret)
with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}):
mounts = get_skills_directory_mount()
safe_path = Path(mounts[0]["host_path"])
assert safe_path != skills_dir
assert (safe_path / "cat" / "myskill" / "SKILL.md").exists()
assert (safe_path / "cat" / "myskill" / "references" / "api.md").exists()
assert not (safe_path / "evil_link").exists()
for excluded in (".hub", ".curator_backups", "node_modules"):
assert not (safe_path / excluded).exists(), excluded
def test_no_symlinks_returns_original_dir(self, tmp_path):
"""When no symlinks exist, the original dir is returned (no copy)."""
hermes_home = tmp_path / ".hermes"
+13 -10
View File
@@ -326,16 +326,19 @@ def _safe_skills_path(skills_dir: Path) -> str:
safe_dir = Path(tempfile.mkdtemp(prefix="hermes-skills-safe-"))
_safe_skills_tempdir = safe_dir
for item in skills_dir.rglob("*"):
if item.is_symlink():
continue
rel = item.relative_to(skills_dir)
target = safe_dir / rel
if item.is_dir():
target.mkdir(parents=True, exist_ok=True)
elif item.is_file():
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(item), str(target))
# Same exclusion rule as the per-file sync path (_iter_syncable_files):
# the sanitized copy is what gets mounted, so it must not carry the
# bookkeeping trees either. Prune before descending so a multi-GB
# .curator_backups is never even walked.
for dirpath, dirnames, filenames in os.walk(skills_dir):
dirnames[:] = sorted(d for d in dirnames if d not in EXCLUDED_SKILL_DIRS)
base = Path(dirpath)
(safe_dir / base.relative_to(skills_dir)).mkdir(parents=True, exist_ok=True)
for name in filenames:
item = base / name
if item.is_symlink() or not item.is_file():
continue
shutil.copy2(str(item), str(safe_dir / item.relative_to(skills_dir)))
def _cleanup():
if safe_dir.is_dir():