diff --git a/tools/skills_hub.py b/tools/skills_hub.py index 657a455cf4a..01b53b68691 100644 --- a/tools/skills_hub.py +++ b/tools/skills_hub.py @@ -120,10 +120,6 @@ def _validate_skill_name(name: str) -> str: return _normalize_bundle_path(name, field_name="skill name", allow_nested=False) -def _validate_category_name(category: str) -> str: - return _normalize_bundle_path(category, field_name="category", allow_nested=False) - - def _validate_install_parent_path(category: str) -> str: return _normalize_bundle_path(category, field_name="install parent path", allow_nested=True) diff --git a/tools/skills_sync.py b/tools/skills_sync.py index 91c1408c432..81710a7b870 100644 --- a/tools/skills_sync.py +++ b/tools/skills_sync.py @@ -390,7 +390,29 @@ def _backfill_optional_provenance(quiet: bool = False) -> List[str]: if changed: lock_path.parent.mkdir(parents=True, exist_ok=True) - lock_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n") + # Atomic write so a crash mid-write can't silently wipe all provenance + # via the JSONDecodeError fallback above (which resets `installed` to + # an empty dict). + import tempfile + + payload = json.dumps(data, indent=2, ensure_ascii=False) + "\n" + fd, tmp_path = tempfile.mkstemp( + dir=str(lock_path.parent), + prefix=".lock_", + suffix=".tmp", + ) + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(payload) + f.flush() + os.fsync(f.fileno()) + atomic_replace(tmp_path, lock_path) + except BaseException: + try: + os.unlink(tmp_path) + except OSError: + pass + raise return backfilled