mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(install): install a hermes-acp launcher onto PATH
setup_path() wrote a `hermes` launcher to ~/.local/bin but nothing for `hermes-acp`. That console script exists only inside the venv, which is not on the login-shell PATH. ACP hosts resolve the agent by command name against that PATH, so an otherwise healthy install looks absent to them. Buzz Desktop ships a Hermes preset that spawns `hermes-acp` and reports the runtime as unavailable; Zed and JetBrains configs that name the bare command have the same problem. Write a hermes-acp launcher next to the hermes one, dispatching to the acp subcommand. Same PYTHONPATH/PYTHONHOME clearing, and the same rm -f before cat > so an older symlink into the venv cannot be followed and stomp the console script (#21454). Uninstall removes both launchers. tests/test_install_sh_acp_launcher.py drives the block out of install.sh rather than asserting on a copy, covering the venv and non-venv branches plus the symlink-stomp case. Reverting the install.sh change turns all three red. Signed-off-by: SHL0MS <SHL0MS@users.noreply.github.com>
This commit is contained in:
parent
c136400c9e
commit
a7d5147cf1
3 changed files with 155 additions and 0 deletions
|
|
@ -100,7 +100,9 @@ def remove_wrapper_script():
|
|||
"""Remove the hermes wrapper script if it exists."""
|
||||
wrapper_paths = [
|
||||
Path.home() / ".local" / "bin" / "hermes",
|
||||
Path.home() / ".local" / "bin" / "hermes-acp",
|
||||
Path("/usr/local/bin/hermes"),
|
||||
Path("/usr/local/bin/hermes-acp"),
|
||||
]
|
||||
|
||||
removed = []
|
||||
|
|
|
|||
|
|
@ -1677,6 +1677,31 @@ EOF
|
|||
chmod +x "$command_link_dir/hermes"
|
||||
log_success "Installed hermes launcher → $command_link_display_dir/hermes"
|
||||
|
||||
# Also expose `hermes-acp`. ACP hosts (Zed, JetBrains, Buzz) resolve the
|
||||
# agent by command name on the login-shell PATH, and the `hermes-acp`
|
||||
# console script lives inside the venv, which is not on that PATH. Without
|
||||
# this launcher those hosts report Hermes as not installed. (#21454 applies
|
||||
# here too: clear the path first so `cat >` cannot follow an old symlink
|
||||
# into the venv and overwrite the console script.)
|
||||
rm -f "$command_link_dir/hermes-acp"
|
||||
if [ "$USE_VENV" = true ]; then
|
||||
cat > "$command_link_dir/hermes-acp" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
unset PYTHONPATH
|
||||
unset PYTHONHOME
|
||||
exec "$HERMES_BIN" "$HERMES_ENTRYPOINT" acp "\$@"
|
||||
EOF
|
||||
else
|
||||
cat > "$command_link_dir/hermes-acp" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
unset PYTHONPATH
|
||||
unset PYTHONHOME
|
||||
exec "$HERMES_BIN" acp "\$@"
|
||||
EOF
|
||||
fi
|
||||
chmod +x "$command_link_dir/hermes-acp"
|
||||
log_success "Installed hermes-acp launcher → $command_link_display_dir/hermes-acp"
|
||||
|
||||
if [ "$DISTRO" = "termux" ]; then
|
||||
export PATH="$command_link_dir:$PATH"
|
||||
log_info "$command_link_display_dir is the native Termux command path"
|
||||
|
|
|
|||
128
tests/test_install_sh_acp_launcher.py
Normal file
128
tests/test_install_sh_acp_launcher.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""`setup_path()` must also install a `hermes-acp` launcher.
|
||||
|
||||
ACP hosts (Zed, JetBrains, Buzz Desktop) spawn the agent by resolving a command
|
||||
name against the login-shell PATH. The `hermes-acp` console script generated by
|
||||
the packaging metadata lives inside the install's venv, which is not on that
|
||||
PATH, so those hosts report Hermes as not installed. `setup_path()` therefore
|
||||
writes a `hermes-acp` launcher next to the `hermes` one.
|
||||
|
||||
These tests drive the real block out of `scripts/install.sh` rather than
|
||||
asserting on a copy of it, so the shim cannot drift away from the test.
|
||||
"""
|
||||
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
INSTALL_SH = Path(__file__).resolve().parent.parent / "scripts" / "install.sh"
|
||||
|
||||
ACP_BLOCK = re.compile(
|
||||
r'( rm -f "\$command_link_dir/hermes-acp".*?'
|
||||
r'log_success "Installed hermes-acp launcher[^\n]*\n)',
|
||||
re.S,
|
||||
)
|
||||
|
||||
|
||||
def _extract_acp_shim_block() -> str:
|
||||
match = ACP_BLOCK.search(INSTALL_SH.read_text(encoding="utf-8"))
|
||||
assert match, (
|
||||
"could not locate the hermes-acp launcher block in scripts/install.sh — "
|
||||
"if it was renamed, update this test with it"
|
||||
)
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _run_block(tmp_path: Path, use_venv: str) -> Path:
|
||||
"""Execute the extracted block with the env vars setup_path() sets."""
|
||||
command_link_dir = tmp_path / "local_bin"
|
||||
command_link_dir.mkdir()
|
||||
hermes_bin = tmp_path / "venv" / "bin" / "python"
|
||||
hermes_bin.parent.mkdir(parents=True)
|
||||
hermes_bin.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
entrypoint = tmp_path / "hermes"
|
||||
entrypoint.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
|
||||
script = (
|
||||
"set -e\n"
|
||||
f"HERMES_BIN={hermes_bin}\n"
|
||||
f"HERMES_ENTRYPOINT={entrypoint}\n"
|
||||
f"command_link_dir={command_link_dir}\n"
|
||||
f"command_link_display_dir={command_link_dir}\n"
|
||||
f"USE_VENV={use_venv}\n"
|
||||
"log_success(){ :; }\n" + _extract_acp_shim_block()
|
||||
)
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=tmp_path,
|
||||
)
|
||||
assert result.returncode == 0, (
|
||||
f"acp shim block failed:\nstdout={result.stdout}\nstderr={result.stderr}"
|
||||
)
|
||||
return command_link_dir / "hermes-acp"
|
||||
|
||||
|
||||
def test_venv_install_writes_executable_acp_launcher(tmp_path):
|
||||
shim = _run_block(tmp_path, "true")
|
||||
assert shim.is_file()
|
||||
assert shim.stat().st_mode & stat.S_IXUSR, "launcher must be user-executable"
|
||||
|
||||
text = shim.read_text(encoding="utf-8")
|
||||
assert "unset PYTHONPATH" in text
|
||||
assert "unset PYTHONHOME" in text
|
||||
# The launcher must dispatch to the ACP subcommand, otherwise the host gets
|
||||
# an interactive CLI on stdio and the handshake never completes.
|
||||
assert re.search(r'exec .*\bacp\b', text), text
|
||||
|
||||
|
||||
def test_non_venv_install_writes_acp_launcher(tmp_path):
|
||||
shim = _run_block(tmp_path, "false")
|
||||
text = shim.read_text(encoding="utf-8")
|
||||
assert re.search(r'exec .*\bacp\b', text), text
|
||||
|
||||
|
||||
def test_acp_launcher_does_not_follow_a_symlink_into_the_venv(tmp_path):
|
||||
"""Guards the #21454 failure mode for the new launcher.
|
||||
|
||||
An older install could leave `hermes-acp` as a symlink to the venv console
|
||||
script. Without the `rm -f`, `cat >` follows it and overwrites that script
|
||||
with a shim that then execs itself.
|
||||
"""
|
||||
command_link_dir = tmp_path / "local_bin"
|
||||
command_link_dir.mkdir()
|
||||
console_script = tmp_path / "venv" / "bin" / "hermes-acp"
|
||||
console_script.parent.mkdir(parents=True)
|
||||
marker = "#!/usr/bin/env python\n# real console script\n"
|
||||
console_script.write_text(marker, encoding="utf-8")
|
||||
|
||||
shim_path = command_link_dir / "hermes-acp"
|
||||
shim_path.symlink_to(console_script)
|
||||
assert shim_path.is_symlink()
|
||||
|
||||
hermes_bin = tmp_path / "venv" / "bin" / "python"
|
||||
hermes_bin.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
entrypoint = tmp_path / "hermes"
|
||||
entrypoint.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
|
||||
script = (
|
||||
"set -e\n"
|
||||
f"HERMES_BIN={hermes_bin}\n"
|
||||
f"HERMES_ENTRYPOINT={entrypoint}\n"
|
||||
f"command_link_dir={command_link_dir}\n"
|
||||
f"command_link_display_dir={command_link_dir}\n"
|
||||
"USE_VENV=true\n"
|
||||
"log_success(){ :; }\n" + _extract_acp_shim_block()
|
||||
)
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script], capture_output=True, text=True, cwd=tmp_path
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
assert console_script.read_text(encoding="utf-8") == marker, (
|
||||
"venv/bin/hermes-acp was overwritten — symlink-stomp regression (#21454)"
|
||||
)
|
||||
assert not shim_path.is_symlink(), (
|
||||
"command_link_dir/hermes-acp must be replaced with a regular file"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue