fix: control-plane guide org/repo params and local repo alignment (#588)

#530's remote/repo guard blocked mcp_get_control_plane_guide(remote=prgs)
because the bare default resolves to Timesheet while local git is
Gitea-Tools, and remediation told callers to pass org/repo on a tool that
did not accept those parameters.

- Accept optional org/repo on mcp_get_control_plane_guide
- Prefer local git remote org/repo when bare defaults mismatch (read-only guide)
- Keep mutation tools fail-closed on bare mismatch
- Align remediation text with tools that accept org/repo

Closes #588
This commit is contained in:
2026-07-09 13:17:27 -04:00
parent 27d75facd4
commit 57b2023611
4 changed files with 202 additions and 6 deletions
+68 -4
View File
@@ -1359,6 +1359,55 @@ def _resolve(remote: str, host: str | None, org: str | None, repo: str | None):
return (resolved_host, resolved_org, resolved_repo)
def _resolve_control_plane_guide_target(
remote: str,
host: str | None,
org: str | None,
repo: str | None,
) -> tuple[str, str, str]:
"""Resolve guide target with schema-supported org/repo + local context (#530 follow-up).
``mcp_get_control_plane_guide`` is a session bootstrap tool. When called with a
bare ``remote`` whose REMOTES default repo mismatches the local git remote
(classic prgsTimesheet vs Gitea-Tools), prefer the local remote's org/repo so
the guide remains usable. Explicit org/repo always win and are validated by
:func:`_resolve`.
"""
if org is not None and repo is not None:
return _resolve(remote, host, org, repo)
if remote not in REMOTES:
raise ValueError(f"Unknown remote '{remote}'. Choose from: {list(REMOTES)}")
profile = REMOTES[remote]
candidate_org = org if org is not None else profile["org"]
candidate_repo = repo if repo is not None else profile["repo"]
local_url = _local_git_remote_url(remote)
assessment = remote_repo_guard.assess_remote_repo_match(
remote=remote,
resolved_org=candidate_org,
resolved_repo=candidate_repo,
local_remote_url=local_url,
org_explicit=org is not None,
repo_explicit=repo is not None,
)
if assessment.get("block"):
parsed = remote_repo_guard.parse_org_repo_from_remote_url(local_url)
if parsed:
local_org, local_repo = parsed
# Fill only unspecified sides from local git context, then resolve
# as explicit so the guard accepts intentional local alignment.
use_org = org if org is not None else local_org
use_repo = repo if repo is not None else local_repo
return _resolve(remote, host, use_org, use_repo)
# No local context to recover — surface the original guard error.
raise RuntimeError(
remote_repo_guard.format_remote_repo_guard_error(assessment)
)
return _resolve(remote, host, org, repo)
def _enforce_remote_repo_guard(
remote: str,
resolved_org: str,
@@ -7706,6 +7755,8 @@ _PROJECT_SKILLS.update(workflow_skill.workflow_skill_registry_entries())
def mcp_get_control_plane_guide(
remote: str = "dadeschools",
host: str | None = None,
org: str | None = None,
repo: str | None = None,
) -> dict:
"""Structured operator guide for the current Gitea MCP session (#128).
@@ -7725,14 +7776,19 @@ def mcp_get_control_plane_guide(
Args:
remote: Known instance 'dadeschools' or 'prgs'.
host: Override the Gitea host.
org: Optional owner/org override (explicit; recommended when the bare
remote default differs from the local git remote).
repo: Optional repository name override (use with org).
Returns:
dict with 'read_only', 'remote', 'profile' (name, operations,
role_kind), 'identity' (username/status/instruction; 'server' only
with the reveal opt-in), 'guidance' (profile-specific), 'rules',
'workflows', and 'skills_tool'. Never secrets.
with the reveal opt-in), 'resolved_repository' (org/repo actually
used after guard/local-context alignment), 'guidance'
(profile-specific), 'rules', 'workflows', and 'skills_tool'.
Never secrets.
"""
h, _, _ = _resolve(remote, host, None, None)
h, o, r = _resolve_control_plane_guide_target(remote, host, org, repo)
profile = get_profile()
allowed = profile["allowed_operations"]
forbidden = profile["forbidden_operations"]
@@ -7761,7 +7817,11 @@ def mcp_get_control_plane_guide(
"Do not hardcode identity or guess the launcher profile. "
"Verify the active identity/profile matches the required role "
"for the task. Use the correct namespace (author vs reviewer) "
"as reported."
"as reported.",
"When the bare remote default repo mismatches the local git remote "
"(e.g. prgs→Timesheet vs Gitea-Tools), pass explicit "
"org=Scaled-Tech-Consulting repo=Gitea-Tools to "
"mcp_get_control_plane_guide and other tools that accept org/repo.",
]
if not username:
guidance.append(
@@ -7806,6 +7866,10 @@ def mcp_get_control_plane_guide(
return {
"read_only": True,
"remote": remote,
"resolved_repository": {
"org": o,
"repo": r,
},
"profile": {
"name": profile["profile_name"],
"allowed_operations": allowed,