Merge pull request 'Update Gitea operator guide and skill registry for role-aware task routing' (#162) from feat/issue-144-operator-guide-updates into master
This commit was merged in pull request #162.
This commit is contained in:
+77
-14
@@ -1740,6 +1740,14 @@ _HARD_STOPS = [
|
|||||||
"mergeable signal is honoured, never overridden.",
|
"mergeable signal is honoured, never overridden.",
|
||||||
"Do not rewrite the live profiles config; config changes are "
|
"Do not rewrite the live profiles config; config changes are "
|
||||||
"operator-owned.",
|
"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 = {
|
_GUIDE_RULES = {
|
||||||
@@ -1787,19 +1795,23 @@ _GUIDE_RULES = {
|
|||||||
"task, STOP and report instead of proceeding.",
|
"task, STOP and report instead of proceeding.",
|
||||||
"Runtime identity (whoami) is the source of truth; config-declared "
|
"Runtime identity (whoami) is the source of truth; config-declared "
|
||||||
"roles are metadata only.",
|
"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 = [
|
_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 "
|
"issue authoring: verify identity, create/claim the issue, keep scope "
|
||||||
"explicit (remote/org/repo).",
|
"explicit (remote/org/repo).",
|
||||||
"implementation: claim issue, branch from fresh master, implement only "
|
"implementation: claim issue, branch from fresh master, implement only "
|
||||||
"the issue scope, test, open a PR referencing the issue, stop.",
|
"the issue scope, test, open a PR referencing the issue, stop.",
|
||||||
"PR review: verify reviewer identity, pin the head SHA, validate "
|
"PR review: verify reviewer identity (must be a reviewer profile/namespace), "
|
||||||
"independently, post a verdict; never review your own work.",
|
"pin the head SHA, validate independently, post a verdict; never review your own work.",
|
||||||
"PR merge: reviewer identity + eligibility check + pinned head + "
|
"PR merge: reviewer identity (must be a reviewer profile/namespace) + "
|
||||||
"explicit 'MERGE PR <n>' confirmation, only with operator "
|
"eligibility check + pinned head + explicit 'MERGE PR <n>' confirmation, "
|
||||||
"authorization.",
|
"only with operator authorization.",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Skill registry (#128). status: 'available' = backed by tools in this
|
# Skill registry (#128). status: 'available' = backed by tools in this
|
||||||
@@ -1813,8 +1825,13 @@ _PROJECT_SKILLS = {
|
|||||||
"issue before implementation (gitea_mark_issue).",
|
"issue before implementation (gitea_mark_issue).",
|
||||||
"required_operations": ["gitea.read"],
|
"required_operations": ["gitea.read"],
|
||||||
"status": "available",
|
"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": [
|
"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).",
|
"Verify identity with gitea_whoami (explicit remote).",
|
||||||
"Check the issue queue with gitea_list_issues.",
|
"Check the issue queue with gitea_list_issues.",
|
||||||
"Create with gitea_create_issue or claim with gitea_mark_issue "
|
"Create with gitea_create_issue or claim with gitea_mark_issue "
|
||||||
@@ -1831,6 +1848,8 @@ _PROJECT_SKILLS = {
|
|||||||
"required_operations": ["gitea.pr.create"],
|
"required_operations": ["gitea.pr.create"],
|
||||||
"status": "available",
|
"status": "available",
|
||||||
"steps": [
|
"steps": [
|
||||||
|
"Resolve task first: gitea_resolve_task_capability(task='create_pr') "
|
||||||
|
"to confirm author profile.",
|
||||||
"Branch from fresh master (git pull first).",
|
"Branch from fresh master (git pull first).",
|
||||||
"Implement only the claimed issue's scope; stage files "
|
"Implement only the claimed issue's scope; stage files "
|
||||||
"explicitly.",
|
"explicitly.",
|
||||||
@@ -1849,6 +1868,8 @@ _PROJECT_SKILLS = {
|
|||||||
"required_operations": ["gitea.pr.review"],
|
"required_operations": ["gitea.pr.review"],
|
||||||
"status": "available",
|
"status": "available",
|
||||||
"steps": [
|
"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 "
|
"Verify reviewer identity with gitea_whoami; the PR author "
|
||||||
"must be a different user.",
|
"must be a different user.",
|
||||||
"Pin the PR head SHA (gitea_view_pr) before validating.",
|
"Pin the PR head SHA (gitea_view_pr) before validating.",
|
||||||
@@ -1866,6 +1887,8 @@ _PROJECT_SKILLS = {
|
|||||||
"required_operations": ["gitea.pr.merge"],
|
"required_operations": ["gitea.pr.merge"],
|
||||||
"status": "available",
|
"status": "available",
|
||||||
"steps": [
|
"steps": [
|
||||||
|
"Resolve task first: gitea_resolve_task_capability(task='merge_pr') "
|
||||||
|
"to confirm reviewer namespace.",
|
||||||
"Confirm operator authorization for this specific merge.",
|
"Confirm operator authorization for this specific merge.",
|
||||||
"Run gitea_check_pr_eligibility action='merge' and resolve "
|
"Run gitea_check_pr_eligibility action='merge' and resolve "
|
||||||
"every reason it reports.",
|
"every reason it reports.",
|
||||||
@@ -1883,8 +1906,11 @@ _PROJECT_SKILLS = {
|
|||||||
"required_operations": ["gitea.issue.comment"],
|
"required_operations": ["gitea.issue.comment"],
|
||||||
"status": "available",
|
"status": "available",
|
||||||
"notes": "Listing needs only gitea.read; posting needs "
|
"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": [
|
"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).",
|
"List with gitea_list_issue_comments (explicit issue number).",
|
||||||
"Post with gitea_create_issue_comment; markdown body, no "
|
"Post with gitea_create_issue_comment; markdown body, no "
|
||||||
"secrets or raw URLs.",
|
"secrets or raw URLs.",
|
||||||
@@ -1892,6 +1918,27 @@ _PROJECT_SKILLS = {
|
|||||||
"versa.",
|
"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": {
|
"profile-switching": {
|
||||||
"description": "Change the active MCP identity between author and "
|
"description": "Change the active MCP identity between author and "
|
||||||
"reviewer profiles.",
|
"reviewer profiles.",
|
||||||
@@ -1989,6 +2036,11 @@ def mcp_get_control_plane_guide(
|
|||||||
profile-aware; if identity cannot be resolved it instructs the session
|
profile-aware; if identity cannot be resolved it instructs the session
|
||||||
to STOP.
|
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:
|
Args:
|
||||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||||
host: Override the Gitea host.
|
host: Override the Gitea host.
|
||||||
@@ -2012,33 +2064,44 @@ def mcp_get_control_plane_guide(
|
|||||||
"status": "verified" if username else "unresolved",
|
"status": "verified" if username else "unresolved",
|
||||||
"instruction": (
|
"instruction": (
|
||||||
"Identity verified — confirm it matches the role this task "
|
"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
|
if username else
|
||||||
"STOP: the authenticated identity could not be resolved. Do "
|
"STOP: the authenticated identity could not be resolved. Do "
|
||||||
"not perform any mutating operation; report to the operator "
|
"not perform any mutating operation; call gitea_resolve_task_capability "
|
||||||
"and wait."),
|
"to find the required profile, report to the operator, and wait."),
|
||||||
}
|
}
|
||||||
if _reveal_endpoints():
|
if _reveal_endpoints():
|
||||||
identity["server"] = h
|
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:
|
if not username:
|
||||||
guidance.append(
|
guidance.append(
|
||||||
"STOP: identity unresolved — no mutating operations until the "
|
"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":
|
if role == "author":
|
||||||
guidance.append(
|
guidance.append(
|
||||||
"Author profile: creating branches, commits, issues, and PRs "
|
"Author profile: creating branches, commits, issues, and PRs "
|
||||||
"is allowed; review, approve, and merge are forbidden for this "
|
"is allowed; review, approve, and merge are forbidden for this "
|
||||||
"profile — never attempt them, and never review or merge your "
|
"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":
|
elif role == "reviewer":
|
||||||
guidance.append(
|
guidance.append(
|
||||||
"Reviewer profile: review/approve/merge may proceed ONLY after "
|
"Reviewer profile: review/approve/merge may proceed ONLY after "
|
||||||
"gitea_check_pr_eligibility passes and the PR head SHA is "
|
"gitea_check_pr_eligibility passes and the PR head SHA is "
|
||||||
"pinned (expected_head_sha); the PR author must be a different "
|
"pinned (expected_head_sha); the PR author must be a different "
|
||||||
"user, and merging additionally requires explicit operator "
|
"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":
|
elif role == "mixed":
|
||||||
guidance.append(
|
guidance.append(
|
||||||
"WARNING: this profile allows both authoring and "
|
"WARNING: this profile allows both authoring and "
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ EXPECTED_SKILLS = [
|
|||||||
"gitea-pr-review",
|
"gitea-pr-review",
|
||||||
"gitea-pr-merge",
|
"gitea-pr-merge",
|
||||||
"gitea-issue-comments",
|
"gitea-issue-comments",
|
||||||
|
"gitea-resolve-task-capability",
|
||||||
"profile-switching",
|
"profile-switching",
|
||||||
"redaction-security-review",
|
"redaction-security-review",
|
||||||
"jenkins-readonly",
|
"jenkins-readonly",
|
||||||
@@ -155,6 +156,50 @@ class TestControlPlaneGuide(GuideTestBase):
|
|||||||
self.assertIn("workflows", g)
|
self.assertIn("workflows", g)
|
||||||
self.assertEqual(g["skills_tool"], "mcp_list_project_skills")
|
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
|
# mcp_list_project_skills
|
||||||
|
|||||||
Reference in New Issue
Block a user