From 789271302bcb8e82afb651b4417424d6f85cfae2 Mon Sep 17 00:00:00 2001 From: lsaether Date: Fri, 22 May 2026 11:52:04 -0500 Subject: [PATCH] feat: support explicit package commands --- .github/workflows/registry_utils.py | 5 +- .../workflows/tests/test_registry_utils.py | 3 + .github/workflows/tests/test_verify_agents.py | 73 +++++++++++++++++++ .github/workflows/verify_agents.py | 48 ++++++++++-- CONTRIBUTING.md | 6 ++ FORMAT.md | 6 +- agent.schema.json | 5 ++ 7 files changed, 134 insertions(+), 12 deletions(-) diff --git a/.github/workflows/registry_utils.py b/.github/workflows/registry_utils.py index 9cf9f301..822968c7 100644 --- a/.github/workflows/registry_utils.py +++ b/.github/workflows/registry_utils.py @@ -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: diff --git a/.github/workflows/tests/test_registry_utils.py b/.github/workflows/tests/test_registry_utils.py index fbdaf177..882a6ee9 100644 --- a/.github/workflows/tests/test_registry_utils.py +++ b/.github/workflows/tests/test_registry_utils.py @@ -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" diff --git a/.github/workflows/tests/test_verify_agents.py b/.github/workflows/tests/test_verify_agents.py index 2708233b..0f71660f 100644 --- a/.github/workflows/tests/test_verify_agents.py +++ b/.github/workflows/tests/test_verify_agents.py @@ -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, @@ -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", diff --git a/.github/workflows/verify_agents.py b/.github/workflows/verify_agents.py index f24ac0ea..8e54f501 100644 --- a/.github/workflows/verify_agents.py +++ b/.github/workflows/verify_agents.py @@ -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, @@ -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: @@ -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: @@ -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() @@ -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...") @@ -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, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b52d9f8d..108a54ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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"] } } @@ -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"] } } @@ -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 [args]`. + ## Optional Fields | Field | Type | Description | diff --git a/FORMAT.md b/FORMAT.md index 7abb9d14..2d0b521a 100644 --- a/FORMAT.md +++ b/FORMAT.md @@ -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"] } } @@ -48,8 +50,8 @@ Each agent has the following structure: | Type | Description | Command | | -------- | ----------------------------- | ---------------------- | | `binary` | Platform-specific executables | Download, extract, run | -| `npx` | npm packages | `npx [args]` | -| `uvx` | PyPI packages via uv | `uvx [args]` | +| `npx` | npm packages | `npx [args]`, or `npx --package [args]` when `cmd` is set | +| `uvx` | PyPI packages via uv | `uvx [args]`, or `uvx --from [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. diff --git a/agent.schema.json b/agent.schema.json index 3f2303f4..e4b40fc4 100644 --- a/agent.schema.json +++ b/agent.schema.json @@ -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": {