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:
+68
-4
@@ -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 prgs→Timesheet 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,
|
||||
|
||||
Reference in New Issue
Block a user