feat(acp-registry): switch to uvx distribution, drop npm launcher

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.
This commit is contained in:
teknium1 2026-05-14 22:05:39 -07:00 committed by Teknium
parent 5af672c753
commit c8c6ce1731
11 changed files with 56 additions and 360 deletions

View file

@ -8,8 +8,9 @@
"authors": ["Nous Research"],
"license": "MIT",
"distribution": {
"npx": {
"package": "@nousresearch/hermes-agent-acp@0.13.0"
"uvx": {
"package": "hermes-agent[acp]==0.13.0",
"args": ["hermes-acp"]
}
}
}

View file

@ -1,97 +0,0 @@
# Hermes Agent ACP Registry + Zed Integration Implementation Plan
> For Hermes: Use subagent-driven-development skill to implement this plan task-by-task.
Goal: Make Hermes Agent installable from Zed's official ACP Registry, so users can add Hermes from Zed's agent panel without manual custom `agent_servers` settings.
Architecture: Use the official `agentclientprotocol/registry` flow instead of the deprecated Zed Agent Server Extension path. Ship a registry-compatible launcher distribution, advertise valid ACP auth methods during every handshake, validate against official registry schema and auth CI, then submit a registry PR for `hermes-agent`.
Tech Stack: Hermes Agent Python package, ACP adapter (`hermes acp` / `hermes-acp`), npm launcher package, official ACP Registry JSON schema, Zed external agent UI.
---
## Compliance constraints
- Zed v0.221.x+ prefers the ACP Registry for external agents; do not use Zed Agent Server Extensions for distribution.
- Registry repo layout is top-level `hermes-agent/agent.json` and `hermes-agent/icon.svg`, not `agents/hermes-agent/`.
- Registry metadata must use the official schema: `id`, `name`, `version`, `description`, `distribution`, optional `repository`, `website`, `authors`, `license`.
- Distribution must be exactly one supported type unless intentionally adding another: `binary`, `npx`, or `uvx`.
- Hermes must advertise at least one valid `authMethods` entry on a clean first-run handshake. No-provider/no-auth is not compliant.
- Terminal Auth must be explicit and deterministic: `id: hermes-setup`, `type: terminal`, `args: ["--setup"]`.
- `icon.svg` must be 16x16, square, monochrome, and use only `currentColor` / `none` for fill/stroke; no gradients, hardcoded colors, or `url(#...)` paints.
- ACP server mode must reserve stdout for JSON-RPC only. Diagnostics/logs go to stderr. `--version`, `--check`, and `--setup` are not server mode and may print normally.
- Published npm package must exist and be runnable before the upstream registry PR references it.
---
## Tasks
1. Verify/implement ACP auth methods.
- Always return terminal setup auth from `initialize()`.
- Return configured provider auth in addition when provider credentials are resolvable.
- Add tests for provider auth, terminal fallback auth, and authenticate behavior before/after provider setup.
2. Add non-interactive ACP commands.
- `hermes acp --version`
- `hermes acp --check`
- `hermes acp --setup`
- Same behavior through `hermes-acp`.
3. Build npm launcher package.
- Package: `@nousresearch/hermes-agent-acp@<version>`.
- Command: `uvx --from 'hermes-agent[acp]==<version>' hermes-acp ...args`.
- Fallback: `uv tool run --from ...` when only `uv` exists.
- Forward all args, including `--setup`, `--version`, and `--check`.
- Preserve stdio in server mode.
- Print actionable stderr error when `uv`/`uvx` is missing.
4. Replace local registry metadata.
- Convert `acp_registry/agent.json` from old command-style local format to official registry schema.
- Replace `acp_registry/icon.svg` with compliant 16x16 currentColor icon.
- Add tests rejecting old fields (`schema_version`, `display_name`, `distribution.type`, `distribution.command`) and unknown distribution keys.
5. Update docs.
- Zed docs show official ACP Registry install first: Add Agent / `zed: acp registry` -> search Hermes Agent -> install.
- Manual `agent_servers` JSON remains only as local-development fallback.
- Docs include `uv` prerequisite and `hermes acp --check` troubleshooting.
- Developer internals mention npm launcher and terminal setup auth.
6. Validate locally.
- `python -m pytest tests/acp/test_auth.py tests/acp/test_server.py tests/acp/test_entry.py tests/acp/test_registry_manifest.py -q`
- `(cd packages/hermes-agent-acp && npm test)`
- `(cd packages/hermes-agent-acp && npm pack --dry-run)`
- `hermes acp --version`
- `hermes acp --check`
7. Validate against official registry tooling before PR.
- In a clone/fork of `agentclientprotocol/registry`, copy files into top-level `hermes-agent/`.
- Run official dry-run build, e.g. `uv run --with jsonschema .github/workflows/build_registry.py --dry-run`.
- Run official auth check if available, e.g. `.github/workflows/scripts/run-registry-docker.sh python3 .github/workflows/verify_agents.py --auth-check`.
- Fix any schema/auth issues before submitting.
8. Publish and submit.
- Publish `@nousresearch/hermes-agent-acp@<version>`.
- Verify published package:
- `npx @nousresearch/hermes-agent-acp@<version> --version`
- `npx @nousresearch/hermes-agent-acp@<version> --check`
- ACP initialize/authMethods smoke test through the published package.
- Open PR to `agentclientprotocol/registry` adding `hermes-agent/agent.json` and `hermes-agent/icon.svg`.
9. End-to-end Zed verification.
- Install Hermes Agent through Zed's ACP Registry.
- Start a Hermes thread.
- Verify workspace cwd, file tools, terminal tools, tool rendering, and approval prompts.
---
## Acceptance criteria
- Hermes appears in Zed's official ACP Registry UI.
- Install starts Hermes without custom Zed settings.
- Registry CI passes schema and auth validation.
- ACP stdout remains JSON-RPC only; all logs go to stderr.
- `authMethods` are present and valid on clean first run.
- Terminal Auth can launch Hermes provider/model setup with `--setup`.
- Zed workspace cwd is honored by Hermes file and terminal tools.
- Docs describe registry install first and manual custom config second.
- Package/release automation prevents registry entries from pointing at unpublished versions.

View file

@ -1,26 +0,0 @@
# @nousresearch/hermes-agent-acp
ACP launcher for Hermes Agent.
This package is intended for clients such as Zed that install agents through the official ACP Registry. It launches the Python Hermes ACP server with:
```bash
uvx --from 'hermes-agent[acp]==0.13.0' hermes-acp
```
## Requirements
- Node.js 18+
- `uv` or `uvx` on PATH
- Hermes provider credentials configured with `hermes model`, or through Hermes' normal `~/.hermes/.env` / `~/.hermes/config.yaml` setup
## Commands
```bash
npx @nousresearch/hermes-agent-acp@0.13.0 --version
npx @nousresearch/hermes-agent-acp@0.13.0 --check
npx @nousresearch/hermes-agent-acp@0.13.0 --setup
npx @nousresearch/hermes-agent-acp@0.13.0
```
Normal no-argument mode reserves stdout for ACP JSON-RPC traffic. Diagnostics are emitted on stderr by Hermes.

View file

@ -1,66 +0,0 @@
#!/usr/bin/env node
'use strict';
const { spawn, spawnSync } = require('node:child_process');
const HERMES_AGENT_VERSION = '0.13.0';
const HERMES_SPEC = `hermes-agent[acp]==${HERMES_AGENT_VERSION}`;
function commandExists(command) {
const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
return !result.error && result.status === 0;
}
function buildCommand(argv, exists = commandExists) {
if (exists('uvx')) {
return {
command: 'uvx',
args: ['--from', HERMES_SPEC, 'hermes-acp', ...argv],
};
}
if (exists('uv')) {
return {
command: 'uv',
args: ['tool', 'run', '--from', HERMES_SPEC, 'hermes-acp', ...argv],
};
}
return null;
}
function main() {
const argv = process.argv.slice(2);
const command = buildCommand(argv);
if (!command) {
console.error('Hermes Agent ACP requires uv or uvx to launch the Python package.');
console.error('Install uv from https://docs.astral.sh/uv/getting-started/installation/');
console.error('Then retry this agent from Zed.');
process.exit(127);
}
const child = spawn(command.command, command.args, {
stdio: 'inherit',
env: process.env,
});
child.on('error', (error) => {
console.error(`Failed to start Hermes Agent ACP: ${error.message}`);
process.exit(1);
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
}
if (require.main === module) {
main();
}
module.exports = { buildCommand, HERMES_AGENT_VERSION, HERMES_SPEC };

View file

@ -1,24 +0,0 @@
{
"name": "@nousresearch/hermes-agent-acp",
"version": "0.13.0",
"description": "ACP launcher for Hermes Agent",
"bin": {
"hermes-agent-acp": "bin/hermes-agent-acp.js"
},
"files": [
"bin/",
"README.md"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/NousResearch/hermes-agent.git",
"directory": "packages/hermes-agent-acp"
},
"engines": {
"node": ">=18"
},
"scripts": {
"test": "node --test"
}
}

View file

@ -1,23 +0,0 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { buildCommand, HERMES_SPEC } = require('../bin/hermes-agent-acp.js');
test('uses uvx when available and forwards args', () => {
const command = buildCommand(['--version'], (name) => name === 'uvx');
assert.equal(command.command, 'uvx');
assert.deepEqual(command.args, ['--from', HERMES_SPEC, 'hermes-acp', '--version']);
});
test('falls back to uv tool run and forwards setup args', () => {
const command = buildCommand(['--setup'], (name) => name === 'uv');
assert.equal(command.command, 'uv');
assert.deepEqual(command.args, ['tool', 'run', '--from', HERMES_SPEC, 'hermes-acp', '--setup']);
});
test('returns null when neither uvx nor uv is available', () => {
assert.equal(buildCommand([], () => false), null);
});

View file

@ -34,12 +34,10 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
VERSION_FILE = REPO_ROOT / "hermes_cli" / "__init__.py"
PYPROJECT_FILE = REPO_ROOT / "pyproject.toml"
# ACP Registry assets that must stay version-locked with pyproject.toml.
# tests/acp/test_registry_manifest.py enforces this lockstep, so the release
# bump touches all four files atomically.
# ACP Registry manifest must stay version-locked with pyproject.toml.
# tests/acp/test_registry_manifest.py enforces this lockstep so the release
# bump touches both files atomically.
ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
ACP_NPM_PACKAGE_JSON = REPO_ROOT / "packages" / "hermes-agent-acp" / "package.json"
ACP_NPM_LAUNCHER = REPO_ROOT / "packages" / "hermes-agent-acp" / "bin" / "hermes-agent-acp.js"
# ──────────────────────────────────────────────────────────────────────
# Git email → GitHub username mapping
@ -1168,38 +1166,23 @@ def update_version_files(semver: str, calver_date: str):
def _update_acp_registry_versions(semver: str) -> None:
"""Bump the ACP Registry manifest, npm package, and launcher in lockstep.
"""Bump the ACP Registry manifest's version + uvx package pin in lockstep
with pyproject.
Skips silently if any of the files are missing the ACP Registry assets
landed mid-cycle and older release branches may not have them.
Skips silently if the manifest is missing older release branches predate
the ACP Registry assets.
"""
if ACP_REGISTRY_MANIFEST.exists():
manifest = json.loads(ACP_REGISTRY_MANIFEST.read_text(encoding="utf-8"))
manifest["version"] = semver
npx = manifest.get("distribution", {}).get("npx", {})
if "package" in npx:
npx["package"] = f"@nousresearch/hermes-agent-acp@{semver}"
uvx = manifest.get("distribution", {}).get("uvx", {})
if "package" in uvx:
uvx["package"] = f"hermes-agent[acp]=={semver}"
# Preserve trailing newline + 2-space indent the file already uses.
ACP_REGISTRY_MANIFEST.write_text(
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
if ACP_NPM_PACKAGE_JSON.exists():
package = json.loads(ACP_NPM_PACKAGE_JSON.read_text(encoding="utf-8"))
package["version"] = semver
ACP_NPM_PACKAGE_JSON.write_text(
json.dumps(package, indent=2) + "\n", encoding="utf-8"
)
if ACP_NPM_LAUNCHER.exists():
launcher = ACP_NPM_LAUNCHER.read_text(encoding="utf-8")
launcher = re.sub(
r"const HERMES_AGENT_VERSION\s*=\s*'[^']+';",
f"const HERMES_AGENT_VERSION = '{semver}';",
launcher,
)
ACP_NPM_LAUNCHER.write_text(launcher, encoding="utf-8")
def build_release_artifacts(semver: str) -> list[Path]:
"""Build sdist/wheel artifacts for the current release.

View file

@ -39,36 +39,30 @@ def test_agent_json_matches_official_registry_required_fields():
assert set(data["distribution"]) <= ALLOWED_DISTRIBUTIONS
def test_agent_json_uses_npx_distribution_without_local_command_fields():
def test_agent_json_uses_uvx_distribution_without_local_command_fields():
data = _manifest()
assert set(data["distribution"]) == {"npx"}
assert set(data["distribution"]["npx"]) == {"package"}
assert data["distribution"]["npx"]["package"] == (
f"@nousresearch/hermes-agent-acp@{data['version']}"
)
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"]
assert "args" not in data["distribution"]
def test_agent_json_version_matches_pyproject():
assert _manifest()["version"] == _pyproject_version()
def test_npm_launcher_versions_match_pyproject_and_manifest():
version = _pyproject_version()
package = json.loads(
(ROOT / "packages" / "hermes-agent-acp" / "package.json").read_text(encoding="utf-8")
)
launcher = (ROOT / "packages" / "hermes-agent-acp" / "bin" / "hermes-agent-acp.js").read_text(
encoding="utf-8"
)
assert package["version"] == version
assert f"const HERMES_AGENT_VERSION = '{version}';" in launcher
assert _manifest()["distribution"]["npx"]["package"] == (
f"@nousresearch/hermes-agent-acp@{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()}"
)

View file

@ -1,11 +1,11 @@
"""Tests for the ACP Registry version-lockstep bump in scripts/release.py.
The official ACP Registry manifest, the @nousresearch/hermes-agent-acp npm
package, and the npm launcher's HERMES_AGENT_VERSION constant must all match
``pyproject.toml`` exactly ``tests/acp/test_registry_manifest.py`` enforces
this at lint time. The release script is the single place that bumps them in
lockstep with pyproject; if that bump ever silently breaks, weekly releases
fail the manifest test until someone hand-edits four files.
The official ACP Registry manifest must match ``pyproject.toml`` exactly
``tests/acp/test_registry_manifest.py`` enforces this at lint time, and the
upstream registry CI rejects ``@latest`` / floating pins. The release script
is the single place that bumps the manifest in lockstep with pyproject; if
that bump ever silently breaks, weekly releases fail the manifest test
until someone hand-edits the JSON.
"""
from __future__ import annotations
@ -25,26 +25,14 @@ def _load_release_module(monkeypatch, tmp_root: Path):
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Repoint every REPO_ROOT-derived path at our temp tree.
monkeypatch.setattr(module, "REPO_ROOT", tmp_root)
monkeypatch.setattr(
module, "ACP_REGISTRY_MANIFEST", tmp_root / "acp_registry" / "agent.json"
)
monkeypatch.setattr(
module,
"ACP_NPM_PACKAGE_JSON",
tmp_root / "packages" / "hermes-agent-acp" / "package.json",
)
monkeypatch.setattr(
module,
"ACP_NPM_LAUNCHER",
tmp_root / "packages" / "hermes-agent-acp" / "bin" / "hermes-agent-acp.js",
)
return module
def _write_fixture(root: Path, version: str) -> None:
"""Write the three ACP-registry files we expect release.py to bump."""
def _write_manifest(root: Path, version: str) -> None:
manifest_dir = root / "acp_registry"
manifest_dir.mkdir(parents=True)
(manifest_dir / "agent.json").write_text(
@ -55,7 +43,10 @@ def _write_fixture(root: Path, version: str) -> None:
"version": version,
"description": "test",
"distribution": {
"npx": {"package": f"@nousresearch/hermes-agent-acp@{version}"}
"uvx": {
"package": f"hermes-agent[acp]=={version}",
"args": ["hermes-acp"],
}
},
},
indent=2,
@ -64,29 +55,9 @@ def _write_fixture(root: Path, version: str) -> None:
encoding="utf-8",
)
package_dir = root / "packages" / "hermes-agent-acp"
(package_dir / "bin").mkdir(parents=True)
(package_dir / "package.json").write_text(
json.dumps(
{
"name": "@nousresearch/hermes-agent-acp",
"version": version,
"bin": {"hermes-agent-acp": "bin/hermes-agent-acp.js"},
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
(package_dir / "bin" / "hermes-agent-acp.js").write_text(
f"const HERMES_AGENT_VERSION = '{version}';\n"
f"const HERMES_SPEC = `hermes-agent[acp]==${{HERMES_AGENT_VERSION}}`;\n",
encoding="utf-8",
)
def test_update_acp_registry_versions_bumps_all_three_files(monkeypatch, tmp_path):
_write_fixture(tmp_path, "0.13.0")
def test_update_acp_registry_versions_bumps_manifest_and_pin(monkeypatch, tmp_path):
_write_manifest(tmp_path, "0.13.0")
module = _load_release_module(monkeypatch, tmp_path)
module._update_acp_registry_versions("0.14.0")
@ -95,41 +66,27 @@ def test_update_acp_registry_versions_bumps_all_three_files(monkeypatch, tmp_pat
(tmp_path / "acp_registry" / "agent.json").read_text(encoding="utf-8")
)
assert manifest["version"] == "0.14.0"
assert (
manifest["distribution"]["npx"]["package"]
== "@nousresearch/hermes-agent-acp@0.14.0"
)
package = json.loads(
(
tmp_path / "packages" / "hermes-agent-acp" / "package.json"
).read_text(encoding="utf-8")
)
assert package["version"] == "0.14.0"
launcher = (
tmp_path / "packages" / "hermes-agent-acp" / "bin" / "hermes-agent-acp.js"
).read_text(encoding="utf-8")
assert "const HERMES_AGENT_VERSION = '0.14.0';" in launcher
assert "0.13.0" not in launcher
assert manifest["distribution"]["uvx"]["package"] == "hermes-agent[acp]==0.14.0"
# args stay untouched so we don't accidentally rewrite them.
assert manifest["distribution"]["uvx"]["args"] == ["hermes-acp"]
def test_update_acp_registry_versions_is_silent_when_files_missing(
def test_update_acp_registry_versions_is_silent_when_manifest_missing(
monkeypatch, tmp_path
):
"""Older release branches predate the ACP Registry assets — must no-op."""
"""Older release branches predate the ACP Registry asset — must no-op."""
module = _load_release_module(monkeypatch, tmp_path)
# No fixture written; function should not raise.
module._update_acp_registry_versions("0.14.0")
def test_update_version_files_bumps_acp_assets_alongside_pyproject(
def test_update_version_files_bumps_manifest_alongside_pyproject(
monkeypatch, tmp_path
):
"""End-to-end: update_version_files() is the function release.py actually
calls, so it must drive the ACP bump too."""
_write_fixture(tmp_path, "0.13.0")
calls, so it must drive the manifest bump too."""
_write_manifest(tmp_path, "0.13.0")
(tmp_path / "pyproject.toml").write_text(
'[project]\nname = "hermes-agent"\nversion = "0.13.0"\n', encoding="utf-8"
)
@ -153,7 +110,4 @@ def test_update_version_files_bumps_acp_assets_alongside_pyproject(
(tmp_path / "acp_registry" / "agent.json").read_text(encoding="utf-8")
)
assert manifest["version"] == "0.14.0"
assert (
manifest["distribution"]["npx"]["package"]
== "@nousresearch/hermes-agent-acp@0.14.0"
)
assert manifest["distribution"]["uvx"]["package"] == "hermes-agent[acp]==0.14.0"

View file

@ -31,7 +31,7 @@ hermes acp / hermes-acp / python -m acp_adapter
-> acp.run_agent(agent, use_unstable_protocol=True)
```
The Zed ACP Registry path launches the same adapter through `npx @nousresearch/hermes-agent-acp@<version>`, which delegates to `uvx --from 'hermes-agent[acp]==<version>' hermes-acp`.
The Zed ACP Registry path launches the same adapter through `uvx --from 'hermes-agent[acp]==<version>' hermes-acp`, pointed at the `hermes-agent` PyPI release.
Stdout is reserved for ACP JSON-RPC transport. Human-readable logs go to stderr.

View file

@ -45,13 +45,13 @@ This installs the `agent-client-protocol` dependency and enables:
- `hermes-acp`
- `python -m acp_adapter`
For Zed registry installs, Zed launches Hermes through the official ACP Registry entry. That entry uses the npm launcher package `@nousresearch/hermes-agent-acp`, which runs:
For Zed registry installs, Zed launches Hermes through the official ACP Registry entry. That entry uses a `uvx` distribution that runs:
```bash
uvx --from 'hermes-agent[acp]==<version>' hermes-acp
```
Make sure `uv` or `uvx` is available on `PATH` before using the registry install path.
Make sure `uv` is available on `PATH` before using the registry install path.
## Launching the ACP server
@ -150,13 +150,13 @@ acp_registry/icon.svg
The upstream registry PR copies those files into the top-level `hermes-agent/` directory in `agentclientprotocol/registry`.
The registry entry uses an `npx` distribution:
The registry entry uses a `uvx` distribution that points directly at the `hermes-agent` PyPI release:
```text
npx @nousresearch/hermes-agent-acp@<version>
uvx --from 'hermes-agent[acp]==<version>' hermes-acp
```
The launcher then runs `hermes-acp` from the matching Python package version.
The registry CI verifies that the pinned version exists on PyPI, so the manifest's `version` and uvx `package` pin must always match `pyproject.toml`. `scripts/release.py` keeps them in lockstep automatically.
## Configuration and credentials
@ -207,7 +207,7 @@ Check:
- For manual/local development, verify the custom `agent_servers` command points to `hermes acp`.
- Hermes is installed and on your PATH.
- The ACP extra is installed (`pip install -e '.[acp]'`).
- `uv` or `uvx` is installed if launching from the official Zed registry entry.
- `uv` is installed if launching from the official Zed registry entry.
### ACP starts but immediately errors