Merge pull request 'feat: add native MCP preference gate and shell circuit breaker (Closes #270)' (#377) from feat/issue-270-native-mcp-preference into master

This commit was merged in pull request #377.
This commit is contained in:
2026-07-07 08:54:03 -05:00
3 changed files with 563 additions and 0 deletions
+73
View File
@@ -478,6 +478,7 @@ import agent_temp_artifacts
import issue_lock_worktree # noqa: E402
import issue_claim_heartbeat # noqa: E402
import merged_cleanup_reconcile # noqa: E402
import native_mcp_preference # noqa: E402
# Fail-closed exact-issue-lock file (#204): written by gitea_lock_issue,
@@ -4083,6 +4084,14 @@ _GUIDE_RULES = {
"inventory, summarize) needs no explicit authorization. Enforced "
"fail closed via subagent_gate.assess_subagent_delegation and "
"validate_subagent_report (#266)."),
"native_mcp_preference": (
"Gitea mutations must use native MCP tools first. When MCP is "
"available, block shell scripts, direct API calls, WebFetch, "
"Playwright, helper encoders, and profile-secret reads unless "
"explicit recovery-mode proof is complete. If MCP transport is "
"broken or the shell circuit breaker trips, emit a terminal recovery "
"report instead of improvising unsafe fallback. Reviewers must not "
"touch or restart MCP server files/processes (#270)."),
}
_COMMON_WORKFLOWS = [
@@ -4787,6 +4796,69 @@ def _build_runtime_task_capabilities(
}
@mcp.tool()
@mcp.tool()
def gitea_assess_gitea_operation_path(
task: str,
path_kind: str | None = None,
command_or_detail: str | None = None,
mcp_available: bool = True,
mcp_tool_visible: bool = True,
recovery_mode: bool = False,
recovery_proof_complete: bool = False,
remote: str = "dadeschools",
host: str | None = None,
) -> dict:
"""Read-only: assess whether a proposed Gitea mutation path is allowed (#270).
Native MCP must be preferred when available. Shell scripts, direct API
calls, WebFetch, Playwright, and helper encoders are blocked unless
explicit recovery-mode proof is supplied.
"""
profile = get_profile()
role = _role_kind(
profile.get("allowed_operations") or [],
profile.get("forbidden_operations") or [],
)
return native_mcp_preference.assess_gitea_operation_path(
task=task,
path_kind=path_kind,
command_or_detail=command_or_detail,
mcp_available=mcp_available,
mcp_tool_visible=mcp_tool_visible,
recovery_mode=recovery_mode,
recovery_proof_complete=recovery_proof_complete,
role=role,
active_profile=profile.get("profile_name"),
)
@mcp.tool()
def gitea_record_shell_spawn_outcome(
exit_code: int | None = None,
stdout: str = "",
stderr: str = "",
spawn_failure: bool | None = None,
probe_attempted: bool = False,
probe_succeeded: bool | None = None,
) -> dict:
"""Record shell spawn outcome and update the session circuit breaker (#270)."""
return native_mcp_preference.record_shell_spawn_outcome(
exit_code=exit_code,
stdout=stdout,
stderr=stderr,
spawn_failure=spawn_failure,
probe_attempted=probe_attempted,
probe_succeeded=probe_succeeded,
)
@mcp.tool()
def gitea_get_shell_health() -> dict:
"""Read-only: return shell spawn circuit-breaker state for this MCP session (#270)."""
return native_mcp_preference.shell_health_status()
@mcp.tool()
def gitea_get_runtime_context(
remote: str = "dadeschools",
@@ -4905,6 +4977,7 @@ def gitea_get_runtime_context(
"preflight_block_reasons": preflight["preflight_block_reasons"],
"preflight_workspace": preflight.get("preflight_workspace"),
"session_capabilities": session_capabilities,
"shell_health": native_mcp_preference.shell_health_status(),
}
if reveal and h: