mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(agent): never load the install-tree AGENTS.md as project context
This commit is contained in:
parent
9a21d0e3f2
commit
33513991be
4 changed files with 88 additions and 16 deletions
|
|
@ -1982,13 +1982,27 @@ def build_context_files_prompt(
|
|||
cwd_path = Path(cwd).resolve()
|
||||
sections = []
|
||||
|
||||
# Priority-based project context: first match wins
|
||||
project_context = (
|
||||
_load_hermes_md(cwd_path, context_length)
|
||||
or _load_agents_md(cwd_path, context_length)
|
||||
or _load_claude_md(cwd_path, context_length)
|
||||
or _load_cursorrules(cwd_path, context_length)
|
||||
)
|
||||
# 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.
|
||||
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",
|
||||
cwd_path,
|
||||
)
|
||||
project_context = ""
|
||||
else:
|
||||
# Priority-based project context: first match wins
|
||||
project_context = (
|
||||
_load_hermes_md(cwd_path, context_length)
|
||||
or _load_agents_md(cwd_path, context_length)
|
||||
or _load_claude_md(cwd_path, context_length)
|
||||
or _load_cursorrules(cwd_path, context_length)
|
||||
)
|
||||
if project_context:
|
||||
sections.append(project_context)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,15 +10,36 @@ Multi-session gateways can pin a logical cwd via the `_SESSION_CWD`
|
|||
contextvar; CLI/cron fall through to `TERMINAL_CWD`/launch cwd.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from contextvars import ContextVar, Token
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_UNSET: Any = object()
|
||||
|
||||
_SESSION_CWD: ContextVar = ContextVar("HERMES_SESSION_CWD", default=_UNSET)
|
||||
|
||||
# The Python package/source root (this file lives at <root>/agent/runtime_cwd.py).
|
||||
# When a backend is launched from, or self-spawns into, this tree (the desktop
|
||||
# app default), an os.getcwd() fallback would inject this repo's contributor
|
||||
# AGENTS.md as authoritative project context. Context discovery must never
|
||||
# resolve here.
|
||||
_PACKAGE_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _is_install_tree(p: Path) -> bool:
|
||||
# True only when p IS the package root or sits inside it. Ancestors of the
|
||||
# package root (a user home that happens to contain the checkout, a --user
|
||||
# site-packages parent) are legitimate workspaces and must not be blocked.
|
||||
try:
|
||||
p = p.resolve()
|
||||
except Exception:
|
||||
return False
|
||||
return p == _PACKAGE_ROOT or _PACKAGE_ROOT in p.parents
|
||||
|
||||
|
||||
def set_session_cwd(cwd: str | None) -> Token:
|
||||
"""Pin the logical cwd for the current context."""
|
||||
|
|
@ -42,21 +63,39 @@ def resolve_agent_cwd() -> Path:
|
|||
p = Path(override).expanduser()
|
||||
if p.is_dir():
|
||||
return p
|
||||
logger.warning("configured working directory does not exist: %s", override)
|
||||
raw = os.environ.get("TERMINAL_CWD", "").strip()
|
||||
if raw:
|
||||
p = Path(raw).expanduser()
|
||||
if p.is_dir():
|
||||
return p
|
||||
logger.warning("TERMINAL_CWD does not exist: %s", raw)
|
||||
return Path(os.getcwd())
|
||||
|
||||
|
||||
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 the local CLI. The gateway
|
||||
# avoids slurping its install dir by setting TERMINAL_CWD (see system_prompt.py)
|
||||
# or, per session, the _SESSION_CWD contextvar above.
|
||||
# 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.
|
||||
override = _session_cwd_override()
|
||||
if override:
|
||||
return Path(override).expanduser()
|
||||
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
|
||||
raw = os.environ.get("TERMINAL_CWD", "").strip()
|
||||
return Path(raw).expanduser() if raw else None
|
||||
if raw:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -708,6 +708,18 @@ 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.
|
||||
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 == ""
|
||||
|
||||
def test_loads_cursorrules(self, tmp_path):
|
||||
(tmp_path / ".cursorrules").write_text("Always use type hints.")
|
||||
result = build_context_files_prompt(cwd=str(tmp_path))
|
||||
|
|
|
|||
|
|
@ -66,12 +66,19 @@ class TestResolveContextCwd:
|
|||
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
||||
assert resolve_context_cwd() is None
|
||||
|
||||
def test_returns_nonexistent_dir_unguarded(self, monkeypatch, tmp_path):
|
||||
# Deliberate asymmetry vs resolve_agent_cwd: context discovery has no isdir
|
||||
# guard, so a missing dir is returned (not None) — discovery just finds nothing.
|
||||
def test_returns_none_for_nonexistent_dir(self, monkeypatch, tmp_path):
|
||||
# A configured but missing dir must not be returned. It previously was,
|
||||
# which diverged from resolve_agent_cwd and let an invalid cwd steer
|
||||
# context discovery. Now it is validated and drops to None.
|
||||
missing = tmp_path / "gone"
|
||||
monkeypatch.setenv("TERMINAL_CWD", str(missing))
|
||||
assert resolve_context_cwd() == 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.
|
||||
monkeypatch.setenv("TERMINAL_CWD", str(rt._PACKAGE_ROOT))
|
||||
assert resolve_context_cwd() is None
|
||||
|
||||
def test_expands_leading_tilde(self, monkeypatch):
|
||||
monkeypatch.setenv("TERMINAL_CWD", "~")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue