diff --git a/agent/system_prompt.py b/agent/system_prompt.py index f47300d9ed..64c39c7c83 100644 --- a/agent/system_prompt.py +++ b/agent/system_prompt.py @@ -892,8 +892,16 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None) # (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_cwd = resolve_context_cwd() + if getattr(agent, "_context_cwd_is_launch_artifact", False): + # Desktop session creation pins the backend launch directory so + # tools have a deterministic cwd even when the user picked no + # workspace. Preserve that tool routing, but let context discovery + # see it as the fallback it really is. The install-tree guard can + # then reject Hermes's bundled contributor AGENTS.md (#97448). + context_cwd = None context_files_prompt = _r.build_context_files_prompt( - cwd=resolve_context_cwd(), skip_soul=_soul_loaded, + cwd=context_cwd, skip_soul=_soul_loaded, context_length=_ctx_len, allow_install_tree_fallback=agent.platform in ("cli", "tui"), home_override=_agent_home(agent)) diff --git a/tests/agent/test_system_prompt.py b/tests/agent/test_system_prompt.py index 15082b9856..b204981cb3 100644 --- a/tests/agent/test_system_prompt.py +++ b/tests/agent/test_system_prompt.py @@ -66,6 +66,50 @@ class TestContextFileCwd: monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) assert _captured_context_cwd(_make_agent()) == tmp_path + def test_desktop_launch_artifact_does_not_load_bundled_agents_md( + self, monkeypatch, tmp_path + ): + import agent.runtime_cwd as runtime_cwd + + monkeypatch.setattr(runtime_cwd, "_PACKAGE_ROOT", tmp_path.resolve()) + monkeypatch.chdir(tmp_path) + (tmp_path / "AGENTS.md").write_text("bundled contributor instructions") + + agent = _make_agent( + platform="desktop", + _context_cwd_is_launch_artifact=True, + ) + with ( + patch("run_agent.load_soul_md", return_value=""), + patch("run_agent.build_environment_hints", return_value=""), + patch("agent.system_prompt.resolve_context_cwd", return_value=tmp_path), + ): + context = build_system_prompt_parts(agent)["context"] + + assert "bundled contributor instructions" not in context + + def test_desktop_explicit_install_tree_workspace_still_loads_agents_md( + self, monkeypatch, tmp_path + ): + import agent.runtime_cwd as runtime_cwd + + monkeypatch.setattr(runtime_cwd, "_PACKAGE_ROOT", tmp_path.resolve()) + monkeypatch.chdir(tmp_path) + (tmp_path / "AGENTS.md").write_text("chosen workspace instructions") + + agent = _make_agent( + platform="desktop", + _context_cwd_is_launch_artifact=False, + ) + with ( + patch("run_agent.load_soul_md", return_value=""), + patch("run_agent.build_environment_hints", return_value=""), + patch("agent.system_prompt.resolve_context_cwd", return_value=tmp_path), + ): + context = build_system_prompt_parts(agent)["context"] + + assert "chosen workspace instructions" in context + def _stable_prompt(agent): with ( diff --git a/tests/tui_gateway/test_projects_rpc.py b/tests/tui_gateway/test_projects_rpc.py index a2f9314828..9e1a6f5256 100644 --- a/tests/tui_gateway/test_projects_rpc.py +++ b/tests/tui_gateway/test_projects_rpc.py @@ -496,6 +496,21 @@ def test_desktop_launch_cwd_is_not_persisted_as_a_workspace(): ) == "/picked/repo" +def test_desktop_launch_cwd_is_marked_as_context_artifact(): + assert server._context_cwd_is_launch_artifact( + {"source": "desktop", "cwd": "/opt/hermes"} + ) is True + + +def test_explicit_desktop_and_terminal_cwds_are_context_workspaces(): + assert server._context_cwd_is_launch_artifact( + {"source": "desktop", "cwd": "/picked/repo", "explicit_cwd": True} + ) is False + assert server._context_cwd_is_launch_artifact( + {"source": "tui", "cwd": "/opt/hermes"} + ) is False + + def test_home_container_dirs_are_never_a_workspace(tmp_path): """`/home` and `/Users` hold homes; they are not workspaces themselves. diff --git a/tui_gateway/compute_host.py b/tui_gateway/compute_host.py index 2cb60cf203..56f7953984 100644 --- a/tui_gateway/compute_host.py +++ b/tui_gateway/compute_host.py @@ -573,6 +573,9 @@ class ComputeHost: reasoning_config_override=frame.get("reasoning_config_override"), service_tier_override=frame.get("service_tier_override"), platform_override=frame.get("source"), + context_cwd_is_launch_artifact=bool( + frame.get("context_cwd_is_launch_artifact", False) + ), session_db=session_db, ) if server._transfer_db_to_agent(agent, session_db): diff --git a/tui_gateway/methods_session.py b/tui_gateway/methods_session.py index 7d827cc895..8b32c05188 100644 --- a/tui_gateway/methods_session.py +++ b/tui_gateway/methods_session.py @@ -945,6 +945,10 @@ def _(rid, params: dict) -> dict: session_id=target, session_db=db, platform_override=source, + context_cwd_is_launch_artifact=( + source in _LAUNCH_CWD_NOT_A_WORKSPACE + and not profile_resume_cwd + ), **stored_runtime_overrides, ) finally: @@ -3304,6 +3308,9 @@ def _(rid, params: dict) -> dict: session_id=new_key, session_db=branch_db, platform_override=source, + context_cwd_is_launch_artifact=( + _context_cwd_is_launch_artifact(session) + ), ) finally: _clear_session_context(tokens) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index b3896f7eba..106f8d1fde 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -2634,6 +2634,7 @@ def _compute_host_turn_frame( "history_version": history_version, "cols": int(session.get("cols", 80) or 80), "cwd": _session_cwd(session), + "context_cwd_is_launch_artifact": _context_cwd_is_launch_artifact(session), "profile_home": session.get("profile_home") or "", "model_override": session.get("model_override"), "reasoning_config_override": session.get("create_reasoning_override"), @@ -3568,6 +3569,15 @@ def _session_cwd(session: dict | None) -> str: _LAUNCH_CWD_NOT_A_WORKSPACE = {"desktop"} +def _context_cwd_is_launch_artifact(session: dict | None) -> bool: + """Whether the session cwd came from app launch rather than user intent.""" + return bool( + session + and not session.get("explicit_cwd") + and _session_source(session) in _LAUNCH_CWD_NOT_A_WORKSPACE + ) + + def _persisted_session_cwd(session: dict) -> str | None: """The cwd to stamp on the session's DB row, or None to leave it unset. @@ -8832,6 +8842,7 @@ def _make_agent( reasoning_config_override: dict | None = None, service_tier_override: str | None = None, platform_override: str | None = None, + context_cwd_is_launch_artifact: bool | None = None, ): # AC-4 test seam: dead unless explicitly armed by the isolated certify # harness. Both inline and compute-host paths construct through _make_agent, @@ -8961,7 +8972,7 @@ def _make_agent( raise RuntimeError("Auth fallback resolved without a model") model = resolution.selected_model _pr = _load_provider_routing() - return AIAgent( + agent = AIAgent( model=model, max_iterations=_cfg_max_turns(cfg, 500), provider=runtime.get("provider"), @@ -9008,6 +9019,16 @@ def _make_agent( fallback_model=_load_fallback_model(), **_agent_cbs(sid), ) + if context_cwd_is_launch_artifact is None: + with _sessions_lock: + context_session = _sessions.get(sid) + context_cwd_is_launch_artifact = _context_cwd_is_launch_artifact( + context_session + ) + agent._context_cwd_is_launch_artifact = bool( + context_cwd_is_launch_artifact + ) + return agent def _init_session(