mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] skills: curated install falls back to cached SKILL.md when GitHub is unreachable (restores offline install, fixes #113 regression)
This commit is contained in:
@@ -165,21 +165,42 @@ class p_CuratedInstallRequest(BaseModel):
|
||||
folder: str
|
||||
|
||||
|
||||
def p_cached_curated_fallback(folder: str) -> Optional[dict]:
|
||||
"""Offline/rate-limited curated-install fallback: rebuild a single-SKILL.md install
|
||||
payload from the warmed catalog (it already holds the SKILL.md body) so a curated
|
||||
install still works when GitHub is unreachable, minus the folder's extra files.
|
||||
Empty version means it's skipped by update checks until re-installed online."""
|
||||
cached = next((s for s in p_cache.values() if s.get("folder") == folder), None)
|
||||
if cached is None:
|
||||
return None
|
||||
name, description, body = cached.get("name", ""), cached.get("description", ""), cached.get("content", "")
|
||||
return {
|
||||
"skill_id": folder.rsplit("/", 1)[-1], "name": name, "description": description,
|
||||
"files": {"SKILL.md": f"---\nname: {name}\ndescription: {description}\n---\n\n{body}"},
|
||||
"scripts": [], "source": sources.REPO, "folder": folder, "version": "",
|
||||
}
|
||||
|
||||
|
||||
@skill_registry.router.post("/install-curated")
|
||||
async def registry_install_curated(req: p_CuratedInstallRequest):
|
||||
"""Install a curated (anthropics/skills) skill with its FULL folder, not just
|
||||
SKILL.md, so scripts/assets land too (the old path wrote only SKILL.md, which
|
||||
left multi-file skills like pdf/docx with dead script references). Curated is
|
||||
the vetted source, so this is one-click; files are still written inert, never
|
||||
executed. Needs network at install time (the catalog only caches SKILL.md)."""
|
||||
executed. When GitHub is unreachable (offline / rate-limited) it falls back to the
|
||||
catalog's cached SKILL.md, so the install still works (single file, no folder
|
||||
extras), restoring the old offline behavior."""
|
||||
try:
|
||||
resolved = await sources.resolve_curated_skill(req.folder)
|
||||
except RegistryRateLimited:
|
||||
raise HTTPException(status_code=429, detail="GitHub rate limit hit fetching this skill; try again in a few minutes.")
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=f"could not fetch skill: {e}")
|
||||
resolved = p_cached_curated_fallback(req.folder)
|
||||
if resolved is None:
|
||||
if isinstance(e, RegistryRateLimited):
|
||||
raise HTTPException(status_code=429, detail="GitHub rate limit hit and no cached copy of this skill; try again in a few minutes.")
|
||||
raise HTTPException(status_code=502, detail=f"GitHub unreachable and no cached copy: {e}")
|
||||
logger.info(f"curated install: GitHub unreachable ({type(e).__name__}); installing '{req.folder}' from cached SKILL.md (single file, no folder extras)")
|
||||
|
||||
from backend.apps.skills.skills import write_folder_skill, unique_skill_slug
|
||||
slug = unique_skill_slug(resolved["skill_id"])
|
||||
|
||||
@@ -350,6 +350,55 @@ def test_curated_install_writes_full_folder(skills_dir, monkeypatch):
|
||||
assert slug in listed and listed[slug]["has_supporting_files"] is True
|
||||
|
||||
|
||||
def test_curated_install_falls_back_to_cached_skill_md_when_offline(skills_dir, monkeypatch):
|
||||
"""Regression: when GitHub is unreachable (rate-limited/offline), /install-curated
|
||||
falls back to the catalog's cached SKILL.md so the install still works (single file,
|
||||
no folder extras) instead of erroring, restoring the old offline behavior."""
|
||||
import secrets as p_secrets
|
||||
from fastapi.testclient import TestClient
|
||||
from backend.main import app
|
||||
import backend.auth as auth_mod
|
||||
import backend.apps.skill_registry.skill_registry as sr_routes
|
||||
from backend.apps.skill_registry.skill_registry_github import RegistryRateLimited
|
||||
if not auth_mod.TOKEN:
|
||||
auth_mod.TOKEN = p_secrets.token_urlsafe(32)
|
||||
client = TestClient(app, headers={"Authorization": f"Bearer {auth_mod.TOKEN}"})
|
||||
|
||||
async def boom(folder):
|
||||
raise RegistryRateLimited()
|
||||
monkeypatch.setattr("backend.apps.skill_registry.skill_registry_sources.resolve_curated_skill", boom)
|
||||
monkeypatch.setattr(sr_routes, "p_cache", {"PDF": {
|
||||
"name": "PDF", "description": "work with pdfs", "content": "Do PDF things.", "folder": "skills/pdf",
|
||||
}})
|
||||
|
||||
r = client.post("/api/skill-registry/install-curated", json={"folder": "skills/pdf"})
|
||||
assert r.status_code == 200 and r.json()["installed"] is True
|
||||
assert r.json()["files"] == ["SKILL.md"] and r.json()["scripts"] == []
|
||||
slug = r.json()["skill"]["id"]
|
||||
md = (skills_dir / slug / "SKILL.md").read_text()
|
||||
assert "Do PDF things." in md and "name: PDF" in md
|
||||
|
||||
|
||||
def test_curated_install_no_cache_no_network_errors_honestly(skills_dir, monkeypatch):
|
||||
"""If GitHub is unreachable AND nothing's cached, surface an honest error, not a
|
||||
silent half-install."""
|
||||
import secrets as p_secrets
|
||||
from fastapi.testclient import TestClient
|
||||
from backend.main import app
|
||||
import backend.auth as auth_mod
|
||||
import backend.apps.skill_registry.skill_registry as sr_routes
|
||||
if not auth_mod.TOKEN:
|
||||
auth_mod.TOKEN = p_secrets.token_urlsafe(32)
|
||||
client = TestClient(app, headers={"Authorization": f"Bearer {auth_mod.TOKEN}"})
|
||||
|
||||
async def boom(folder):
|
||||
raise RuntimeError("connection refused")
|
||||
monkeypatch.setattr("backend.apps.skill_registry.skill_registry_sources.resolve_curated_skill", boom)
|
||||
monkeypatch.setattr(sr_routes, "p_cache", {})
|
||||
r = client.post("/api/skill-registry/install-curated", json={"folder": "skills/pdf"})
|
||||
assert r.status_code == 502
|
||||
|
||||
|
||||
def test_manual_rm_leaves_no_ghost_blocking_slug(skills_dir):
|
||||
"""A folder deleted out-of-band (manual rm) must not keep squatting its slug via
|
||||
a leftover index entry: existence is by files on disk, and a prune cleans the index."""
|
||||
|
||||
Reference in New Issue
Block a user