mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
The ACP Registry schema supports uvx as a first-class distribution method alongside npx and binary. Pointing the registry directly at the existing hermes-agent PyPI release removes: - the @nousresearch npm scope (we don't own it) - a separate npm publish step on every weekly release - 90 lines of Node launcher + tests in packages/hermes-agent-acp/ The Zed registry now installs Hermes via: uvx --from 'hermes-agent[acp]==<version>' hermes-acp This is the same command the npm launcher was shelling out to anyway, so end-user behavior is unchanged. Registry CI validates the PyPI URL + version-pin exact match automatically. Changes: - acp_registry/agent.json: distribution.npx -> distribution.uvx - delete packages/hermes-agent-acp/ entirely - scripts/release.py: drop npm-launcher bump paths, keep manifest lockstep - tests/acp/test_registry_manifest.py: assert uvx shape + version pin - tests/scripts/test_release_acp_registry.py: rewrite for uvx-only shape - docs (user-guide + dev-guide): drop all npm-launcher references - delete docs/plans/acp-registry-zed-integration.md (stale, npm-shaped) Validated against agentclientprotocol/registry agent.schema.json via jsonschema. hermes-agent==0.13.0 is already live on PyPI.
90 lines
3 KiB
Python
90 lines
3 KiB
Python
"""Tests for ACP Registry metadata shipped with Hermes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import tomllib
|
|
from pathlib import Path
|
|
import xml.etree.ElementTree as ET
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
MANIFEST = ROOT / "acp_registry" / "agent.json"
|
|
ICON = ROOT / "acp_registry" / "icon.svg"
|
|
FORBIDDEN_MANIFEST_KEYS = {"schema_version", "display_name"}
|
|
ALLOWED_DISTRIBUTIONS = {"binary", "npx", "uvx"}
|
|
|
|
|
|
def _manifest() -> dict:
|
|
return json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
|
|
|
|
def _pyproject_version() -> str:
|
|
data = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
return data["project"]["version"]
|
|
|
|
|
|
def test_agent_json_matches_official_registry_required_fields():
|
|
data = _manifest()
|
|
|
|
assert FORBIDDEN_MANIFEST_KEYS.isdisjoint(data)
|
|
assert data["id"] == "hermes-agent"
|
|
assert re.fullmatch(r"[a-z][a-z0-9-]*", data["id"])
|
|
assert data["name"] == "Hermes Agent"
|
|
assert data["description"]
|
|
assert data["repository"] == "https://github.com/NousResearch/hermes-agent"
|
|
assert data["website"].startswith("https://hermes-agent.nousresearch.com/")
|
|
assert data["authors"] == ["Nous Research"]
|
|
assert data["license"] == "MIT"
|
|
assert set(data["distribution"]) <= ALLOWED_DISTRIBUTIONS
|
|
|
|
|
|
def test_agent_json_uses_uvx_distribution_without_local_command_fields():
|
|
data = _manifest()
|
|
|
|
assert set(data["distribution"]) == {"uvx"}
|
|
uvx = data["distribution"]["uvx"]
|
|
# Schema allows {package, args, env}; we use {package, args}.
|
|
assert set(uvx) <= {"package", "args", "env"}
|
|
assert "package" in uvx
|
|
assert uvx["package"] == f"hermes-agent[acp]=={data['version']}"
|
|
assert uvx["args"] == ["hermes-acp"]
|
|
# Old command-shape fields must not leak back in.
|
|
assert "type" not in data["distribution"]
|
|
assert "command" not in data["distribution"]
|
|
|
|
|
|
def test_agent_json_version_matches_pyproject():
|
|
assert _manifest()["version"] == _pyproject_version()
|
|
|
|
|
|
def test_agent_json_pins_uvx_package_to_pyproject_version():
|
|
"""The registry CI rejects ``@latest`` and floating pins; the manifest must
|
|
always reference the exact PyPI version listed in pyproject.toml."""
|
|
assert _manifest()["distribution"]["uvx"]["package"] == (
|
|
f"hermes-agent[acp]=={_pyproject_version()}"
|
|
)
|
|
|
|
|
|
def test_icon_svg_is_16x16_current_color():
|
|
root = ET.fromstring(ICON.read_text(encoding="utf-8"))
|
|
|
|
assert root.attrib["viewBox"] == "0 0 16 16"
|
|
assert root.attrib["width"] == "16"
|
|
assert root.attrib["height"] == "16"
|
|
|
|
|
|
def test_icon_svg_has_no_hardcoded_colors_or_gradients():
|
|
text = ICON.read_text(encoding="utf-8")
|
|
|
|
assert "linearGradient" not in text
|
|
assert "radialGradient" not in text
|
|
assert "url(#" not in text
|
|
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", text)
|
|
|
|
root = ET.fromstring(text)
|
|
for element in root.iter():
|
|
for attr in ("fill", "stroke"):
|
|
value = element.attrib.get(attr)
|
|
if value is not None:
|
|
assert value in {"currentColor", "none"}
|