fix(skills): refuse SKILLS_DIR root in rmtree guard, not just outside-tree

The salvaged guard allowed _rmtree_writable(SKILLS_DIR) itself. No call
site ever passes the root — every site passes a skill subdir or its .bak
sibling — so allowing the root only preserves the #48200 footgun (a dest
that collapses to the root wipes every installed skill). Require a strict
strict-child relationship and update the test that documented the
nonexistent 'full reset' capability.
This commit is contained in:
Teknium 2026-06-18 05:47:20 -07:00
parent f1254c8eaf
commit 25c590ccd0
2 changed files with 22 additions and 8 deletions

View file

@ -684,9 +684,16 @@ def _rmtree_writable(path: Path) -> None:
# instead of silently destroying the user's install.
target = Path(path).resolve()
skills_root = SKILLS_DIR.resolve()
if not (target == skills_root or skills_root in target.parents):
# Every legitimate caller passes a skill directory or its ``.bak``
# sibling — always a strict child of the skills root. The skills root
# itself must never be removed: a ``dest`` that collapses to
# ``SKILLS_DIR`` (e.g. a relative path resolving to ``.``) would wipe
# every installed skill, and its ``.bak`` sibling lands one level up in
# ``HERMES_HOME``. Require a strict-child relationship so both escape
# into the skills root and out of it are refused.
if skills_root not in target.parents:
raise ValueError(
f"refusing to rmtree {target!r}: not under {skills_root!r} "
f"refusing to rmtree {target!r}: not strictly under {skills_root!r} "
f"(scope guard — see #48200)"
)
import stat