Merge pull request 'Detect stale MCP runtime before mutation tools execute' (#545) from fix/issue-531-mcp-eof-defect into master
This commit was merged in pull request #545.
This commit is contained in:
@@ -696,6 +696,39 @@ def _verify_role_mutation_workspace(
|
||||
task: str | None = None,
|
||||
) -> str:
|
||||
"""Bind reviewer/merger mutations to the active namespace workspace (#510)."""
|
||||
# Check running runtimes to prevent stale mutations
|
||||
try:
|
||||
if "PYTEST_CURRENT_TEST" not in os.environ or "GITEA_FORCE_MCP_RUNTIME_CHECK" in os.environ:
|
||||
config = gitea_config.load_config()
|
||||
required_permission = task_capability_map.required_permission(task) if task else None
|
||||
matching_profiles = []
|
||||
if required_permission and config and "profiles" in config:
|
||||
for p_name, p_data in config["profiles"].items():
|
||||
p_allowed = p_data.get("allowed_operations") or []
|
||||
p_forbidden = p_data.get("forbidden_operations") or []
|
||||
p_allowed_n = []
|
||||
for op in p_allowed:
|
||||
try:
|
||||
p_allowed_n.append(gitea_config.normalize_operation(op))
|
||||
except Exception:
|
||||
pass
|
||||
p_forbidden_n = []
|
||||
for op in p_forbidden:
|
||||
try:
|
||||
p_forbidden_n.append(gitea_config.normalize_operation(op))
|
||||
except Exception:
|
||||
pass
|
||||
ok, _ = gitea_config.check_operation(required_permission, p_allowed_n, p_forbidden_n)
|
||||
if ok:
|
||||
matching_profiles.append(p_name)
|
||||
runtime_reasons = _check_mcp_runtimes_diagnostics(task or "unknown", matching_profiles)
|
||||
if runtime_reasons:
|
||||
raise RuntimeError("; ".join(runtime_reasons))
|
||||
except Exception as exc:
|
||||
if "stale-runtime:" in str(exc):
|
||||
raise RuntimeError(str(exc))
|
||||
pass
|
||||
|
||||
role = _effective_workspace_role()
|
||||
git_state = issue_lock_worktree.read_worktree_git_state(
|
||||
_resolve_preflight_workspace_path(worktree_path)
|
||||
@@ -8601,6 +8634,102 @@ def gitea_route_task_session(
|
||||
)
|
||||
|
||||
|
||||
def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) -> list[str]:
|
||||
"""Check running runtimes and return errors if they are missing or stale."""
|
||||
import subprocess
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
reasons = []
|
||||
code_path = os.path.join(PROJECT_ROOT, "gitea_mcp_server.py")
|
||||
if not os.path.exists(code_path):
|
||||
return reasons
|
||||
|
||||
code_mtime = datetime.fromtimestamp(os.path.getmtime(code_path))
|
||||
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["ps", "-o", "pid,lstart,command", "-ax"],
|
||||
capture_output=True, text=True, check=True
|
||||
)
|
||||
except Exception as exc:
|
||||
return [f"stale-runtime: failed to list running processes: {exc}"]
|
||||
|
||||
self_pid = os.getpid()
|
||||
self_stale = False
|
||||
|
||||
running_profiles = {}
|
||||
for line in proc.stdout.splitlines()[1:]:
|
||||
line = line.strip()
|
||||
if not line or "mcp_server.py" not in line:
|
||||
continue
|
||||
|
||||
parts = line.split(None, 6)
|
||||
if len(parts) < 7:
|
||||
continue
|
||||
pid_str = parts[0]
|
||||
lstart_str = " ".join(parts[1:6])
|
||||
|
||||
try:
|
||||
pid = int(pid_str)
|
||||
start_time = datetime.strptime(lstart_str, "%a %b %d %H:%M:%S %Y")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
try:
|
||||
env_proc = subprocess.run(
|
||||
["ps", "eww", str(pid)],
|
||||
capture_output=True, text=True, check=True
|
||||
)
|
||||
env_out = env_proc.stdout
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
profile = "gitea-default"
|
||||
match = re.search(r'\bGITEA_MCP_PROFILE=([^\s]+)', env_out)
|
||||
if match:
|
||||
profile = match.group(1)
|
||||
|
||||
is_stale = start_time < code_mtime
|
||||
if pid == self_pid and is_stale:
|
||||
self_stale = True
|
||||
|
||||
if profile not in running_profiles or start_time > running_profiles[profile]["start_time"]:
|
||||
running_profiles[profile] = {
|
||||
"pid": pid,
|
||||
"start_time": start_time,
|
||||
"is_stale": is_stale
|
||||
}
|
||||
|
||||
if self_stale:
|
||||
reasons.append(
|
||||
"stale-runtime: The active Gitea MCP server process is stale (running code from before changes were merged). "
|
||||
"Please fully restart the Gitea MCP server (e.g. touch /Users/jasonwalker/.gemini/config/mcp_config.json) and retry."
|
||||
)
|
||||
|
||||
if matching_profiles:
|
||||
any_running = False
|
||||
any_fresh = False
|
||||
for mp in matching_profiles:
|
||||
if mp in running_profiles:
|
||||
any_running = True
|
||||
if not running_profiles[mp]["is_stale"]:
|
||||
any_fresh = True
|
||||
break
|
||||
if not any_running:
|
||||
reasons.append(
|
||||
f"stale-runtime: None of the matching profiles for task '{task}' ({matching_profiles}) are running in the OS. "
|
||||
"Please restart the MCP server to ensure they are spawned."
|
||||
)
|
||||
elif not any_fresh:
|
||||
reasons.append(
|
||||
f"stale-runtime: All matching profiles for task '{task}' ({matching_profiles}) are running but stale. "
|
||||
"Please fully restart the Gitea MCP server and retry."
|
||||
)
|
||||
|
||||
return reasons
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_resolve_task_capability(
|
||||
task: str,
|
||||
@@ -8711,6 +8840,16 @@ def gitea_resolve_task_capability(
|
||||
configured = len(matching_profiles) > 0
|
||||
available_in_session = allowed_in_current_session
|
||||
|
||||
if "PYTEST_CURRENT_TEST" not in os.environ or "GITEA_FORCE_MCP_RUNTIME_CHECK" in os.environ:
|
||||
runtime_reasons = _check_mcp_runtimes_diagnostics(task, matching_profiles)
|
||||
if runtime_reasons:
|
||||
restart_required = True
|
||||
reason_msg = "; ".join(runtime_reasons)
|
||||
next_safe_action = (
|
||||
"stale-runtime: Gitea MCP runtime conflict or missing process detected. "
|
||||
"Please fully restart the Gitea MCP server and retry."
|
||||
)
|
||||
|
||||
if not allowed_in_current_session:
|
||||
if configured and switching:
|
||||
restart_required = True
|
||||
|
||||
Reference in New Issue
Block a user