feat(apps): add extension registry source

This commit is contained in:
Xubin Ren 2026-05-29 04:40:26 +08:00
parent 232df45126
commit c1357e86de
4 changed files with 147 additions and 26 deletions

View File

@ -25,7 +25,13 @@ from nanobot.security.workspace_policy import is_path_within
CLI_ANYTHING_REGISTRY_URL = "https://hkuds.github.io/CLI-Anything/registry.json"
CLI_ANYTHING_PUBLIC_REGISTRY_URL = "https://hkuds.github.io/CLI-Anything/public_registry.json"
CLI_ANYTHING_RAW_BASE = "https://raw.githubusercontent.com/HKUDS/CLI-Anything/main"
CLI_ANYTHING_RAW_SKILLS_BASE = f"{CLI_ANYTHING_RAW_BASE}/skills/"
NANOBOT_EXTENSION_REGISTRY_URL = "https://raw.githubusercontent.com/Re-bin/nanobot-extension/main/registry.json"
NANOBOT_EXTENSION_RAW_BASE = "https://raw.githubusercontent.com/Re-bin/nanobot-extension/main"
_CATALOG_SOURCES = (
("harness", CLI_ANYTHING_REGISTRY_URL, CLI_ANYTHING_RAW_BASE, True),
("public", CLI_ANYTHING_PUBLIC_REGISTRY_URL, CLI_ANYTHING_RAW_BASE, True),
("extensions", NANOBOT_EXTENSION_REGISTRY_URL, NANOBOT_EXTENSION_RAW_BASE, False),
)
_MAX_TOOL_OUTPUT_CHARS = 12_000
_MAX_ARTIFACT_SCAN_PATHS = 4_000
@ -344,16 +350,17 @@ def _safe_skill_path(value: str) -> str | None:
return value if parts[-1] == "SKILL.md" else None
def _skill_content_url(skill_md: str) -> str | None:
def _skill_content_url(skill_md: str, *, raw_base: str = CLI_ANYTHING_RAW_BASE) -> str | None:
safe_path = _safe_skill_path(skill_md)
if safe_path:
return f"{CLI_ANYTHING_RAW_BASE}/{safe_path}"
return f"{raw_base.rstrip('/')}/{safe_path}"
parsed = urlparse(skill_md)
if parsed.scheme != "https" or parsed.netloc != "raw.githubusercontent.com":
return None
if not skill_md.startswith(CLI_ANYTHING_RAW_SKILLS_BASE):
raw_prefix = raw_base.rstrip("/") + "/"
if not skill_md.startswith(raw_prefix):
return None
suffix = skill_md.removeprefix(f"{CLI_ANYTHING_RAW_BASE}/")
suffix = skill_md.removeprefix(raw_prefix)
return skill_md if _safe_skill_path(suffix) else None
@ -435,27 +442,22 @@ class CliAppManager:
return data
def catalog(self, *, force_refresh: bool = False) -> tuple[list[dict[str, Any]], str | None]:
registries = [
(
"harness",
self._fetch_registry(
CLI_ANYTHING_REGISTRY_URL,
self._cache_path("harness"),
registries: list[tuple[str, str, dict[str, Any]]] = []
for source, url, raw_base, required in _CATALOG_SOURCES:
try:
registry = self._fetch_registry(
url,
self._cache_path(source),
force_refresh=force_refresh,
),
),
(
"public",
self._fetch_registry(
CLI_ANYTHING_PUBLIC_REGISTRY_URL,
self._cache_path("public"),
force_refresh=force_refresh,
),
),
]
)
except Exception:
if required:
raise
continue
registries.append((source, raw_base, registry))
apps_by_name: dict[str, dict[str, Any]] = {}
updated_values: list[str] = []
for source, registry in registries:
for source, raw_base, registry in registries:
meta = registry.get("meta")
if isinstance(meta, dict) and isinstance(meta.get("updated"), str):
updated_values.append(meta["updated"])
@ -464,6 +466,7 @@ class CliAppManager:
continue
entry = dict(row)
entry["_source"] = source
entry["_raw_base"] = raw_base
key = str(entry["name"]).lower()
previous = apps_by_name.get(key)
if previous:
@ -476,6 +479,15 @@ class CliAppManager:
apps_by_name[key] = entry
return list(apps_by_name.values()), max(updated_values) if updated_values else None
def _manifest_source(self, app: dict[str, Any]) -> str:
source = str(app.get("_source") or "harness")
if source == "extensions":
return "nanobot-extension"
return f"cli-anything:{source}"
def _trust_registry(self, app: dict[str, Any]) -> str:
return "nanobot-extension" if str(app.get("_source") or "") == "extensions" else "cli-anything"
def get_app(self, name: str, *, force_refresh: bool = False) -> dict[str, Any]:
wanted = name.lower()
for app in self.catalog(force_refresh=force_refresh)[0]:
@ -640,14 +652,14 @@ class CliAppManager:
version=str(app.get("version") or ""),
description=_catalog_description(app),
category=str(app.get("category") or "uncategorized"),
source=f"cli-anything:{app.get('_source') or 'harness'}",
source=self._manifest_source(app),
logo_url=logo_url,
brand_color=brand_color,
capabilities=capabilities,
install=install,
remove=remove,
trust={
"registry": "cli-anything",
"registry": self._trust_registry(app),
"level": "catalog",
"review_status": "catalog_entry",
},
@ -793,7 +805,7 @@ class CliAppManager:
skill_md = str(app.get("skill_md") or "").strip()
if not skill_md:
return None
url = _skill_content_url(skill_md)
url = _skill_content_url(skill_md, raw_base=str(app.get("_raw_base") or CLI_ANYTHING_RAW_BASE))
if not url:
return None
try:

View File

@ -261,6 +261,10 @@ async def test_cli_app_scope_controls_working_dir(
json.dumps({"_cached_at": time.time(), "data": {"meta": {}, "clis": []}}),
encoding="utf-8",
)
(data_dir / "extensions_registry_cache.json").write_text(
json.dumps({"_cached_at": time.time(), "data": {"meta": {}, "clis": []}}),
encoding="utf-8",
)
CliAppManager(workspace=project, data_dir=data_dir)._save_installed(
{"demo": {"entry_point": "demo-cli"}}
)

View File

@ -121,6 +121,7 @@ def _seed_catalog(manager: CliAppManager) -> None:
}
_write_cache(manager._cache_path("harness"), harness)
_write_cache(manager._cache_path("public"), public)
_write_cache(manager._cache_path("extensions"), {"meta": {}, "clis": []})
def test_payload_merges_catalog_and_marks_unsupported_installs(tmp_path: Path) -> None:
@ -187,6 +188,7 @@ def test_payload_uses_anygen_official_domain_for_logo(tmp_path: Path) -> None:
],
},
)
_write_cache(manager._cache_path("extensions"), {"meta": {}, "clis": []})
payload = manager.payload()
@ -195,6 +197,75 @@ def test_payload_uses_anygen_official_domain_for_logo(tmp_path: Path) -> None:
assert app["logo_url"] == "https://www.google.com/s2/favicons?domain=anygen.io&sz=64"
def test_payload_includes_nanobot_extension_registry(tmp_path: Path) -> None:
manager = _manager(tmp_path)
_write_cache(manager._cache_path("harness"), {"meta": {"updated": "2026-04-16"}, "clis": []})
_write_cache(manager._cache_path("public"), {"meta": {"updated": "2026-04-18"}, "clis": []})
_write_cache(
manager._cache_path("extensions"),
{
"meta": {"updated": "2026-05-29"},
"clis": [
{
"name": "hyperframes",
"display_name": "HyperFrames",
"version": "latest",
"description": "HTML-to-MP4 motion graphics CLI",
"category": "video",
"package_manager": "npm",
"npm_package": "hyperframes",
"install_cmd": "npm install -g hyperframes",
"entry_point": "hyperframes",
"skill_md": "skills/hyperframes/SKILL.md",
}
],
},
)
payload = manager.payload()
assert payload["catalog_updated_at"] == "2026-05-29"
app = payload["apps"][0]
assert app["name"] == "hyperframes"
assert app["source"] == "extensions"
assert app["install_supported"] is True
assert app["manifest"]["source"] == "nanobot-extension"
assert app["manifest"]["trust"]["registry"] == "nanobot-extension"
def test_optional_extension_registry_failure_does_not_break_payload(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = _manager(tmp_path)
_write_cache(
manager._cache_path("harness"),
{
"meta": {"updated": "2026-04-16"},
"clis": [
{
"name": "gimp",
"display_name": "GIMP",
"description": "Image editing",
"install_cmd": "pip install cli-anything-gimp",
"entry_point": "cli-anything-gimp",
}
],
},
)
_write_cache(manager._cache_path("public"), {"meta": {"updated": "2026-04-18"}, "clis": []})
def fail_get(*args, **kwargs):
raise RuntimeError("network unavailable")
monkeypatch.setattr("nanobot.apps.cli.service.httpx.get", fail_get)
payload = manager.payload()
assert payload["catalog_updated_at"] == "2026-04-18"
assert [app["name"] for app in payload["apps"]] == ["gimp"]
def test_install_dispatches_safe_pip_and_installs_skill(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
@ -333,6 +404,38 @@ def test_fetch_skill_content_allows_cli_anything_raw_skill_url(
]
def test_fetch_skill_content_uses_extension_raw_base_for_relative_skills(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = _manager(tmp_path)
seen: list[str] = []
class Response:
text = "---\nname: hyperframes\ndescription: HyperFrames\n---\n# HyperFrames\n"
@staticmethod
def raise_for_status() -> None:
return None
def fake_get(url: str, **kwargs):
seen.append(url)
return Response()
monkeypatch.setattr("nanobot.apps.cli.service.httpx.get", fake_get)
content = manager._fetch_skill_content({
"name": "hyperframes",
"skill_md": "skills/hyperframes/SKILL.md",
"_raw_base": "https://raw.githubusercontent.com/Re-bin/nanobot-extension/main",
})
assert content and "# HyperFrames" in content
assert seen == [
"https://raw.githubusercontent.com/Re-bin/nanobot-extension/main/skills/hyperframes/SKILL.md"
]
def test_uninstall_removes_installed_state_and_generated_skill(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,

View File

@ -41,6 +41,7 @@ def test_run_cli_app_uses_installed_registry_app(
}
_write_cache(data_dir / "harness_registry_cache.json", registry)
_write_cache(data_dir / "public_registry_cache.json", {"meta": {}, "clis": []})
_write_cache(data_dir / "extensions_registry_cache.json", {"meta": {}, "clis": []})
CliAppManager(workspace=workspace, data_dir=data_dir)._save_installed(
{"gimp": {"entry_point": "cli-anything-gimp"}}
)
@ -102,6 +103,7 @@ def test_run_cli_app_rejects_uninstalled_app(tmp_path: Path, monkeypatch) -> Non
}
_write_cache(data_dir / "harness_registry_cache.json", registry)
_write_cache(data_dir / "public_registry_cache.json", {"meta": {}, "clis": []})
_write_cache(data_dir / "extensions_registry_cache.json", {"meta": {}, "clis": []})
monkeypatch.setattr("nanobot.apps.cli.service.get_runtime_subdir", lambda _name: data_dir)
tool = CliAppsTool(workspace=workspace, restrict_to_workspace=True)