fix(skills): resolve project roots from session cwd

This commit is contained in:
liuzikaii
2026-08-31 22:43:45 +08:00
committed by Teknium
parent 309023a00c
commit 3316a1224b
2 changed files with 30 additions and 11 deletions

View File

@@ -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

View File

@@ -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.