Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/registry_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,9 @@ def extract_npm_package_version(package_spec: str) -> str | None:


def extract_pypi_package_name(package_spec: str) -> str:
"""Extract PyPI package name from spec like package==version."""
return re.split(r"[<>=!@]", package_spec)[0]
"""Extract PyPI package name from spec like package[extra]==version."""
package_name = re.split(r"[<>=!@]", package_spec)[0]
return package_name.split("[", 1)[0].strip()


def normalize_version(version: str) -> str:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/tests/test_registry_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class TestExtractPypiPackageName:
def test_with_double_equals(self):
assert extract_pypi_package_name("some-package==1.2.3") == "some-package"

def test_with_extra_and_double_equals(self):
assert extract_pypi_package_name("some-package[acp]==1.2.3") == "some-package"

def test_with_at_version(self):
assert extract_pypi_package_name("some-package@1.2.3") == "some-package"

Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/tests/test_verify_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path

from verify_agents import (
build_agent_command,
build_agent_process_env,
build_installed_npx_command,
ensure_executable,
Expand Down Expand Up @@ -240,6 +241,78 @@ def test_build_installed_npx_command_prefers_home_shim(tmp_path: Path):
assert command == [str(home_bin / "junie"), "--acp=true"]


def test_build_installed_npx_command_uses_explicit_package_cmd(tmp_path: Path):
auth_home = tmp_path / "auth-home"
home_bin = auth_home / ".local" / "bin"
home_bin.mkdir(parents=True)
(home_bin / "example-acp").write_text("#!/bin/sh\n")

command = build_installed_npx_command(
"@example/agent@1.2.3",
["--acp"],
tmp_path,
auth_home,
"example-acp",
)

assert command == [str(home_bin / "example-acp"), "--acp"]


def test_build_agent_command_uses_uvx_package_cmd(tmp_path: Path):
agent = {
"id": "hermes-agent",
"distribution": {
"uvx": {
"package": "hermes-agent[acp]==0.14.0",
"cmd": "hermes-acp",
"args": ["--check"],
}
},
}

command, cwd, env = build_agent_command(agent, "uvx", tmp_path)

assert command == [
"uvx",
"--cache-dir",
str(tmp_path / "uv-cache"),
"--from",
"hermes-agent[acp]==0.14.0",
"hermes-acp",
"--check",
]
assert cwd == tmp_path
assert env == {}


def test_build_agent_command_uses_npx_package_cmd(tmp_path: Path):
agent = {
"id": "example-agent",
"distribution": {
"npx": {
"package": "@example/agent@1.2.3",
"cmd": "example-acp",
"args": ["--acp"],
}
},
}

command, cwd, env = build_agent_command(agent, "npx", tmp_path)

assert command == [
"npx",
"--prefix",
str(tmp_path),
"--yes",
"--package",
"@example/agent@1.2.3",
"example-acp",
"--acp",
]
assert cwd == tmp_path
assert env == {}


def test_should_retry_npx_auth_with_install_on_shim_error():
assert should_retry_npx_auth_with_install(
"Timeout after 120s waiting for initialize response",
Expand Down
48 changes: 40 additions & 8 deletions .github/workflows/verify_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,10 @@ def build_installed_npx_command(
args: list[str],
sandbox: Path,
auth_home: Path,
package_cmd: str | None = None,
) -> list[str] | None:
"""Resolve the best command for an npm package installed into the sandbox."""
bin_name = npm_package_bin_name(package_spec, sandbox)
bin_name = package_cmd or npm_package_bin_name(package_spec, sandbox)
candidates = [
auth_home / ".local" / "bin" / bin_name,
sandbox / "node_modules" / ".bin" / bin_name,
Expand Down Expand Up @@ -539,12 +540,17 @@ def verify_npx(agent: dict, sandbox: Path, timeout: int, verbose: bool) -> Resul

npx_dist = agent["distribution"].get("npx", {})
package = npx_dist.get("package", "")
package_cmd = npx_dist.get("cmd", "")
args = npx_dist.get("args", [])
env = npx_dist.get("env", {})

print(f" → Running: npx {package} {' '.join(args)}")
if package_cmd:
print(f" → Running: npx --package {package} {package_cmd} {' '.join(args)}")
cmd = ["npx", "--prefix", str(sandbox), "--yes", "--package", package, package_cmd] + args
else:
print(f" → Running: npx {package} {' '.join(args)}")
cmd = ["npx", "--prefix", str(sandbox), "--yes", package] + args

cmd = ["npx", "--prefix", str(sandbox), "--yes", package] + args
exit_code, stdout, stderr = run_process(cmd, sandbox, env, timeout)

if exit_code is None:
Expand All @@ -569,15 +575,22 @@ def verify_uvx(agent: dict, sandbox: Path, timeout: int, verbose: bool) -> Resul

uvx_dist = agent["distribution"].get("uvx", {})
package = uvx_dist.get("package", "")
package_cmd = uvx_dist.get("cmd", "")
args = uvx_dist.get("args", [])
env = uvx_dist.get("env", {})

print(f" → Running: uvx {package} {' '.join(args)}")
if package_cmd:
print(f" → Running: uvx --from {package} {package_cmd} {' '.join(args)}")
else:
print(f" → Running: uvx {package} {' '.join(args)}")

cache_dir = sandbox / "uv-cache"
cache_dir.mkdir(exist_ok=True)

cmd = ["uvx", "--cache-dir", str(cache_dir), package] + args
if package_cmd:
cmd = ["uvx", "--cache-dir", str(cache_dir), "--from", package, package_cmd] + args
else:
cmd = ["uvx", "--cache-dir", str(cache_dir), package] + args
exit_code, stdout, stderr = run_process(cmd, sandbox, env, timeout)

if exit_code is None:
Expand Down Expand Up @@ -653,18 +666,34 @@ def build_agent_command(
if dist_type == "npx":
npx_dist = distribution.get("npx", {})
package = npx_dist.get("package", "")
package_cmd = npx_dist.get("cmd", "")
args = npx_dist.get("args", [])
env = npx_dist.get("env", {})
cmd = ["npx", "--prefix", str(sandbox), "--yes", package] + args
if package_cmd:
cmd = [
"npx",
"--prefix",
str(sandbox),
"--yes",
"--package",
package,
package_cmd,
] + args
else:
cmd = ["npx", "--prefix", str(sandbox), "--yes", package] + args
cwd = sandbox
elif dist_type == "uvx":
uvx_dist = distribution.get("uvx", {})
package = uvx_dist.get("package", "")
package_cmd = uvx_dist.get("cmd", "")
args = uvx_dist.get("args", [])
env = uvx_dist.get("env", {})
cache_dir = sandbox / "uv-cache"
cache_dir.mkdir(exist_ok=True)
cmd = ["uvx", "--cache-dir", str(cache_dir), package] + args
if package_cmd:
cmd = ["uvx", "--cache-dir", str(cache_dir), "--from", package, package_cmd] + args
else:
cmd = ["uvx", "--cache-dir", str(cache_dir), package] + args
cwd = sandbox
elif dist_type == "binary":
current_platform = get_current_platform()
Expand Down Expand Up @@ -773,6 +802,7 @@ def verify_auth(
if dist_type == "npx" and should_retry_npx_auth_with_install(result.error, result.stderr_tail):
npx_dist = agent["distribution"].get("npx", {})
package = npx_dist.get("package", "")
package_cmd = npx_dist.get("cmd", "")
args = npx_dist.get("args", [])

print(" Installing package into sandbox and retrying...")
Expand All @@ -787,7 +817,9 @@ def verify_auth(
if install_error is not None:
return Result(agent_id, dist_type, False, install_error)

installed_cmd = build_installed_npx_command(package, args, sandbox, auth_sandbox)
installed_cmd = build_installed_npx_command(
package, args, sandbox, auth_sandbox, package_cmd
)
if installed_cmd is None:
return Result(
agent_id,
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ Supported platforms: `darwin-aarch64`, `darwin-x86_64`, `linux-aarch64`, `linux-
"distribution": {
"npx": {
"package": "@your-scope/your-package@1.0.0",
"cmd": "your-command",
"args": ["--acp"]
}
}
Expand All @@ -118,6 +119,7 @@ Supported platforms: `darwin-aarch64`, `darwin-x86_64`, `linux-aarch64`, `linux-
"distribution": {
"uvx": {
"package": "your-package",
"cmd": "your-command",
"args": ["serve", "--acp"]
}
}
Expand All @@ -134,6 +136,10 @@ Supported platforms: `darwin-aarch64`, `darwin-x86_64`, `linux-aarch64`, `linux-
| `description` | string | Brief description |
| `distribution` | object | At least one distribution method |

For package distributions, `cmd` is optional. Use it when the executable that
starts ACP is not the package manager's default command for the package. For
example, uvx entries with `cmd` run as `uvx --from <package> <cmd> [args]`.

## Optional Fields

| Field | Type | Description |
Expand Down
6 changes: 4 additions & 2 deletions FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ Each agent has the following structure:
},
"npx": {
"package": "@scope/package",
"cmd": "package-command",
"args": ["--acp"]
},
"uvx": {
"package": "package-name",
"cmd": "package-command",
"args": ["serve"]
}
}
Expand All @@ -48,8 +50,8 @@ Each agent has the following structure:
| Type | Description | Command |
| -------- | ----------------------------- | ---------------------- |
| `binary` | Platform-specific executables | Download, extract, run |
| `npx` | npm packages | `npx <package> [args]` |
| `uvx` | PyPI packages via uv | `uvx <package> [args]` |
| `npx` | npm packages | `npx <package> [args]`, or `npx --package <package> <cmd> [args]` when `cmd` is set |
| `uvx` | PyPI packages via uv | `uvx <package> [args]`, or `uvx --from <package> <cmd> [args]` when `cmd` is set |

**Supported archive formats for binary distribution:** `.zip`, `.tar.gz`, `.tgz`, `.tar.bz2`, `.tbz2`, or raw binaries. Installer formats (`.dmg`, `.pkg`, `.deb`, `.rpm`, `.msi`, `.appimage`) are not supported.

Expand Down
5 changes: 5 additions & 0 deletions agent.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@
"minLength": 1,
"description": "Package name (with optional version)"
},
"cmd": {
"type": "string",
"minLength": 1,
"description": "Executable command to run from the package. When omitted, the package manager default command is used."
},
"args": {
"type": "array",
"items": {
Expand Down