Merge remote-tracking branch 'upstream/main' into ethie/pm-clean
# Conflicts: # .github/actions/detect-changes/action.yml # .github/workflows/ci.yaml # .github/workflows/tests-os.yml # agent/prompt_builder.py # agent/ssl_verify.py # agent/subdirectory_hints.py # apps/desktop/electron/main.ts # apps/desktop/electron/preload.ts # apps/desktop/src/app/settings/about-settings.tsx # apps/desktop/src/global.d.ts # apps/desktop/src/i18n/ar.ts # apps/desktop/src/store/updates.ts # cron/suggestions.py # gateway/channel_directory.py # hermes_cli/config.py # hermes_cli/doctor.py # hermes_cli/linux_desktop_entry.py # hermes_cli/main.py # hermes_cli/web_routers/profiles.py # hermes_constants.py # plugins/platforms/photon/adapter.py # scripts/ci/classify_changes.py # scripts/install.ps1 # tests/agent/test_relay_runtime_plugins.py # tests/ci/test_classify_changes.py # tests/hermes_cli/test_gui_command.py # tests/hermes_cli/test_linux_desktop_entry.py # tests/hermes_cli/test_update_fleet_restart_pending.py # tests/state/test_fts_runtime_rebuild.py # tests/tools/test_lazy_deps.py # tests/tools/test_macos_protected_search.py # tools/browser_tool.py # tools/file_operations.py # tools/lazy_deps.py # tools/mcp_tool.py # tools/working_diff.py # uv.lock
This commit is contained in:
@@ -113,6 +113,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"
|
||||
@@ -149,6 +177,46 @@ class TestIterSkillsFiles:
|
||||
# Symlink should be excluded
|
||||
assert not any("evil" in f["container_path"] for f in files)
|
||||
|
||||
def test_skips_excluded_bookkeeping_dirs(self, tmp_path):
|
||||
"""Bookkeeping and dependency dirs must not be uploaded to a sandbox.
|
||||
|
||||
The sync path used a bare rglob("*"), so the .hub download cache,
|
||||
.archive, curator backups and any node_modules/.git under a skills
|
||||
tree were packed up on every sync even though the sandbox never
|
||||
reads them. Sync now honours EXCLUDED_SKILL_DIRS like discovery.
|
||||
"""
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
skills_dir = hermes_home / "skills"
|
||||
(skills_dir / "cat" / "myskill").mkdir(parents=True)
|
||||
(skills_dir / "cat" / "myskill" / "SKILL.md").write_text("# skill")
|
||||
# Progressive-disclosure support files must still be synced.
|
||||
(skills_dir / "cat" / "myskill" / "references").mkdir()
|
||||
(skills_dir / "cat" / "myskill" / "references" / "api.md").write_text("ref")
|
||||
|
||||
for excluded in (".hub", ".archive", ".curator_backups", "node_modules"):
|
||||
junk = skills_dir / excluded / "vendored"
|
||||
junk.mkdir(parents=True)
|
||||
(junk / "SKILL.md").write_text("# stale copy")
|
||||
# Also nested inside an otherwise-valid skill package.
|
||||
cache = skills_dir / "cat" / "myskill" / "__pycache__"
|
||||
cache.mkdir()
|
||||
(cache / "helper.cpython-311.pyc").write_text("bytecode")
|
||||
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}):
|
||||
files = iter_skills_files()
|
||||
|
||||
paths = {f["container_path"] for f in files}
|
||||
assert "/root/.hermes/skills/cat/myskill/SKILL.md" in paths
|
||||
assert "/root/.hermes/skills/cat/myskill/references/api.md" in paths
|
||||
for excluded in (
|
||||
".hub",
|
||||
".archive",
|
||||
".curator_backups",
|
||||
"node_modules",
|
||||
"__pycache__",
|
||||
):
|
||||
assert not any(excluded in path for path in paths), excluded
|
||||
|
||||
def test_empty_when_no_skills_dir(self, tmp_path):
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
|
||||
Reference in New Issue
Block a user