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:
ethernet
2026-09-04 03:18:16 -04:00
919 changed files with 71382 additions and 6233 deletions

View File

@@ -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()