feat(mcp): enforce strict cross-project mutation boundaries (Closes #707)
This commit is contained in:
+60
-7
@@ -54,20 +54,62 @@ def assess_remote_repo_match(
|
||||
local_remote_url: str | None,
|
||||
org_explicit: bool,
|
||||
repo_explicit: bool,
|
||||
for_mutation: bool = False,
|
||||
primary_org: str | None = None,
|
||||
primary_repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Fail closed when the resolved org/repo disagrees with the local git remote.
|
||||
|
||||
The guard is intentionally conservative:
|
||||
The guard enforces two protection levels:
|
||||
|
||||
* When the caller passed both ``org`` and ``repo`` explicitly, their intent is
|
||||
authoritative and the guard never blocks.
|
||||
* When the local git remote URL is unavailable (``None``/empty), corroboration
|
||||
is impossible, so the guard does not block (best-effort only).
|
||||
* Otherwise, the resolved ``org/repo`` slug must appear in the local remote URL
|
||||
(case-insensitive); if it does not, the guard blocks.
|
||||
1. Cross-Project Mutation Boundary (#707):
|
||||
When ``for_mutation`` is True (codebase mutation operation: branch, commit, PR,
|
||||
merge, branch deletion), the target repository (``resolved_org/resolved_repo``)
|
||||
MUST match the primary authorized project context (``primary_org/primary_repo``
|
||||
or parsed from ``local_remote_url``). Any attempt to mutate a different project
|
||||
fails closed, even if explicit org/repo were passed. Metadata operations (such
|
||||
as creating issues or commenting on issues) across projects remain allowed.
|
||||
|
||||
2. Workspace Mismatch Guard (#530):
|
||||
When ``for_mutation`` is False (or targets match), bare remotes must resolve
|
||||
to an org/repo slug present in the local git remote URL. When explicit org/repo
|
||||
are supplied for non-mutation operations, the caller's intent is authoritative.
|
||||
"""
|
||||
reasons: list[str] = []
|
||||
|
||||
eff_primary_org = primary_org
|
||||
eff_primary_repo = primary_repo
|
||||
if not eff_primary_org or not eff_primary_repo:
|
||||
parsed_primary = parse_org_repo_from_remote_url(local_remote_url)
|
||||
if parsed_primary:
|
||||
eff_primary_org = eff_primary_org or parsed_primary[0]
|
||||
eff_primary_repo = eff_primary_repo or parsed_primary[1]
|
||||
|
||||
# #707: Enforce cross-project codebase mutation boundary if for_mutation is True
|
||||
if for_mutation and eff_primary_org and eff_primary_repo:
|
||||
if (
|
||||
resolved_org.lower() != eff_primary_org.lower()
|
||||
or resolved_repo.lower() != eff_primary_repo.lower()
|
||||
):
|
||||
reasons.append(
|
||||
f"Cross-project mutation guard (#707): Attempted codebase mutation targeting "
|
||||
f"'{resolved_org}/{resolved_repo}' outside of primary authorized project "
|
||||
f"context '{eff_primary_org}/{eff_primary_repo}'"
|
||||
)
|
||||
return {
|
||||
"proven": False,
|
||||
"block": True,
|
||||
"cross_project_mutation_block": True,
|
||||
"reasons": reasons,
|
||||
"remote": remote,
|
||||
"resolved_org": resolved_org,
|
||||
"resolved_repo": resolved_repo,
|
||||
"primary_org": eff_primary_org,
|
||||
"primary_repo": eff_primary_repo,
|
||||
"local_remote_url": local_remote_url,
|
||||
"remediation": f"Cross-project codebase work is forbidden. Create an issue in the target repository ('{resolved_org}/{resolved_repo}') instead.",
|
||||
}
|
||||
|
||||
if org_explicit and repo_explicit:
|
||||
return _assessment(True, reasons, remote, resolved_org, resolved_repo, local_remote_url)
|
||||
|
||||
@@ -88,6 +130,16 @@ def assess_remote_repo_match(
|
||||
|
||||
def format_remote_repo_guard_error(assessment: dict) -> str:
|
||||
"""Single RuntimeError message for the MCP resolver gate."""
|
||||
if assessment.get("cross_project_mutation_block"):
|
||||
resolved = f"{assessment.get('resolved_org')}/{assessment.get('resolved_repo')}"
|
||||
primary = f"{assessment.get('primary_org')}/{assessment.get('primary_repo')}"
|
||||
return (
|
||||
f"Cross-project mutation guard (#707): Attempted codebase mutation targeting '{resolved}' "
|
||||
f"outside of primary authorized project context '{primary}'. "
|
||||
f"Cross-project codebase work (creating branches, committing files, creating PRs) is forbidden; "
|
||||
f"create an issue in the target repository ('{resolved}') instead."
|
||||
)
|
||||
|
||||
reasons = "; ".join(
|
||||
assessment.get("reasons") or ["remote/repo resolution mismatch"]
|
||||
)
|
||||
@@ -120,3 +172,4 @@ def _assessment(
|
||||
"local_remote_url": local_remote_url,
|
||||
"remediation": REMEDIATION,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user