From 4e28cab29763896623f3b92d52688bf889b1e611 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 19 Mar 2026 21:38:35 +0100 Subject: [PATCH] fix(mcp): resolve repo paths with existence check before trusting hostname match Stale host_paths entries (wrong username, old machine) were silently overriding the correct local_path, causing FileNotFoundError on tools like list_kaizen_agents. Extracts _resolve_repo_path(repo) helper that tries host_paths[hostname] first but validates the path exists on disk before trusting it, then falls back to local_path. Both candidates support ~ expansion. Applied to all 4 call sites: _kaizen_agents_dir, validate_repo_adr, check_repo_consistency, ingest_sbom_tool. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- mcp_server/server.py | 89 ++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/mcp_server/server.py b/mcp_server/server.py index d1bfa78..4cb59c6 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -1032,19 +1032,54 @@ def update_repo_path(repo_slug: str, path: str, host: str | None = None) -> str: return json.dumps(repo, indent=2) +# --------------------------------------------------------------------------- +# Shared path resolution helper +# --------------------------------------------------------------------------- + +def _resolve_repo_path(repo: dict) -> str: + """Return the best local filesystem path for *repo* on this host. + + Resolution order — each candidate is expanded (supports ``~``) and + verified to exist before being accepted: + + 1. ``host_paths[hostname]`` — host-specific override + 2. ``local_path`` — default fallback + + Returns the resolved path string, or ``""`` if no valid path is found. + """ + import socket as _socket + hostname = _socket.gethostname() + host_paths = repo.get("host_paths") or {} + + candidates = [] + if host_paths.get(hostname): + candidates.append(host_paths[hostname]) + if repo.get("local_path"): + candidates.append(repo["local_path"]) + + for raw in candidates: + resolved = str(Path(raw).expanduser()) + if Path(resolved).is_dir(): + return resolved + + return "" + + # --------------------------------------------------------------------------- # Kaizen Agents # --------------------------------------------------------------------------- def _kaizen_agents_dir() -> Path: - """Resolve the kaizen-agentic agents/ directory via host_paths → local_path fallback.""" - import socket as _socket + """Resolve the kaizen-agentic agents/ directory.""" repo = _get("/repos/kaizen-agentic") - hostname = _socket.gethostname() - host_paths = repo.get("host_paths") or {} - base = host_paths.get(hostname) or repo.get("local_path") or "" + base = _resolve_repo_path(repo) if not base: - raise FileNotFoundError("kaizen-agentic path not found for this host. Register it with update_repo_path().") + import socket as _socket + hostname = _socket.gethostname() + raise FileNotFoundError( + f"kaizen-agentic path not found on host '{hostname}'. " + "Register it with update_repo_path('kaizen-agentic', '/path/to/repo')." + ) agents_dir = Path(base) / "agents" if not agents_dir.is_dir(): raise FileNotFoundError(f"agents/ directory not found at {agents_dir}") @@ -1128,8 +1163,8 @@ def validate_repo_adr(repo_slug: str, domain_slug: str | None = None) -> str: no active state-hub workstreams for the domain lack a backing file (orphan detection — DB-only records are an ADR-001 violation). - The repo path is resolved from the DB using the current machine's hostname - (host_paths[hostname] → local_path fallback). This tool always runs against + The repo path is resolved from the DB: host_paths[hostname] is tried first + (with existence check), then local_path — both support ~ expansion. This tool always runs against the server's copy of the repo. Remote agents on a different branch should sync first, or run validate_repo_adr.py locally with --api-base http://127.0.0.1:18000. @@ -1146,22 +1181,15 @@ def validate_repo_adr(repo_slug: str, domain_slug: str | None = None) -> str: if isinstance(repo, dict) and repo.get("error"): return f"Repo '{repo_slug}' not found: {repo['error']}" - hostname = _socket.gethostname() - host_paths = repo.get("host_paths") or {} - repo_path = host_paths.get(hostname) or repo.get("local_path") or "" - + repo_path = _resolve_repo_path(repo) if not repo_path: + hostname = _socket.gethostname() return ( - f"⚠ No path registered for repo '{repo_slug}' on this host ({hostname}).\n" + f"⚠ No accessible path found for repo '{repo_slug}' on host '{hostname}'.\n" f"Register with: update_repo_path('{repo_slug}', '/path/to/repo')\n" f"Remote agents: run validate_repo_adr.py locally with " f"--api-base {API_BASE}" ) - if not Path(repo_path).is_dir(): - return ( - f"⚠ Registered path for '{repo_slug}' on {hostname} does not exist: {repo_path}\n" - f"Update with: update_repo_path('{repo_slug}', '/correct/path')" - ) script = Path(__file__).parent.parent / "scripts" / "validate_repo_adr.py" cmd = [sys.executable, str(script), repo_path, "--json", @@ -1238,21 +1266,15 @@ def check_repo_consistency(repo_slug: str, fix: bool = False) -> str: repo = _get(f"/repos/{repo_slug}") if isinstance(repo, dict) and repo.get("error"): return f"Repo '{repo_slug}' not found: {repo['error']}" - hostname = _socket.gethostname() - host_paths = repo.get("host_paths") or {} - repo_path = host_paths.get(hostname) or repo.get("local_path") or "" + repo_path = _resolve_repo_path(repo) if not repo_path: + hostname = _socket.gethostname() return ( - f"⚠ No path registered for repo '{repo_slug}' on this host ({hostname}).\n" + f"⚠ No accessible path found for repo '{repo_slug}' on host '{hostname}'.\n" f"Register with: update_repo_path('{repo_slug}', '/path/to/repo')\n" f"Remote agents: run consistency_check.py locally with " f"--api-base {API_BASE}" ) - if not Path(repo_path).is_dir(): - return ( - f"⚠ Registered path for '{repo_slug}' on {hostname} does not exist: {repo_path}\n" - f"Update with: update_repo_path('{repo_slug}', '/correct/path')" - ) script = Path(__file__).parent.parent / "scripts" / "consistency_check.py" cmd = [sys.executable, str(script), "--repo", repo_slug, "--json", @@ -1448,20 +1470,13 @@ def ingest_sbom_tool(repo_slug: str, lockfile_path: str | None = None) -> str: if isinstance(repo, dict) and repo.get("error"): return f"Repo '{repo_slug}' not found: {repo['error']}" - hostname = _socket.gethostname() - host_paths = repo.get("host_paths") or {} - repo_root = host_paths.get(hostname) or repo.get("local_path") or "" - + repo_root = _resolve_repo_path(repo) if not repo_root: + hostname = _socket.gethostname() return ( - f"⚠ No path registered for repo '{repo_slug}' on this host ({hostname}).\n" + f"⚠ No accessible path found for repo '{repo_slug}' on host '{hostname}'.\n" f"Register with: update_repo_path('{repo_slug}', '/path/to/repo')" ) - if not Path(repo_root).is_dir(): - return ( - f"⚠ Registered path for '{repo_slug}' on {hostname} does not exist: {repo_root}\n" - f"Update with: update_repo_path('{repo_slug}', '/correct/path')" - ) script = Path(__file__).parent.parent / "scripts" / "ingest_sbom.py" cmd = [sys.executable, str(script), "--repo", repo_slug,