feat: add ACP registry metadata for Zed

This commit is contained in:
mr-r0b0t 2026-05-14 14:43:27 -05:00 committed by Teknium
parent e8b9f5ff9a
commit 4c94396206
17 changed files with 683 additions and 75 deletions

View file

@ -1,8 +1,11 @@
"""ACP auth helpers — detect the currently configured Hermes provider."""
"""ACP auth helpers — detect and advertise Hermes authentication methods."""
from __future__ import annotations
from typing import Optional
from typing import Any, Optional
TERMINAL_SETUP_AUTH_METHOD_ID = "hermes-setup"
def detect_provider() -> Optional[str]:
@ -22,3 +25,44 @@ def detect_provider() -> Optional[str]:
def has_provider() -> bool:
"""Return True if Hermes can resolve any runtime provider credentials."""
return detect_provider() is not None
def build_auth_methods() -> list[Any]:
"""Return registry-compatible ACP auth methods for Hermes.
The official ACP registry validates that agents advertise at least one
usable auth method during the initial handshake. A fresh Zed install may
not have Hermes provider credentials configured yet, so Hermes always
advertises a terminal setup method. When credentials are already present,
it also advertises the resolved provider as the default agent-managed
runtime credential method.
"""
from acp.schema import AuthMethodAgent, TerminalAuthMethod
methods: list[Any] = []
provider = detect_provider()
if provider:
methods.append(
AuthMethodAgent(
id=provider,
name=f"{provider} runtime credentials",
description=(
"Authenticate Hermes using the currently configured "
f"{provider} runtime credentials."
),
)
)
methods.append(
TerminalAuthMethod(
id=TERMINAL_SETUP_AUTH_METHOD_ID,
name="Configure Hermes provider",
description=(
"Open Hermes' interactive model/provider setup in a terminal. "
"Use this when Hermes has not been configured on this machine yet."
),
type="terminal",
args=["--setup"],
)
)
return methods

View file

@ -24,6 +24,7 @@ except ModuleNotFoundError:
# means UTF-8 stdio setup is skipped on Windows; POSIX is unaffected.
pass
import argparse
import asyncio
import logging
import sys
@ -107,8 +108,62 @@ def _load_env() -> None:
)
def main() -> None:
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="hermes-acp",
description="Run Hermes Agent as an ACP stdio server.",
)
parser.add_argument("--version", action="store_true", help="Print Hermes version and exit")
parser.add_argument(
"--check",
action="store_true",
help="Verify ACP dependencies and adapter imports, then exit",
)
parser.add_argument(
"--setup",
action="store_true",
help="Run interactive Hermes provider/model setup for ACP terminal auth",
)
return parser.parse_args(argv)
def _print_version() -> None:
from hermes_cli import __version__ as hermes_version
print(hermes_version)
def _run_check() -> None:
import acp # noqa: F401
from acp_adapter.server import HermesACPAgent # noqa: F401
print("Hermes ACP check OK")
def _run_setup() -> None:
from hermes_cli.main import main as hermes_main
old_argv = sys.argv[:]
try:
sys.argv = [old_argv[0] if old_argv else "hermes", "model"]
hermes_main()
finally:
sys.argv = old_argv
def main(argv: list[str] | None = None) -> None:
"""Entry point: load env, configure logging, run the ACP agent."""
args = _parse_args(argv)
if args.version:
_print_version()
return
if args.check:
_run_check()
return
if args.setup:
_run_setup()
return
_setup_logging()
_load_env()

View file

@ -57,13 +57,7 @@ from acp.schema import (
UserMessageChunk,
)
# AuthMethodAgent was renamed from AuthMethod in agent-client-protocol 0.9.0
try:
from acp.schema import AuthMethodAgent
except ImportError:
from acp.schema import AuthMethod as AuthMethodAgent # type: ignore[attr-defined]
from acp_adapter.auth import detect_provider
from acp_adapter.auth import TERMINAL_SETUP_AUTH_METHOD_ID, build_auth_methods, detect_provider
from acp_adapter.events import (
make_message_cb,
make_step_cb,
@ -744,16 +738,7 @@ class HermesACPAgent(acp.Agent):
resolved_protocol_version = (
protocol_version if isinstance(protocol_version, int) else acp.PROTOCOL_VERSION
)
provider = detect_provider()
auth_methods = None
if provider:
auth_methods = [
AuthMethodAgent(
id=provider,
name=f"{provider} runtime credentials",
description=f"Authenticate Hermes using the currently configured {provider} runtime credentials.",
)
]
auth_methods = build_auth_methods()
client_name = client_info.name if client_info else "unknown"
logger.info(
@ -784,10 +769,18 @@ class HermesACPAgent(acp.Agent):
# server has provider credentials configured — harmless under
# Hermes' threat model (ACP is stdio-only, local-trust), but poor
# API hygiene and confusing if ACP ever grows multi-method auth.
provider = detect_provider()
if not provider:
if not isinstance(method_id, str):
return None
if not isinstance(method_id, str) or method_id.strip().lower() != provider:
normalized_method = method_id.strip().lower()
provider = detect_provider()
if normalized_method == TERMINAL_SETUP_AUTH_METHOD_ID:
# Terminal auth launches Hermes setup/model selection out-of-band.
# Only report success once that flow has produced usable runtime
# credentials for the normal ACP session.
return AuthenticateResponse() if provider else None
if not provider or normalized_method != provider:
return None
return AuthenticateResponse()

View file

@ -1,12 +1,15 @@
{
"schema_version": 1,
"name": "hermes-agent",
"display_name": "Hermes Agent",
"description": "AI agent by Nous Research with 90+ tools, persistent memory, and multi-platform support",
"icon": "icon.svg",
"id": "hermes-agent",
"name": "Hermes Agent",
"version": "0.13.0",
"description": "Self-improving open-source AI agent by Nous Research with ACP editor integration, persistent memory, skills, and rich tool support.",
"repository": "https://github.com/NousResearch/hermes-agent",
"website": "https://hermes-agent.nousresearch.com/docs/user-guide/features/acp",
"authors": ["Nous Research"],
"license": "MIT",
"distribution": {
"type": "command",
"command": "hermes",
"args": ["acp"]
"npx": {
"package": "@nousresearch/hermes-agent-acp@0.13.0"
}
}
}

View file

@ -1,25 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<linearGradient id="gold" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#F5C542;stop-opacity:1" />
<stop offset="100%" style="stop-color:#D4961C;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Staff -->
<rect x="30" y="10" width="4" height="46" rx="2" fill="url(#gold)" />
<!-- Wings (left) -->
<path d="M30 18 C24 14, 14 14, 10 18 C14 16, 22 16, 28 20" fill="#F5C542" opacity="0.9" />
<path d="M30 22 C26 19, 18 19, 14 22 C18 20, 24 20, 28 24" fill="#D4961C" opacity="0.8" />
<!-- Wings (right) -->
<path d="M34 18 C40 14, 50 14, 54 18 C50 16, 42 16, 36 20" fill="#F5C542" opacity="0.9" />
<path d="M34 22 C38 19, 46 19, 50 22 C46 20, 40 20, 36 24" fill="#D4961C" opacity="0.8" />
<!-- Left serpent -->
<path d="M32 48 C22 44, 20 38, 26 34 C20 36, 18 42, 24 46 C18 40, 22 30, 30 28 C24 32, 22 38, 28 42"
fill="none" stroke="#F5C542" stroke-width="2.5" stroke-linecap="round" />
<!-- Right serpent -->
<path d="M32 48 C42 44, 44 38, 38 34 C44 36, 46 42, 40 46 C46 40, 42 30, 34 28 C40 32, 42 38, 36 42"
fill="none" stroke="#D4961C" stroke-width="2.5" stroke-linecap="round" />
<!-- Orb at top -->
<circle cx="32" cy="10" r="4" fill="#F5C542" />
<circle cx="32" cy="10" r="2" fill="#FFF8E1" opacity="0.7" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="none">
<path d="M8 1.5v13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8 3.25c-2.35-1.4-4.7-.95-6.25.35 1.85-.2 3.8.2 5.55 1.55" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 3.25c2.35-1.4 4.7-.95 6.25.35-1.85-.2-3.8.2-5.55 1.55" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13.25c-2.3-1-3.05-2.65-1.35-4.15-2 .8-2.35 2.95-.35 4" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13.25c2.3-1 3.05-2.65 1.35-4.15 2 .8 2.35 2.95.35 4" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="8" cy="1.8" r="1.1" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 882 B

Before After
Before After

View file

@ -0,0 +1,97 @@
# 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

@ -11699,16 +11699,39 @@ Examples:
description="Start Hermes Agent in ACP mode for editor integration (VS Code, Zed, JetBrains)",
)
_add_accept_hooks_flag(acp_parser)
acp_parser.add_argument(
"--version",
action="store_true",
dest="acp_version",
help="Print Hermes ACP version and exit",
)
acp_parser.add_argument(
"--check",
action="store_true",
help="Verify ACP dependencies and adapter imports, then exit",
)
acp_parser.add_argument(
"--setup",
action="store_true",
help="Run interactive Hermes provider/model setup for ACP terminal auth",
)
def cmd_acp(args):
"""Launch Hermes Agent as an ACP server."""
try:
from acp_adapter.entry import main as acp_main
acp_main()
acp_argv = []
if getattr(args, "acp_version", False):
acp_argv.append("--version")
if getattr(args, "check", False):
acp_argv.append("--check")
if getattr(args, "setup", False):
acp_argv.append("--setup")
acp_main(acp_argv)
except ImportError:
print("ACP dependencies not installed.")
print("Install them with: pip install -e '.[acp]'")
print("ACP dependencies not installed.", file=sys.stderr)
print("Install them with: pip install -e '.[acp]'", file=sys.stderr)
sys.exit(1)
acp_parser.set_defaults(func=cmd_acp)

View file

@ -0,0 +1,26 @@
# @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

@ -0,0 +1,66 @@
#!/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

@ -0,0 +1,24 @@
{
"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

@ -0,0 +1,23 @@
'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

@ -1,6 +1,11 @@
"""Tests for acp_adapter.auth — provider detection."""
from acp_adapter.auth import has_provider, detect_provider
from acp_adapter.auth import (
TERMINAL_SETUP_AUTH_METHOD_ID,
build_auth_methods,
has_provider,
detect_provider,
)
class TestHasProvider:
@ -54,3 +59,44 @@ class TestDetectProvider:
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _boom)
assert detect_provider() is None
def test_detect_provider_strips_and_lowercases_provider(self, monkeypatch):
monkeypatch.setattr(
"hermes_cli.runtime_provider.resolve_runtime_provider",
lambda: {"provider": " OpenRouter ", "api_key": " sk-or-test "},
)
assert detect_provider() == "openrouter"
class TestBuildAuthMethods:
def test_build_auth_methods_returns_provider_and_terminal_when_configured(self, monkeypatch):
monkeypatch.setattr("acp_adapter.auth.detect_provider", lambda: "openrouter")
methods = build_auth_methods()
payloads = [method.model_dump(by_alias=True, exclude_none=True) for method in methods]
assert payloads[0]["id"] == "openrouter"
assert payloads[0]["name"] == "openrouter runtime credentials"
assert any(payload["id"] == TERMINAL_SETUP_AUTH_METHOD_ID for payload in payloads)
terminal = next(payload for payload in payloads if payload["id"] == TERMINAL_SETUP_AUTH_METHOD_ID)
assert terminal["type"] == "terminal"
assert terminal["args"] == ["--setup"]
def test_build_auth_methods_returns_terminal_setup_when_unconfigured(self, monkeypatch):
monkeypatch.setattr("acp_adapter.auth.detect_provider", lambda: None)
methods = build_auth_methods()
payloads = [method.model_dump(by_alias=True, exclude_none=True) for method in methods]
assert payloads == [
{
"args": ["--setup"],
"description": (
"Open Hermes' interactive model/provider setup in a terminal. "
"Use this when Hermes has not been configured on this machine yet."
),
"id": TERMINAL_SETUP_AUTH_METHOD_ID,
"name": "Configure Hermes provider",
"type": "terminal",
}
]

View file

@ -15,6 +15,39 @@ def test_main_enables_unstable_protocol(monkeypatch):
monkeypatch.setattr(entry, "_load_env", lambda: None)
monkeypatch.setattr(acp, "run_agent", fake_run_agent)
entry.main()
entry.main([])
assert calls["kwargs"]["use_unstable_protocol"] is True
def test_main_version_prints_without_starting_server(monkeypatch, capsys):
monkeypatch.setattr(entry, "_setup_logging", lambda: (_ for _ in ()).throw(AssertionError("started server")))
entry.main(["--version"])
output = capsys.readouterr().out.strip()
assert output
assert "Starting hermes-agent ACP adapter" not in output
def test_main_check_prints_ok_without_starting_server(monkeypatch, capsys):
monkeypatch.setattr(entry, "_setup_logging", lambda: (_ for _ in ()).throw(AssertionError("started server")))
entry.main(["--check"])
assert capsys.readouterr().out.strip() == "Hermes ACP check OK"
def test_main_setup_runs_model_configuration(monkeypatch):
calls = {}
def fake_hermes_main():
import sys
calls["argv"] = sys.argv[:]
monkeypatch.setattr("hermes_cli.main.main", fake_hermes_main)
entry.main(["--setup"])
assert calls["argv"][1:] == ["model"]

View file

@ -0,0 +1,96 @@
"""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_npx_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 "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_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"}

View file

@ -33,6 +33,7 @@ from acp.schema import (
UsageUpdate,
UserMessageChunk,
)
from acp_adapter.auth import TERMINAL_SETUP_AUTH_METHOD_ID
from acp_adapter.server import HermesACPAgent, HERMES_VERSION
from acp_adapter.session import SessionManager
from hermes_state import SessionDB
@ -92,6 +93,41 @@ class TestInitialize:
assert "list" in session_caps
assert "resume" in session_caps
@pytest.mark.asyncio
async def test_initialize_advertises_provider_and_terminal_auth_methods(self, agent, monkeypatch):
monkeypatch.setattr("acp_adapter.auth.detect_provider", lambda: "openrouter")
monkeypatch.setattr("acp_adapter.server.detect_provider", lambda: "openrouter")
resp = await agent.initialize(protocol_version=1)
payloads = [method.model_dump(by_alias=True, exclude_none=True) for method in resp.auth_methods]
assert payloads[0]["id"] == "openrouter"
assert payloads[0]["name"] == "openrouter runtime credentials"
terminal = next(payload for payload in payloads if payload["id"] == TERMINAL_SETUP_AUTH_METHOD_ID)
assert terminal["type"] == "terminal"
assert terminal["args"] == ["--setup"]
@pytest.mark.asyncio
async def test_initialize_advertises_terminal_setup_auth_when_no_provider(self, agent, monkeypatch):
monkeypatch.setattr("acp_adapter.auth.detect_provider", lambda: None)
monkeypatch.setattr("acp_adapter.server.detect_provider", lambda: None)
resp = await agent.initialize(protocol_version=1)
payloads = [method.model_dump(by_alias=True, exclude_none=True) for method in resp.auth_methods]
assert payloads == [
{
"args": ["--setup"],
"description": (
"Open Hermes' interactive model/provider setup in a terminal. "
"Use this when Hermes has not been configured on this machine yet."
),
"id": TERMINAL_SETUP_AUTH_METHOD_ID,
"name": "Configure Hermes provider",
"type": "terminal",
}
]
# ---------------------------------------------------------------------------
# authenticate
@ -135,6 +171,24 @@ class TestAuthenticate:
resp = await agent.authenticate(method_id="openrouter")
assert resp is None
@pytest.mark.asyncio
async def test_authenticate_accepts_terminal_setup_after_provider_configured(self, agent, monkeypatch):
monkeypatch.setattr(
"acp_adapter.server.detect_provider",
lambda: "openrouter",
)
resp = await agent.authenticate(method_id=TERMINAL_SETUP_AUTH_METHOD_ID)
assert isinstance(resp, AuthenticateResponse)
@pytest.mark.asyncio
async def test_authenticate_rejects_terminal_setup_without_provider(self, agent, monkeypatch):
monkeypatch.setattr(
"acp_adapter.server.detect_provider",
lambda: None,
)
resp = await agent.authenticate(method_id=TERMINAL_SETUP_AUTH_METHOD_ID)
assert resp is None
# ---------------------------------------------------------------------------
# new_session / cancel / load / resume

View file

@ -24,12 +24,15 @@ Key implementation files:
```text
hermes acp / hermes-acp / python -m acp_adapter
-> acp_adapter.entry.main()
-> parse --version / --check / --setup before server startup
-> load ~/.hermes/.env
-> configure stderr logging
-> construct HermesACPAgent
-> 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`.
Stdout is reserved for ACP JSON-RPC transport. Human-readable logs go to stderr.
## Major components
@ -146,7 +149,7 @@ Instead it reuses Hermes' runtime resolver:
- `acp_adapter/auth.py`
- `hermes_cli/runtime_provider.py`
So ACP advertises and uses the currently configured Hermes provider/credentials.
So ACP advertises and uses the currently configured Hermes provider/credentials. It also always advertises a terminal setup auth method (`hermes-setup`, args `--setup`) so first-run registry clients can open Hermes' interactive model/provider configuration before starting a normal ACP session.
## Working directory binding

View file

@ -45,6 +45,14 @@ 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:
```bash
uvx --from 'hermes-agent[acp]==<version>' hermes-acp
```
Make sure `uv` or `uvx` is available on `PATH` before using the registry install path.
## Launching the ACP server
Any of the following starts Hermes in ACP mode:
@ -63,6 +71,13 @@ python -m acp_adapter
Hermes logs to stderr so stdout remains reserved for ACP JSON-RPC traffic.
For non-interactive checks:
```bash
hermes acp --version
hermes acp --check
```
## Editor setup
### VS Code
@ -90,7 +105,19 @@ If you want to define Hermes manually, add it through VS Code settings under `ac
### Zed
Example settings snippet:
Zed v0.221.x and newer installs external agents through the official ACP Registry.
1. Open the Agent Panel.
2. Click **Add Agent**, or run the `zed: acp registry` command.
3. Search for **Hermes Agent**.
4. Install it and start a new Hermes external-agent thread.
Prerequisites:
- Configure Hermes provider credentials first with `hermes model`, or set them in `~/.hermes/.env` / `~/.hermes/config.yaml`.
- Install `uv` so the registry launcher can run `uvx --from 'hermes-agent[acp]==<version>' hermes-acp`.
For local development before the registry entry is available, use a custom agent server in Zed settings:
```json
{
@ -98,9 +125,9 @@ Example settings snippet:
"hermes-agent": {
"type": "custom",
"command": "hermes",
"args": ["acp"],
},
},
"args": ["acp"]
}
}
}
```
@ -114,18 +141,23 @@ Use an ACP-compatible plugin and point it at:
## Registry manifest
The ACP registry manifest lives at:
The source copy of Hermes' official ACP Registry metadata lives at:
```text
acp_registry/agent.json
acp_registry/icon.svg
```
It advertises a command-based agent whose launch command is:
The upstream registry PR copies those files into the top-level `hermes-agent/` directory in `agentclientprotocol/registry`.
The registry entry uses an `npx` distribution:
```text
hermes acp
npx @nousresearch/hermes-agent-acp@<version>
```
The launcher then runs `hermes-acp` from the matching Python package version.
## Configuration and credentials
ACP mode uses the same Hermes configuration as the CLI:
@ -135,7 +167,7 @@ ACP mode uses the same Hermes configuration as the CLI:
- `~/.hermes/skills/`
- `~/.hermes/state.db`
Provider resolution uses Hermes' normal runtime resolver, so ACP inherits the currently configured provider and credentials.
Provider resolution uses Hermes' normal runtime resolver, so ACP inherits the currently configured provider and credentials. Hermes also advertises a terminal auth method (`--setup`) for first-run registry clients; this opens Hermes' interactive model/provider setup.
## Session behavior
@ -171,29 +203,36 @@ On timeout or error, the approval bridge denies the request.
Check:
- the editor is pointed at the correct `acp_registry/` path
- Hermes is installed and on your PATH
- the ACP extra is installed (`pip install -e '.[acp]'`)
- In Zed, open the ACP Registry with `zed: acp registry` and search for **Hermes Agent**.
- 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.
### ACP starts but immediately errors
Try these checks:
```bash
hermes acp --version
hermes acp --check
hermes doctor
hermes status
hermes acp
```
### Missing credentials
ACP mode does not have its own login flow. It uses Hermes' existing provider setup. Configure credentials with:
ACP mode uses Hermes' existing provider setup. Configure credentials with:
```bash
hermes model
```
or by editing `~/.hermes/.env`.
or by editing `~/.hermes/.env`. Registry clients can also trigger Hermes' terminal auth flow, which runs the same interactive provider/model setup.
### Zed registry launcher cannot find uv
Install `uv` from the official uv installation docs, then retry the Hermes Agent thread from Zed.
## See also