mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
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:
parent
5af672c753
commit
c8c6ce1731
11 changed files with 56 additions and 360 deletions
|
|
@ -8,8 +8,9 @@
|
||||||
"authors": ["Nous Research"],
|
"authors": ["Nous Research"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"distribution": {
|
"distribution": {
|
||||||
"npx": {
|
"uvx": {
|
||||||
"package": "@nousresearch/hermes-agent-acp@0.13.0"
|
"package": "hermes-agent[acp]==0.13.0",
|
||||||
|
"args": ["hermes-acp"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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 };
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
|
|
@ -34,12 +34,10 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
VERSION_FILE = REPO_ROOT / "hermes_cli" / "__init__.py"
|
VERSION_FILE = REPO_ROOT / "hermes_cli" / "__init__.py"
|
||||||
PYPROJECT_FILE = REPO_ROOT / "pyproject.toml"
|
PYPROJECT_FILE = REPO_ROOT / "pyproject.toml"
|
||||||
|
|
||||||
# ACP Registry assets that must stay version-locked with pyproject.toml.
|
# ACP Registry manifest must stay version-locked with pyproject.toml.
|
||||||
# tests/acp/test_registry_manifest.py enforces this lockstep, so the release
|
# tests/acp/test_registry_manifest.py enforces this lockstep so the release
|
||||||
# bump touches all four files atomically.
|
# bump touches both files atomically.
|
||||||
ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
|
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
|
# 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:
|
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
|
Skips silently if the manifest is missing — older release branches predate
|
||||||
landed mid-cycle and older release branches may not have them.
|
the ACP Registry assets.
|
||||||
"""
|
"""
|
||||||
if ACP_REGISTRY_MANIFEST.exists():
|
if ACP_REGISTRY_MANIFEST.exists():
|
||||||
manifest = json.loads(ACP_REGISTRY_MANIFEST.read_text(encoding="utf-8"))
|
manifest = json.loads(ACP_REGISTRY_MANIFEST.read_text(encoding="utf-8"))
|
||||||
manifest["version"] = semver
|
manifest["version"] = semver
|
||||||
npx = manifest.get("distribution", {}).get("npx", {})
|
uvx = manifest.get("distribution", {}).get("uvx", {})
|
||||||
if "package" in npx:
|
if "package" in uvx:
|
||||||
npx["package"] = f"@nousresearch/hermes-agent-acp@{semver}"
|
uvx["package"] = f"hermes-agent[acp]=={semver}"
|
||||||
# Preserve trailing newline + 2-space indent the file already uses.
|
# Preserve trailing newline + 2-space indent the file already uses.
|
||||||
ACP_REGISTRY_MANIFEST.write_text(
|
ACP_REGISTRY_MANIFEST.write_text(
|
||||||
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
|
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]:
|
def build_release_artifacts(semver: str) -> list[Path]:
|
||||||
"""Build sdist/wheel artifacts for the current release.
|
"""Build sdist/wheel artifacts for the current release.
|
||||||
|
|
|
||||||
|
|
@ -39,36 +39,30 @@ def test_agent_json_matches_official_registry_required_fields():
|
||||||
assert set(data["distribution"]) <= ALLOWED_DISTRIBUTIONS
|
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()
|
data = _manifest()
|
||||||
|
|
||||||
assert set(data["distribution"]) == {"npx"}
|
assert set(data["distribution"]) == {"uvx"}
|
||||||
assert set(data["distribution"]["npx"]) == {"package"}
|
uvx = data["distribution"]["uvx"]
|
||||||
assert data["distribution"]["npx"]["package"] == (
|
# Schema allows {package, args, env}; we use {package, args}.
|
||||||
f"@nousresearch/hermes-agent-acp@{data['version']}"
|
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 "type" not in data["distribution"]
|
||||||
assert "command" not in data["distribution"]
|
assert "command" not in data["distribution"]
|
||||||
assert "args" not in data["distribution"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_agent_json_version_matches_pyproject():
|
def test_agent_json_version_matches_pyproject():
|
||||||
assert _manifest()["version"] == _pyproject_version()
|
assert _manifest()["version"] == _pyproject_version()
|
||||||
|
|
||||||
|
|
||||||
def test_npm_launcher_versions_match_pyproject_and_manifest():
|
def test_agent_json_pins_uvx_package_to_pyproject_version():
|
||||||
version = _pyproject_version()
|
"""The registry CI rejects ``@latest`` and floating pins; the manifest must
|
||||||
package = json.loads(
|
always reference the exact PyPI version listed in pyproject.toml."""
|
||||||
(ROOT / "packages" / "hermes-agent-acp" / "package.json").read_text(encoding="utf-8")
|
assert _manifest()["distribution"]["uvx"]["package"] == (
|
||||||
)
|
f"hermes-agent[acp]=={_pyproject_version()}"
|
||||||
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}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
"""Tests for the ACP Registry version-lockstep bump in scripts/release.py.
|
"""Tests for the ACP Registry version-lockstep bump in scripts/release.py.
|
||||||
|
|
||||||
The official ACP Registry manifest, the @nousresearch/hermes-agent-acp npm
|
The official ACP Registry manifest must match ``pyproject.toml`` exactly —
|
||||||
package, and the npm launcher's HERMES_AGENT_VERSION constant must all match
|
``tests/acp/test_registry_manifest.py`` enforces this at lint time, and the
|
||||||
``pyproject.toml`` exactly — ``tests/acp/test_registry_manifest.py`` enforces
|
upstream registry CI rejects ``@latest`` / floating pins. The release script
|
||||||
this at lint time. The release script is the single place that bumps them in
|
is the single place that bumps the manifest in lockstep with pyproject; if
|
||||||
lockstep with pyproject; if that bump ever silently breaks, weekly releases
|
that bump ever silently breaks, weekly releases fail the manifest test
|
||||||
fail the manifest test until someone hand-edits four files.
|
until someone hand-edits the JSON.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -25,26 +25,14 @@ def _load_release_module(monkeypatch, tmp_root: Path):
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
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, "REPO_ROOT", tmp_root)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
module, "ACP_REGISTRY_MANIFEST", tmp_root / "acp_registry" / "agent.json"
|
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
|
return module
|
||||||
|
|
||||||
|
|
||||||
def _write_fixture(root: Path, version: str) -> None:
|
def _write_manifest(root: Path, version: str) -> None:
|
||||||
"""Write the three ACP-registry files we expect release.py to bump."""
|
|
||||||
manifest_dir = root / "acp_registry"
|
manifest_dir = root / "acp_registry"
|
||||||
manifest_dir.mkdir(parents=True)
|
manifest_dir.mkdir(parents=True)
|
||||||
(manifest_dir / "agent.json").write_text(
|
(manifest_dir / "agent.json").write_text(
|
||||||
|
|
@ -55,7 +43,10 @@ def _write_fixture(root: Path, version: str) -> None:
|
||||||
"version": version,
|
"version": version,
|
||||||
"description": "test",
|
"description": "test",
|
||||||
"distribution": {
|
"distribution": {
|
||||||
"npx": {"package": f"@nousresearch/hermes-agent-acp@{version}"}
|
"uvx": {
|
||||||
|
"package": f"hermes-agent[acp]=={version}",
|
||||||
|
"args": ["hermes-acp"],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
|
|
@ -64,29 +55,9 @@ def _write_fixture(root: Path, version: str) -> None:
|
||||||
encoding="utf-8",
|
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_manifest_and_pin(monkeypatch, tmp_path):
|
||||||
def test_update_acp_registry_versions_bumps_all_three_files(monkeypatch, tmp_path):
|
_write_manifest(tmp_path, "0.13.0")
|
||||||
_write_fixture(tmp_path, "0.13.0")
|
|
||||||
module = _load_release_module(monkeypatch, tmp_path)
|
module = _load_release_module(monkeypatch, tmp_path)
|
||||||
|
|
||||||
module._update_acp_registry_versions("0.14.0")
|
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")
|
(tmp_path / "acp_registry" / "agent.json").read_text(encoding="utf-8")
|
||||||
)
|
)
|
||||||
assert manifest["version"] == "0.14.0"
|
assert manifest["version"] == "0.14.0"
|
||||||
assert (
|
assert manifest["distribution"]["uvx"]["package"] == "hermes-agent[acp]==0.14.0"
|
||||||
manifest["distribution"]["npx"]["package"]
|
# args stay untouched so we don't accidentally rewrite them.
|
||||||
== "@nousresearch/hermes-agent-acp@0.14.0"
|
assert manifest["distribution"]["uvx"]["args"] == ["hermes-acp"]
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
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
|
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)
|
module = _load_release_module(monkeypatch, tmp_path)
|
||||||
|
|
||||||
# No fixture written; function should not raise.
|
# No fixture written; function should not raise.
|
||||||
module._update_acp_registry_versions("0.14.0")
|
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
|
monkeypatch, tmp_path
|
||||||
):
|
):
|
||||||
"""End-to-end: update_version_files() is the function release.py actually
|
"""End-to-end: update_version_files() is the function release.py actually
|
||||||
calls, so it must drive the ACP bump too."""
|
calls, so it must drive the manifest bump too."""
|
||||||
_write_fixture(tmp_path, "0.13.0")
|
_write_manifest(tmp_path, "0.13.0")
|
||||||
(tmp_path / "pyproject.toml").write_text(
|
(tmp_path / "pyproject.toml").write_text(
|
||||||
'[project]\nname = "hermes-agent"\nversion = "0.13.0"\n', encoding="utf-8"
|
'[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")
|
(tmp_path / "acp_registry" / "agent.json").read_text(encoding="utf-8")
|
||||||
)
|
)
|
||||||
assert manifest["version"] == "0.14.0"
|
assert manifest["version"] == "0.14.0"
|
||||||
assert (
|
assert manifest["distribution"]["uvx"]["package"] == "hermes-agent[acp]==0.14.0"
|
||||||
manifest["distribution"]["npx"]["package"]
|
|
||||||
== "@nousresearch/hermes-agent-acp@0.14.0"
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ hermes acp / hermes-acp / python -m acp_adapter
|
||||||
-> acp.run_agent(agent, use_unstable_protocol=True)
|
-> 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.
|
Stdout is reserved for ACP JSON-RPC transport. Human-readable logs go to stderr.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,13 @@ This installs the `agent-client-protocol` dependency and enables:
|
||||||
- `hermes-acp`
|
- `hermes-acp`
|
||||||
- `python -m acp_adapter`
|
- `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
|
```bash
|
||||||
uvx --from 'hermes-agent[acp]==<version>' hermes-acp
|
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
|
## 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 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
|
```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
|
## Configuration and credentials
|
||||||
|
|
||||||
|
|
@ -207,7 +207,7 @@ Check:
|
||||||
- For manual/local development, verify the custom `agent_servers` command points to `hermes acp`.
|
- For manual/local development, verify the custom `agent_servers` command points to `hermes acp`.
|
||||||
- Hermes is installed and on your PATH.
|
- Hermes is installed and on your PATH.
|
||||||
- The ACP extra is installed (`pip install -e '.[acp]'`).
|
- 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
|
### ACP starts but immediately errors
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue