Update Gitea operator guide and skill registry for role-aware task routing #162
+77
-14
@@ -1631,6 +1631,14 @@ _HARD_STOPS = [
|
||||
"mergeable signal is honoured, never overridden.",
|
||||
"Do not rewrite the live profiles config; config changes are "
|
||||
"operator-owned.",
|
||||
"Do not hardcode Gitea identities or assume profile roles from prompts. "
|
||||
"Always call gitea_whoami to verify the active identity and profile, and "
|
||||
"call gitea_resolve_task_capability to check required task permissions.",
|
||||
"Use the correct namespace/profile: PR review and merge require a reviewer "
|
||||
"namespace (e.g. gitea-reviewer), and branch/PR creation requires an author "
|
||||
"namespace (e.g. gitea-author).",
|
||||
"Issue comments require explicit gitea.issue.comment permission. PR "
|
||||
"comments (gitea.pr.comment) do not imply or satisfy issue comment requirements.",
|
||||
]
|
||||
|
||||
_GUIDE_RULES = {
|
||||
@@ -1678,19 +1686,23 @@ _GUIDE_RULES = {
|
||||
"task, STOP and report instead of proceeding.",
|
||||
"Runtime identity (whoami) is the source of truth; config-declared "
|
||||
"roles are metadata only.",
|
||||
"Do not hardcode Gitea identities; always dynamically verify your "
|
||||
"identity and resolve task capability via gitea_resolve_task_capability.",
|
||||
],
|
||||
}
|
||||
|
||||
_COMMON_WORKFLOWS = [
|
||||
"task routing: resolve task capability via gitea_resolve_task_capability "
|
||||
"to check required permission/role kind and identify the safe next action.",
|
||||
"issue authoring: verify identity, create/claim the issue, keep scope "
|
||||
"explicit (remote/org/repo).",
|
||||
"implementation: claim issue, branch from fresh master, implement only "
|
||||
"the issue scope, test, open a PR referencing the issue, stop.",
|
||||
"PR review: verify reviewer identity, pin the head SHA, validate "
|
||||
"independently, post a verdict; never review your own work.",
|
||||
"PR merge: reviewer identity + eligibility check + pinned head + "
|
||||
"explicit 'MERGE PR <n>' confirmation, only with operator "
|
||||
"authorization.",
|
||||
"PR review: verify reviewer identity (must be a reviewer profile/namespace), "
|
||||
"pin the head SHA, validate independently, post a verdict; never review your own work.",
|
||||
"PR merge: reviewer identity (must be a reviewer profile/namespace) + "
|
||||
"eligibility check + pinned head + explicit 'MERGE PR <n>' confirmation, "
|
||||
"only with operator authorization.",
|
||||
]
|
||||
|
||||
# Skill registry (#128). status: 'available' = backed by tools in this
|
||||
@@ -1704,8 +1716,13 @@ _PROJECT_SKILLS = {
|
||||
"issue before implementation (gitea_mark_issue).",
|
||||
"required_operations": ["gitea.read"],
|
||||
"status": "available",
|
||||
"notes": "Always pass remote/org/repo explicitly.",
|
||||
"notes": "Always pass remote/org/repo explicitly. Resolve task "
|
||||
"capability first to confirm author profile and "
|
||||
"gitea.issue.* permissions.",
|
||||
"steps": [
|
||||
"Resolve task first: gitea_resolve_task_capability to confirm "
|
||||
"author namespace and required permissions (e.g. for "
|
||||
"create_issue or comment_issue).",
|
||||
"Verify identity with gitea_whoami (explicit remote).",
|
||||
"Check the issue queue with gitea_list_issues.",
|
||||
"Create with gitea_create_issue or claim with gitea_mark_issue "
|
||||
@@ -1722,6 +1739,8 @@ _PROJECT_SKILLS = {
|
||||
"required_operations": ["gitea.pr.create"],
|
||||
"status": "available",
|
||||
"steps": [
|
||||
"Resolve task first: gitea_resolve_task_capability(task='create_pr') "
|
||||
"to confirm author profile.",
|
||||
"Branch from fresh master (git pull first).",
|
||||
"Implement only the claimed issue's scope; stage files "
|
||||
"explicitly.",
|
||||
@@ -1740,6 +1759,8 @@ _PROJECT_SKILLS = {
|
||||
"required_operations": ["gitea.pr.review"],
|
||||
"status": "available",
|
||||
"steps": [
|
||||
"Resolve task first: gitea_resolve_task_capability(task='review_pr') "
|
||||
"to confirm reviewer namespace and avoid author-profile blocks.",
|
||||
"Verify reviewer identity with gitea_whoami; the PR author "
|
||||
"must be a different user.",
|
||||
"Pin the PR head SHA (gitea_view_pr) before validating.",
|
||||
@@ -1757,6 +1778,8 @@ _PROJECT_SKILLS = {
|
||||
"required_operations": ["gitea.pr.merge"],
|
||||
"status": "available",
|
||||
"steps": [
|
||||
"Resolve task first: gitea_resolve_task_capability(task='merge_pr') "
|
||||
"to confirm reviewer namespace.",
|
||||
"Confirm operator authorization for this specific merge.",
|
||||
"Run gitea_check_pr_eligibility action='merge' and resolve "
|
||||
"every reason it reports.",
|
||||
@@ -1774,8 +1797,11 @@ _PROJECT_SKILLS = {
|
||||
"required_operations": ["gitea.issue.comment"],
|
||||
"status": "available",
|
||||
"notes": "Listing needs only gitea.read; posting needs "
|
||||
"gitea.issue.comment, gated separately from gitea.pr.*.",
|
||||
"gitea.issue.comment, gated separately from gitea.pr.*. "
|
||||
"Resolve with gitea_resolve_task_capability first.",
|
||||
"steps": [
|
||||
"Resolve task first: gitea_resolve_task_capability(task='comment_issue') "
|
||||
"to confirm gitea.issue.comment and correct profile/namespace.",
|
||||
"List with gitea_list_issue_comments (explicit issue number).",
|
||||
"Post with gitea_create_issue_comment; markdown body, no "
|
||||
"secrets or raw URLs.",
|
||||
@@ -1783,6 +1809,27 @@ _PROJECT_SKILLS = {
|
||||
"versa.",
|
||||
],
|
||||
},
|
||||
"gitea-resolve-task-capability": {
|
||||
"description": "Determine the required profile, permission, role kind, "
|
||||
"and safe next action (namespace switch or relaunch) "
|
||||
"for a Gitea task before acting.",
|
||||
"when_to_use": "Before any mutating action, review, or comment; "
|
||||
"to select correct author/reviewer namespace and "
|
||||
"avoid permission failures.",
|
||||
"required_operations": ["gitea.read"],
|
||||
"status": "available",
|
||||
"steps": [
|
||||
"Call gitea_resolve_task_capability with the intended task "
|
||||
"(e.g. 'create_issue', 'review_pr', 'comment_issue').",
|
||||
"Inspect 'allowed_in_current_session', 'required_role_kind', "
|
||||
"'matching_configured_profile', 'different_mcp_namespace_required', "
|
||||
"and 'exact_safe_next_action'.",
|
||||
"If not allowed, follow the exact safe next action (do not "
|
||||
"hardcode identity or guess).",
|
||||
"Verify the active profile matches the required role before "
|
||||
"proceeding.",
|
||||
],
|
||||
},
|
||||
"profile-switching": {
|
||||
"description": "Change the active MCP identity between author and "
|
||||
"reviewer profiles.",
|
||||
@@ -1880,6 +1927,11 @@ def mcp_get_control_plane_guide(
|
||||
profile-aware; if identity cannot be resolved it instructs the session
|
||||
to STOP.
|
||||
|
||||
Always resolve task capability (gitea_resolve_task_capability) before
|
||||
acting to determine required role/namespace. Issue comments require
|
||||
gitea.issue.comment (distinct from gitea.pr.comment); review/merge
|
||||
require reviewer namespace.
|
||||
|
||||
Args:
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override the Gitea host.
|
||||
@@ -1903,33 +1955,44 @@ def mcp_get_control_plane_guide(
|
||||
"status": "verified" if username else "unresolved",
|
||||
"instruction": (
|
||||
"Identity verified — confirm it matches the role this task "
|
||||
"requires before acting."
|
||||
"requires before acting. If mismatched or unclear, call "
|
||||
"gitea_resolve_task_capability to check the correct profile or namespace."
|
||||
if username else
|
||||
"STOP: the authenticated identity could not be resolved. Do "
|
||||
"not perform any mutating operation; report to the operator "
|
||||
"and wait."),
|
||||
"not perform any mutating operation; call gitea_resolve_task_capability "
|
||||
"to find the required profile, report to the operator, and wait."),
|
||||
}
|
||||
if _reveal_endpoints():
|
||||
identity["server"] = h
|
||||
|
||||
guidance = []
|
||||
guidance = [
|
||||
"ALWAYS resolve the task capability first: call "
|
||||
"gitea_resolve_task_capability (task=...) before any action. "
|
||||
"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."
|
||||
]
|
||||
if not username:
|
||||
guidance.append(
|
||||
"STOP: identity unresolved — no mutating operations until the "
|
||||
"operator fixes credentials/profile and gitea_whoami verifies.")
|
||||
"operator fixes credentials/profile. Call gitea_resolve_task_capability "
|
||||
"to check required profile/permissions, and verify via gitea_whoami.")
|
||||
if role == "author":
|
||||
guidance.append(
|
||||
"Author profile: creating branches, commits, issues, and PRs "
|
||||
"is allowed; review, approve, and merge are forbidden for this "
|
||||
"profile — never attempt them, and never review or merge your "
|
||||
"own PR from any profile.")
|
||||
"own PR from any profile. Issue comments require "
|
||||
"gitea.issue.comment (separate from gitea.pr.comment).")
|
||||
elif role == "reviewer":
|
||||
guidance.append(
|
||||
"Reviewer profile: review/approve/merge may proceed ONLY after "
|
||||
"gitea_check_pr_eligibility passes and the PR head SHA is "
|
||||
"pinned (expected_head_sha); the PR author must be a different "
|
||||
"user, and merging additionally requires explicit operator "
|
||||
"authorization plus the 'MERGE PR <n>' confirmation.")
|
||||
"authorization plus the 'MERGE PR <n>' confirmation. "
|
||||
"PR comments do not imply issue comments.")
|
||||
elif role == "mixed":
|
||||
guidance.append(
|
||||
"WARNING: this profile allows both authoring and "
|
||||
|
||||
@@ -43,6 +43,7 @@ EXPECTED_SKILLS = [
|
||||
"gitea-pr-review",
|
||||
"gitea-pr-merge",
|
||||
"gitea-issue-comments",
|
||||
"gitea-resolve-task-capability",
|
||||
"profile-switching",
|
||||
"redaction-security-review",
|
||||
"jenkins-readonly",
|
||||
@@ -155,6 +156,50 @@ class TestControlPlaneGuide(GuideTestBase):
|
||||
self.assertIn("workflows", g)
|
||||
self.assertEqual(g["skills_tool"], "mcp_list_project_skills")
|
||||
|
||||
@patch("mcp_server.api_request", return_value={"login": "author-bot"})
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_guide_mentions_resolve_capability_and_issue_comment_separation(self, _auth, _api):
|
||||
"""Covers #144 AC: guide tells to resolve task cap, issue comments require gitea.issue.comment (not implied by pr.comment)."""
|
||||
env = dict(AUTHOR_ENV, GITEA_ALLOWED_OPERATIONS="gitea.read,gitea.pr.comment,gitea.pr.create")
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
g = mcp_get_control_plane_guide(remote="prgs")
|
||||
blob = " ".join(g["guidance"]).lower()
|
||||
self.assertIn("resolve", blob)
|
||||
self.assertIn("gitea.issue.comment", blob)
|
||||
self.assertIn("pr.comment", blob) # separation noted
|
||||
|
||||
@patch("mcp_server.api_request", return_value={"login": "author-bot"})
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_guide_static_profile_safe_next_action(self, _auth, _api):
|
||||
"""Covers #144 AC: static-profile safe next action guidance."""
|
||||
# default is static (no allow_runtime_switching)
|
||||
with patch.dict(os.environ, AUTHOR_ENV, clear=True):
|
||||
g = mcp_get_control_plane_guide(remote="prgs")
|
||||
# since author, guidance includes author info; resolver advice always present
|
||||
blob = json.dumps(g).lower()
|
||||
self.assertTrue("static" in blob or "relaunch" in blob or "namespace" in blob)
|
||||
self.assertIn("resolve", " ".join(g["guidance"]).lower())
|
||||
|
||||
@patch("mcp_server.api_request", return_value={"login": "no-comment-bot"})
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_missing_issue_comment_guidance(self, _auth, _api):
|
||||
"""Covers #144 AC: Guide lists issue comments require gitea.issue.comment and PR comments do not imply them."""
|
||||
with patch.dict(os.environ, AUTHOR_ENV, clear=True):
|
||||
g = mcp_get_control_plane_guide(remote="prgs")
|
||||
hard_stops = g["rules"]["hard_stops"]
|
||||
self.assertTrue(any("gitea.issue.comment" in r for r in hard_stops))
|
||||
self.assertTrue(any("gitea.pr.comment" in r for r in hard_stops))
|
||||
|
||||
@patch("mcp_server.api_request", return_value={"login": "static-bot"})
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_static_profile_safe_next_action(self, _auth, _api):
|
||||
"""Covers #144 AC: Guide covers static-profile safe next action switching instruction."""
|
||||
with patch.dict(os.environ, AUTHOR_ENV, clear=True):
|
||||
g = mcp_get_control_plane_guide(remote="prgs")
|
||||
switching_rules = g["rules"]["profile_switching"]
|
||||
self.assertTrue(any("static-profile mode" in r for r in switching_rules))
|
||||
self.assertTrue(any("GITEA_MCP_PROFILE" in r for r in switching_rules))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# mcp_list_project_skills
|
||||
|
||||
Reference in New Issue
Block a user