Files
hermes-agent/tools/skills_sync_optional.py
ethernet ce49cdbc59 merge: reconcile upstream main with pm audit closeout
Merge upstream 5e645791ac.

Retain the PM feature-flag owner and add upstream connection options.
Use the deny-only window-open policy while trusted external links keep
the existing IPC path. Keep both session-import and external-link copy.
Preserve captured timeout output when adding terminal yield handoff.
Quickstart tests patch the explicit upstream model-assignment owner.
Migrate incoming legacy OS markers to the branch's platforms gate.

Desktop renderer and Electron typechecks passed. Targeted Electron tests
passed (42 tests), Python conflict checks passed (26 tests, 3 skips),
and the plugin-compat import checker passed. CI owns the broad merge gate.
2026-09-06 13:17:07 -04:00

236 lines
11 KiB
Python

"""Official optional-skill provenance: hub-lock backfill and restore. Profile-scoped paths and
patchable helpers resolve through ``_ss()`` at call time so ``tools.skills_sync`` patches work."""
import json
import logging
from contextlib import suppress
from datetime import datetime, timezone
from pathlib import Path, PurePosixPath
from typing import Dict, Iterator, List, Optional, Set, Tuple
from agent.skill_utils import is_excluded_skill_path
from utils import atomic_write_text
logger = logging.getLogger("tools.skills_sync")
def _ss():
"""Live ``tools.skills_sync`` module (imported lazily: it imports helpers from this module)."""
from tools import skills_sync
return skills_sync
def _content_hash(directory: Path) -> str:
"""Hub-lock hash style; provenance metadata only, so fall back to local MD5 sans guard deps."""
try:
from tools.skills_guard import content_hash
return content_hash(directory)
except Exception:
return _ss()._dir_hash(directory)
def _safe_rel_install_path(path: Path, base: Path) -> str:
"""Normalized relative POSIX path; rejects traversal/absolute paths."""
pure = PurePosixPath(posix := path.relative_to(base).as_posix())
parts = [part for part in pure.parts if part not in {"", "."}]
if pure.is_absolute() or not parts or ".." in parts:
raise ValueError(f"Unsafe optional skill path: {posix}")
return "/".join(parts)
# Only generated runtime state, never generic cache/ or arbitrary dotfiles. This
# is updater ownership, not the security scanner's content-integrity policy.
_RUNTIME_CACHE_DIRS = frozenset({"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"})
def _is_runtime_cache(path: Path, skill_dir: Path) -> bool:
"""Whether a skill-relative path is disposable Python/tool runtime state.
Install prefixes may themselves contain a cache directory name. Legacy
sibling bytecode is ignored only alongside its source; source-less .pyc
files can be deliberately shipped or user-owned content.
"""
relative = path.relative_to(skill_dir)
if any(part in _RUNTIME_CACHE_DIRS for part in relative.parts[:-1]):
return True
if path.name in _RUNTIME_CACHE_DIRS and path.is_dir():
return True
return path.suffix in {".pyc", ".pyo"} and path.with_suffix(".py").is_file()
def _ignore_runtime_cache(directory: str, names: List[str]) -> List[str]:
"""copytree callback: don't seed generated caches into managed packages."""
root = Path(directory)
return [name for name in names if _is_runtime_cache(root / name, root)]
def _skill_file_list(skill_dir: Path) -> List[str]:
"""List package files for provenance/diff, excluding generated runtime caches."""
return [f.relative_to(skill_dir).as_posix() for f in sorted(skill_dir.rglob("*"))
if not _is_runtime_cache(f, skill_dir) and f.is_file()]
def _load_hub_lock() -> Optional[dict]:
"""Parse the skills-hub lock; None when missing or unreadable."""
try:
return json.loads((_ss()._skills_dir() / ".hub" / "lock.json").read_text(encoding="utf-8-sig"))
except (FileNotFoundError, json.JSONDecodeError, OSError):
return None
def _hub_lock_entries(data: Optional[dict]) -> List[dict]:
return [e for e in ((data or {}).get("installed") or {}).values() if isinstance(e, dict)]
def _read_hub_install_paths() -> Set[str]:
"""Hub-lock install paths as POSIX strings. Hub-installed skills are owned by the hub: rename
recovery must not move them even when content matches a bundled origin hash (dangling lock)."""
return {str(e["install_path"]).strip("/") for e in _hub_lock_entries(_load_hub_lock()) if e.get("install_path")}
def _iter_optional_skills(optional_dir: Path, *, root_relative: bool) -> Iterator[Tuple[Path, Path, str]]:
"""Yield ``(skill_md, src, install_path)`` for every safe official optional skill."""
for skill_md in sorted(optional_dir.rglob("SKILL.md")):
if (is_excluded_skill_path(skill_md.relative_to(optional_dir), root=optional_dir)
if root_relative else is_excluded_skill_path(skill_md)):
continue
try:
yield skill_md, skill_md.parent, _safe_rel_install_path(skill_md.parent, optional_dir)
except ValueError as e:
logger.debug("Skipping optional skill with unsafe path %s: %s", skill_md.parent, e)
def _optional_skill_index() -> Dict[str, Tuple[str, str, Path]]:
"""Official optional skills keyed by BOTH folder name and frontmatter name (hub-lock slug or
user-facing name). Values are ``(folder_name, install_path, source_dir)``."""
ss = _ss()
optional_dir = ss._get_optional_dir()
index: Dict[str, Tuple[str, str, Path]] = {}
if optional_dir.exists():
for skill_md, src, install_path in _iter_optional_skills(optional_dir, root_relative=True):
value = (src.name, install_path, src)
index[src.name] = index[ss._read_skill_name(skill_md, src.name)] = value
return index
def _move_to_restore_backup(path: Path, backup_root: Path) -> str:
"""Move an existing skill directory into a restore backup, preserving rel path."""
ss = _ss()
rel = path.relative_to(ss._skills_dir())
target, suffix = backup_root / rel, 0
while target.exists():
suffix += 1
target = (backup_root / rel).with_name(f"{rel.name}-{suffix}")
ss._move_dir(path, target)
return rel.as_posix()
def restore_official_optional_skill(name: str, *, restore: bool = False) -> dict:
"""Restore one or all official optional skills from repo source. ``restore=False`` only
backfills exact-match provenance; ``restore=True`` also backs up matching active copies
and copies the official source into its canonical path."""
ss = _ss()
index = _optional_skill_index()
if index and name in {"all", "*"}:
targets = sorted(set(index.values()), key=lambda item: item[1])
elif name in index:
targets = [index[name]]
else:
message = f"Official optional skill not found: {name}" if index else "No official optional skills directory found."
return {"ok": False, "message": message, "restored": [], "backfilled": [], "backed_up": []}
restored: List[str] = []
backed_up: List[str] = []
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
backup_root = ss._skills_dir() / ".restore-backups" / f"official-optional-{timestamp}"
for folder_name, install_path, src in targets if restore else []:
dest = ss._skills_dir() / Path(*install_path.split("/"))
canonical_ok = dest.exists() and ss._dir_hash(dest) == ss._dir_hash(src)
# Active copies by frontmatter name or folder slug (the curator may have moved the skill
# into another category); ``dest`` itself is handled below.
names = {folder_name, ss._read_skill_name(src / "SKILL.md", folder_name)}
for md in ss._iter_active_skill_mds(sort=True):
match = md.parent
if match != dest and match.exists() and (
match.name == folder_name or ss._read_skill_name(md, match.name) in names):
backed_up.append(_move_to_restore_backup(match, backup_root))
if dest.exists() and not canonical_ok:
backed_up.append(_move_to_restore_backup(dest, backup_root))
if not dest.exists():
ss._copy_dir(src, dest)
restored.append(folder_name)
return {
"ok": True, "message": "Official optional skill repair complete.", "restored": restored,
"backfilled": _backfill_optional_provenance(quiet=True), "backed_up": backed_up,
"backup_dir": str(backup_root) if backed_up else ""}
def _index_installed_skill_dirs_by_name() -> Dict[str, List[Path]]:
"""Installed skills by directory name in one active-tree scan, skipping anything that
resolves outside the skills tree (symlinks/external)."""
ss = _ss()
index: Dict[str, List[Path]] = {}
root = ss._skills_dir().resolve()
for skill_md in ss._iter_active_skill_mds():
with suppress(OSError, ValueError):
skill_md.parent.resolve().relative_to(root)
index.setdefault(skill_md.parent.name, []).append(skill_md.parent)
return index
def _relocated_dest(src_name: str, index: Dict[str, List[Path]]) -> Optional[Tuple[Path, str]]:
"""``(dest, install_path)`` of a UNIQUE same-directory-name match, else None: the active
tree may hold a skill under a DIFFERENT category than the repo (upstream reorganized,
installed copy kept its place); ambiguity gives no basis to pick."""
if len(candidates := index.get(src_name, [])) != 1:
return None
try:
return candidates[0], _safe_rel_install_path(candidates[0], _ss()._skills_dir())
except ValueError as e:
logger.debug("Skipping relocated optional skill %s: %s", candidates[0], e)
return None
def _backfill_optional_provenance(quiet: bool = False) -> List[str]:
"""Mark already-present official optional skills as hub-installed: formerly bundled (or
hand-copied) skills now under optional-skills/ get official provenance when byte-identical
to the source; modified/local skills are left alone."""
ss = _ss()
optional_dir = ss._get_optional_dir()
if not optional_dir.exists():
return []
data = _load_hub_lock()
if data is None:
data = {"version": 1, "installed": {}}
installed = data.setdefault("installed", {})
existing_paths = {entry.get("install_path") for entry in _hub_lock_entries(data)}
backfilled: List[str] = []
installed_dir_index: Optional[Dict[str, List[Path]]] = None
for _skill_md, src, install_path in _iter_optional_skills(optional_dir, root_relative=False):
lock_name = src.name
if lock_name in installed or install_path in existing_paths:
continue
dest = ss._skills_dir() / Path(*install_path.split("/"))
if not dest.is_dir():
if installed_dir_index is None:
installed_dir_index = _index_installed_skill_dirs_by_name()
if (found := _relocated_dest(src.name, installed_dir_index)) is None:
continue
dest, install_path = found # still requires a byte-identical hash below
if install_path in existing_paths or ss._dir_hash(dest) != ss._dir_hash(src):
continue
timestamp = datetime.now(timezone.utc).isoformat()
installed[lock_name] = {
"source": "official", "identifier": f"official/{install_path}",
"trust_level": "builtin", "scan_verdict": "backfilled",
"content_hash": _content_hash(dest), "install_path": install_path,
"files": _skill_file_list(dest), "metadata": {"backfilled_from": "optional-skills"},
"installed_at": timestamp, "updated_at": timestamp}
existing_paths.add(install_path)
backfilled.append(lock_name)
if not quiet:
print(f" = {lock_name} (official optional provenance backfilled)")
if backfilled: # atomic: a mid-write crash must not wipe provenance (reader resets bad JSON)
atomic_write_text(ss._skills_dir() / ".hub" / "lock.json", tmp_prefix=".lock_",
content=json.dumps(data, indent=2, ensure_ascii=False) + "\n")
return backfilled