From f80977c0bde06e49d2cbf44a813e98edd7d8d733 Mon Sep 17 00:00:00 2001 From: luyifan Date: Mon, 29 Jun 2026 13:22:25 +0800 Subject: [PATCH] fix(cli): guard legacy hermes-agent entrypoint --- pyproject.toml | 2 +- run_agent.py | 93 ++++++++++++++++++++++- tests/agent/test_legacy_cli_entrypoint.py | 83 ++++++++++++++++++++ 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 tests/agent/test_legacy_cli_entrypoint.py diff --git a/pyproject.toml b/pyproject.toml index 62920eaf49..daec744860 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -430,7 +430,7 @@ build-backend = "setuptools.build_meta" [project.scripts] hermes = "hermes_cli.main:main" -hermes-agent = "run_agent:main" +hermes-agent = "run_agent:legacy_cli_main" hermes-acp = "acp_adapter.entry:main" [tool.uv] diff --git a/run_agent.py b/run_agent.py index 64e60e4d96..8d191fb4e4 100644 --- a/run_agent.py +++ b/run_agent.py @@ -12,6 +12,7 @@ try: except ModuleNotFoundError: pass # partial `hermes update` — only skips the Windows UTF-8 stdio setup +import argparse import json import logging logger = logging.getLogger(__name__) @@ -1487,6 +1488,95 @@ def _save_sample_trajectory(agent: "AIAgent", result: dict, user_query: str, mod print(f"\n⚠️ Failed to save sample: {e}") +def _build_legacy_cli_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="hermes-agent", + description=( + "Legacy Hermes Agent runner. For the primary CLI, use the " + "`hermes` command." + ), + add_help=False, + ) + parser.add_argument( + "prompt", + nargs="*", + help="Natural language query to run. Prefer --query for scripted use.", + ) + parser.add_argument("-h", "--help", action="store_true", dest="show_help") + parser.add_argument("--version", "-V", action="store_true") + parser.add_argument("--query", "-q") + parser.add_argument("--model", default="") + parser.add_argument("--api-key", "--api_key", dest="api_key") + parser.add_argument("--base-url", "--base_url", dest="base_url", default="") + parser.add_argument("--max-turns", "--max_turns", dest="max_turns", type=int, default=10) + parser.add_argument("--enabled-toolsets", "--enabled_toolsets", dest="enabled_toolsets") + parser.add_argument("--disabled-toolsets", "--disabled_toolsets", dest="disabled_toolsets") + parser.add_argument("--list-tools", "--list_tools", dest="list_tools", action="store_true") + parser.add_argument( + "--save-trajectories", + "--save_trajectories", + dest="save_trajectories", + action="store_true", + ) + parser.add_argument("--save-sample", "--save_sample", dest="save_sample", action="store_true") + parser.add_argument("--verbose", action="store_true") + parser.add_argument( + "--log-prefix-chars", + "--log_prefix_chars", + dest="log_prefix_chars", + type=int, + default=20, + ) + return parser + + +def legacy_cli_main(argv: Optional[List[str]] = None) -> int: + """Console-script wrapper for the legacy ``hermes-agent`` entry point. + + The packaged console script calls the target function with no arguments. + Pointing it at ``main`` therefore used to start a live model request before + flags like ``--version`` or ``--help`` could be parsed. + """ + parser = _build_legacy_cli_parser() + args = parser.parse_args(argv) + + if args.version: + from hermes_cli import __release_date__, __version__ + + print(f"Hermes Agent v{__version__} ({__release_date__})") + return 0 + + if args.show_help: + parser.print_help() + return 0 + + positional_query = " ".join(args.prompt).strip() if args.prompt else None + if args.query and positional_query: + parser.error("pass the query either positionally or via --query, not both") + + query = args.query or positional_query + if not query and not args.list_tools: + parser.print_help() + print("\nNo query provided; pass --query to run the legacy agent entry point.") + return 0 + + main( + query=query, + model=args.model, + api_key=args.api_key, + base_url=args.base_url, + max_turns=args.max_turns, + enabled_toolsets=args.enabled_toolsets, + disabled_toolsets=args.disabled_toolsets, + list_tools=args.list_tools, + save_trajectories=args.save_trajectories, + save_sample=args.save_sample, + verbose=args.verbose, + log_prefix_chars=args.log_prefix_chars, + ) + return 0 + + def main( query: str = None, model: str = "", api_key: str = None, base_url: str = "", max_turns: int = 10, enabled_toolsets: str = None, disabled_toolsets: str = None, list_tools: bool = False, @@ -1557,8 +1647,7 @@ def main( if __name__ == "__main__": - import fire - fire.Fire(main) + raise SystemExit(legacy_cli_main()) # ---- BEGIN PLUGIN-COMPAT (revert-scheduled; see COMPAT_MANIFEST.md) ---- diff --git a/tests/agent/test_legacy_cli_entrypoint.py b/tests/agent/test_legacy_cli_entrypoint.py new file mode 100644 index 0000000000..8a028a2738 --- /dev/null +++ b/tests/agent/test_legacy_cli_entrypoint.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import tomllib +from pathlib import Path + +import run_agent + + +def _raise_if_agent_starts(**_kwargs): + raise AssertionError("legacy metadata commands must not start the agent") + + +def test_legacy_cli_version_does_not_start_agent(monkeypatch, capsys): + monkeypatch.setattr(run_agent, "main", _raise_if_agent_starts) + + assert run_agent.legacy_cli_main(["--version"]) == 0 + + output = capsys.readouterr().out + assert "Hermes Agent v" in output + + +def test_legacy_cli_help_does_not_start_agent(monkeypatch, capsys): + monkeypatch.setattr(run_agent, "main", _raise_if_agent_starts) + + assert run_agent.legacy_cli_main(["--help"]) == 0 + + output = capsys.readouterr().out + assert "usage: hermes-agent" in output + + +def test_legacy_cli_no_args_is_side_effect_free(monkeypatch, capsys): + monkeypatch.setattr(run_agent, "main", _raise_if_agent_starts) + + assert run_agent.legacy_cli_main([]) == 0 + + output = capsys.readouterr().out + assert "usage: hermes-agent" in output + assert "No query provided" in output + + +def test_legacy_cli_query_dispatches_to_existing_runner(monkeypatch): + calls = {} + + def fake_main(**kwargs): + calls.update(kwargs) + + monkeypatch.setattr(run_agent, "main", fake_main) + + assert run_agent.legacy_cli_main( + [ + "--query", + "hello from the legacy entry point", + "--model", + "provider/model", + "--max-turns", + "3", + "--enabled-toolsets", + "web,terminal", + "--verbose", + ] + ) == 0 + + assert calls == { + "query": "hello from the legacy entry point", + "model": "provider/model", + "api_key": None, + "base_url": "", + "max_turns": 3, + "enabled_toolsets": "web,terminal", + "disabled_toolsets": None, + "list_tools": False, + "save_trajectories": False, + "save_sample": False, + "verbose": True, + "log_prefix_chars": 20, + } + + +def test_packaged_hermes_agent_script_uses_safe_wrapper(): + repo_root = Path(__file__).resolve().parents[2] + pyproject = tomllib.loads((repo_root / "pyproject.toml").read_text(encoding="utf-8")) + + assert pyproject["project"]["scripts"]["hermes-agent"] == "run_agent:legacy_cli_main"