mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(agent): scope install-tree guard to fallback-picked cwds, allow cli/tui in-tree dev
Follow-up on the salvaged #64611 commit: the original guard blocked the install tree unconditionally, which would have broken the legitimate 'developing Hermes from a source clone' CLI flow (launching hermes inside the repo and getting its AGENTS.md as project context). Refined policy: - resolve_context_cwd(): validates configured paths (missing dir -> None + warning) but honors an EXPLICIT install-tree cwd verbatim — deliberate user choice. - build_context_files_prompt(): blocks only the cwd=None -> os.getcwd() FALLBACK into the install tree, with a new allow_install_tree_fallback param. system_prompt.py passes it for platform cli/tui (launch dir is the user's real shell cwd there); desktop/gateway surfaces keep the guard (their fallback dir is self-spawned, never user-picked). - Warning log names the resolved dir and the terminal.cwd remedy. E2E-verified all five scenarios: desktop fallback blocked, in-tree CLI dev keeps AGENTS.md, explicit install-tree cwd honored, invalid TERMINAL_CWD falls to None then blocked, normal workspace loads.
This commit is contained in:
parent
33513991be
commit
244f70aae5
7 changed files with 77 additions and 26 deletions
|
|
@ -1957,6 +1957,7 @@ def build_context_files_prompt(
|
|||
cwd: Optional[str] = None,
|
||||
skip_soul: bool = False,
|
||||
context_length: Optional[int] = None,
|
||||
allow_install_tree_fallback: bool = False,
|
||||
) -> str:
|
||||
"""Discover and load context files for the system prompt.
|
||||
|
||||
|
|
@ -1978,20 +1979,32 @@ def build_context_files_prompt(
|
|||
"""
|
||||
if cwd is None:
|
||||
cwd = os.getcwd()
|
||||
cwd_is_fallback = True
|
||||
else:
|
||||
cwd_is_fallback = False
|
||||
|
||||
cwd_path = Path(cwd).resolve()
|
||||
sections = []
|
||||
|
||||
# Never discover project context inside the Hermes install/source tree. A
|
||||
# backend launched from, or self-spawning into, that tree (the desktop app
|
||||
# default) would otherwise load this repo's contributor AGENTS.md as
|
||||
# authoritative project context. resolve_context_cwd() already guards the
|
||||
# configured-path cases; this covers the cwd=None -> os.getcwd() fallback.
|
||||
# Never let a FALLBACK-picked directory inside the Hermes install/source
|
||||
# tree gain system-prompt authority. A backend that self-spawns into that
|
||||
# tree (the desktop app default) would otherwise load this repo's
|
||||
# contributor AGENTS.md as authoritative project context (#64590). An
|
||||
# explicitly configured cwd is honored verbatim — the Hermes tree is a
|
||||
# legitimate workspace when the user deliberately points a session at it —
|
||||
# and CLI-style surfaces pass allow_install_tree_fallback=True because
|
||||
# their launch dir IS the user's shell cwd (developing Hermes in-tree).
|
||||
from agent.runtime_cwd import _is_install_tree
|
||||
|
||||
if _is_install_tree(cwd_path):
|
||||
logger.info(
|
||||
"skipping project-context discovery in the Hermes install tree: %s",
|
||||
if (
|
||||
cwd_is_fallback
|
||||
and not allow_install_tree_fallback
|
||||
and _is_install_tree(cwd_path)
|
||||
):
|
||||
logger.warning(
|
||||
"skipping project-context discovery: working-directory resolution "
|
||||
"fell back to the Hermes install tree (%s) — set terminal.cwd to "
|
||||
"your project directory",
|
||||
cwd_path,
|
||||
)
|
||||
project_context = ""
|
||||
|
|
|
|||
|
|
@ -77,15 +77,16 @@ def resolve_context_cwd() -> Path | None:
|
|||
# None means "no configured cwd": build_context_files_prompt then falls back
|
||||
# to the launch dir (os.getcwd()), correct for a local CLI launched inside a
|
||||
# real project. A configured path is validated here (previously it was passed
|
||||
# through unchecked, diverging from resolve_agent_cwd), and the Hermes install
|
||||
# tree is never returned, since its AGENTS.md would take over the system prompt.
|
||||
# through unchecked, diverging from resolve_agent_cwd). An explicitly
|
||||
# configured path is otherwise honored verbatim — including the Hermes
|
||||
# source tree itself, which is a legitimate workspace when the user is
|
||||
# developing Hermes (per-surface policy for fallback-picked directories
|
||||
# lives in build_context_files_prompt; see #64590).
|
||||
override = _session_cwd_override()
|
||||
if override:
|
||||
p = Path(override).expanduser()
|
||||
if not p.is_dir():
|
||||
logger.warning("configured working directory does not exist: %s", override)
|
||||
elif _is_install_tree(p):
|
||||
logger.warning("not loading context files from the Hermes install tree: %s", p)
|
||||
else:
|
||||
return p
|
||||
return None
|
||||
|
|
@ -94,8 +95,6 @@ def resolve_context_cwd() -> Path | None:
|
|||
p = Path(raw).expanduser()
|
||||
if not p.is_dir():
|
||||
logger.warning("TERMINAL_CWD does not exist: %s", raw)
|
||||
elif _is_install_tree(p):
|
||||
logger.warning("not loading context files from the Hermes install tree: %s", p)
|
||||
else:
|
||||
return p
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -463,9 +463,16 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None)
|
|||
# CLI), None lets build_context_files_prompt fall back to the launch
|
||||
# dir — the user's real cwd there, but the install dir for the gateway
|
||||
# daemon, which is why the gateway sets TERMINAL_CWD.
|
||||
#
|
||||
# allow_install_tree_fallback: for cli/tui the launch dir IS the
|
||||
# user's shell cwd, so an in-tree fallback is a deliberate choice
|
||||
# (developing Hermes). Every other surface (desktop chat panel,
|
||||
# gateway daemons) self-spawns into the install tree, where the
|
||||
# fallback would inject this repo's contributor AGENTS.md (#64590).
|
||||
context_files_prompt = _r.build_context_files_prompt(
|
||||
cwd=resolve_context_cwd(), skip_soul=_soul_loaded,
|
||||
context_length=_ctx_len)
|
||||
context_length=_ctx_len,
|
||||
allow_install_tree_fallback=agent.platform in ("cli", "tui"))
|
||||
if context_files_prompt:
|
||||
context_parts.append(context_files_prompt)
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
|
|||
# Auto-extracted from noreply emails + manual overrides
|
||||
AUTHOR_MAP = {
|
||||
"doogie@spark.local": "SAMBAS123", # PR #64986 salvage (gateway: multiplex primary bot token scope)
|
||||
"evefromwayback@gmail.com": "evefromwayback", # PR #64611 salvage (agent: never load install-tree AGENTS.md as project context)
|
||||
"41409874+2751738943@users.noreply.github.com": "2751738943", # PR #54785 salvage (tui: post-turn completion ownership routing)
|
||||
"Burgunthy@users.noreply.github.com": "Burgunthy", # PR #20096 salvage (gateway: profile-based routing for inbound messages)
|
||||
"75556242+webtecnica@users.noreply.github.com": "webtecnica", # PR #63360 salvage (nous: restore inference-api base_url)
|
||||
|
|
|
|||
|
|
@ -708,17 +708,43 @@ class TestBuildContextFilesPrompt:
|
|||
assert "Ruff for linting" in result
|
||||
assert "Project Context" in result
|
||||
|
||||
def test_skips_agents_md_in_install_tree(self, monkeypatch, tmp_path):
|
||||
# A backend launched from, or self-spawning into, the install tree must not
|
||||
# load that tree's contributor AGENTS.md as project context. The guard keys
|
||||
# off the package root, so point it at a fake tree holding an AGENTS.md.
|
||||
def test_skips_agents_md_in_install_tree_on_fallback(self, monkeypatch, tmp_path):
|
||||
# A backend that FALLS BACK into the install tree (cwd=None → getcwd,
|
||||
# the desktop default) must not load that tree's contributor AGENTS.md
|
||||
# as project context. The guard keys off the package root, so point it
|
||||
# at a fake tree holding an AGENTS.md and getcwd into it.
|
||||
import agent.runtime_cwd as rt
|
||||
|
||||
monkeypatch.setattr(rt, "_PACKAGE_ROOT", tmp_path.resolve())
|
||||
(tmp_path / "AGENTS.md").write_text("Never give up on the right solution.")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
result = build_context_files_prompt(cwd=None, skip_soul=True)
|
||||
assert "Never give up" not in result
|
||||
assert result == ""
|
||||
|
||||
def test_loads_agents_md_in_install_tree_when_explicit(self, monkeypatch, tmp_path):
|
||||
# An EXPLICIT cwd pointing at the install tree is a deliberate user
|
||||
# choice (developing Hermes) — discovery must still run.
|
||||
import agent.runtime_cwd as rt
|
||||
|
||||
monkeypatch.setattr(rt, "_PACKAGE_ROOT", tmp_path.resolve())
|
||||
(tmp_path / "AGENTS.md").write_text("Never give up on the right solution.")
|
||||
result = build_context_files_prompt(cwd=str(tmp_path), skip_soul=True)
|
||||
assert "Never give up" not in result
|
||||
assert result == ""
|
||||
assert "Never give up" in result
|
||||
|
||||
def test_loads_agents_md_in_install_tree_fallback_for_cli(self, monkeypatch, tmp_path):
|
||||
# CLI/TUI surfaces launch from the user's shell cwd, so an in-tree
|
||||
# fallback there is deliberate — allow_install_tree_fallback=True
|
||||
# (system_prompt.py passes it for platform cli/tui) keeps discovery on.
|
||||
import agent.runtime_cwd as rt
|
||||
|
||||
monkeypatch.setattr(rt, "_PACKAGE_ROOT", tmp_path.resolve())
|
||||
(tmp_path / "AGENTS.md").write_text("Never give up on the right solution.")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
result = build_context_files_prompt(
|
||||
cwd=None, skip_soul=True, allow_install_tree_fallback=True
|
||||
)
|
||||
assert "Never give up" in result
|
||||
|
||||
def test_loads_cursorrules(self, tmp_path):
|
||||
(tmp_path / ".cursorrules").write_text("Always use type hints.")
|
||||
|
|
|
|||
|
|
@ -74,11 +74,13 @@ class TestResolveContextCwd:
|
|||
monkeypatch.setenv("TERMINAL_CWD", str(missing))
|
||||
assert resolve_context_cwd() is None
|
||||
|
||||
def test_returns_none_for_install_tree(self, monkeypatch):
|
||||
# Context discovery must never resolve to the Hermes install/source tree,
|
||||
# whose contributor AGENTS.md would take over the system prompt.
|
||||
def test_returns_install_tree_when_explicitly_configured(self, monkeypatch):
|
||||
# An EXPLICITLY configured install-tree cwd is honored verbatim — the
|
||||
# Hermes source tree is a legitimate workspace when the user is
|
||||
# developing Hermes. Only the fallback path (cwd=None → os.getcwd())
|
||||
# is policed, in build_context_files_prompt (#64590).
|
||||
monkeypatch.setenv("TERMINAL_CWD", str(rt._PACKAGE_ROOT))
|
||||
assert resolve_context_cwd() is None
|
||||
assert resolve_context_cwd() == rt._PACKAGE_ROOT
|
||||
|
||||
def test_expands_leading_tilde(self, monkeypatch):
|
||||
monkeypatch.setenv("TERMINAL_CWD", "~")
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ def _captured_context_cwd(agent):
|
|||
"""The cwd build_system_prompt_parts hands to build_context_files_prompt."""
|
||||
captured = {}
|
||||
|
||||
def fake_context_files(cwd=None, skip_soul=False, context_length=None):
|
||||
def fake_context_files(
|
||||
cwd=None, skip_soul=False, context_length=None,
|
||||
allow_install_tree_fallback=False,
|
||||
):
|
||||
captured["cwd"] = cwd
|
||||
return ""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue