From 3316a1224ba7ca32a705b17de284d82a5a529b88 Mon Sep 17 00:00:00 2001 From: liuzikaii <2319582736@qq.com> Date: Mon, 31 Aug 2026 22:43:45 +0800 Subject: [PATCH] fix(skills): resolve project roots from session cwd --- agent/skill_utils.py | 22 +++++++++++----------- tests/agent/test_project_skills.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/agent/skill_utils.py b/agent/skill_utils.py index 1b0b002ab4..e9677cae30 100644 --- a/agent/skill_utils.py +++ b/agent/skill_utils.py @@ -419,20 +419,20 @@ _PROJECT_ROOT_MAX_DEPTH = 64 # walk-up bound for pathological cwds def find_project_root(start: Optional[Path] = None) -> Optional[Path]: """Nearest ancestor containing ``.git`` (dir or worktree file), or None. - Without *start*, the surface's ``TERMINAL_CWD`` wins over process cwd so - cron/API surfaces inherit an interactive trust decision by project identity. - - When *start* is not given, the surface's working directory wins over the process cwd: ``TERMINAL_CWD`` - is the same per-surface workdir the terminal tool and cron jobs use (a cron job sets it from its per-job - ``workdir`` without chdir'ing the scheduler process). This is what lets non-interactive surfaces inherit - a prior interactive trust decision by project identity — and a surface with no workdir in a trusted repo - simply resolves no project and loads nothing (#48975). + Without *start*, the surface's effective working directory wins over the process cwd — the same + ladder every other cwd consumer reads (``resolve_agent_cwd``: session-bound cwd, then the scope's + ``TERMINAL_CWD``, then the process cwd). The session cwd comes first because a multi-session host + (TUI/desktop gateway) pins each session's workspace there while its terminal scope resolves a + placeholder ``terminal.cwd`` to ``$HOME``; reading only the scope made every project skill invisible + on those surfaces (#114359). ``TERMINAL_CWD`` is the per-surface workdir the terminal tool and cron + jobs use (a cron job sets it from its per-job ``workdir`` without chdir'ing the scheduler process), + which lets non-interactive surfaces inherit a prior interactive trust decision by project identity — + and a surface with no workdir in a trusted repo simply resolves no project and loads nothing (#48975). """ try: if start is None: - from agent.runtime_cwd import scope_terminal_cwd - env_cwd = scope_terminal_cwd() - start = Path(env_cwd) if env_cwd else Path.cwd() + from agent.runtime_cwd import resolve_agent_cwd + start = resolve_agent_cwd() cur = Path(start).resolve() except OSError: return None diff --git a/tests/agent/test_project_skills.py b/tests/agent/test_project_skills.py index 2cbb8c9e87..9d3937e37f 100644 --- a/tests/agent/test_project_skills.py +++ b/tests/agent/test_project_skills.py @@ -139,6 +139,25 @@ class TestNonInteractiveInheritance: assert su.find_project_root() == project_env["repo"].resolve() assert su.get_project_skills_dirs() != [] + def test_session_cwd_beats_backend_launch_cwd( + self, project_env, monkeypatch, tmp_path + ): + """Desktop project skills follow the active session, not launch cwd.""" + from gateway.session_context import clear_session_vars, set_session_vars + + launcher = tmp_path / "desktop-launcher" + launcher.mkdir() + monkeypatch.chdir(launcher) + monkeypatch.setenv("TERMINAL_CWD", str(launcher)) + _trust(project_env["config"], project_env["repo"]) + + tokens = set_session_vars(cwd=str(project_env["repo"])) + try: + assert su.find_project_root() == project_env["repo"].resolve() + assert su.get_project_skills_dirs() != [] + finally: + clear_session_vars(tokens) + def test_no_workdir_no_trust_inheritance(self, project_env, monkeypatch, tmp_path): # A surface running outside any repo (API server from home-like dir) # resolves no project even when OTHER repos are trusted.