feat: add read-only gitea_get_profile discovery tool (#13)

Add a read-only MCP tool that reports the active runtime execution
profile so an LLM can inspect what the current process is configured to
do before deciding whether to attempt an action later.

- gitea_get_profile: returns profile_name, allowed/forbidden operation
  categories, audit_label, token_source_name (a NAME, never a value),
  base_url, remote, resolved server, and — optionally — the verified
  authenticated username. Identity resolution fails soft and marks
  identity_status (verified/unknown/unavailable/not_resolved); the
  profile config is always returned. Never mutates Gitea.
- gitea_auth.get_profile(): extended with forbidden_operations,
  audit_label, token_source_name from env (non-secret metadata).
- .env.example / README: document the new optional metadata vars and
  the discovery tool.
- tests: metadata parsing, verified/unavailable/unknown identity paths,
  skip-identity, and secret-redaction.

Read-only. No token exposure. No multi-token switching. No PR
eligibility, review, or merge workflow. No Jenkins/Ops/GlitchTip/
Release/deploy behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 13:41:14 -04:00
parent 769bec05e7
commit 38c96d5815
5 changed files with 195 additions and 8 deletions
+87 -1
View File
@@ -27,6 +27,7 @@ from mcp_server import ( # noqa: E402
gitea_get_file,
gitea_commit_files,
gitea_whoami,
gitea_get_profile,
)
from gitea_auth import get_profile # noqa: E402
@@ -547,8 +548,10 @@ class TestRuntimeProfile(unittest.TestCase):
with patch.dict(os.environ, env, clear=True):
p = get_profile()
blob = repr(p).lower()
# The token VALUE must never appear. (The field name
# 'token_source_name' is non-secret metadata and may exist.)
self.assertNotIn("super-secret-token", blob)
self.assertNotIn("token", blob)
self.assertIsNone(p.get("token_source_name")) # GITEA_TOKEN_SOURCE unset
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
@@ -570,5 +573,88 @@ class TestRuntimeProfile(unittest.TestCase):
self.assertNotIn(secret, blob)
# ---------------------------------------------------------------------------
# Profile discovery (read-only) — issue #13
# ---------------------------------------------------------------------------
class TestProfileDiscovery(unittest.TestCase):
def test_get_profile_new_fields_default_empty(self):
with patch.dict(os.environ, {}, clear=True):
p = get_profile()
self.assertEqual(p["forbidden_operations"], [])
self.assertIsNone(p["audit_label"])
self.assertIsNone(p["token_source_name"])
def test_get_profile_reads_all_metadata(self):
env = {
"GITEA_PROFILE_NAME": "gitea-reviewer",
"GITEA_ALLOWED_OPERATIONS": "read,review,approve",
"GITEA_FORBIDDEN_OPERATIONS": "merge, branch.push",
"GITEA_AUDIT_LABEL": "reviewer-runtime",
"GITEA_TOKEN_SOURCE": "GITEA_TOKEN",
}
with patch.dict(os.environ, env, clear=True):
p = get_profile()
self.assertEqual(p["forbidden_operations"], ["merge", "branch.push"])
self.assertEqual(p["audit_label"], "reviewer-runtime")
self.assertEqual(p["token_source_name"], "GITEA_TOKEN")
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_discovery_verified_identity(self, _auth, mock_api):
mock_api.return_value = {"id": 3, "login": "reviewer-bot"}
env = {
"GITEA_PROFILE_NAME": "gitea-reviewer",
"GITEA_ALLOWED_OPERATIONS": "read,review,approve",
"GITEA_TOKEN_SOURCE": "GITEA_TOKEN",
"GITEA_TOKEN": "super-secret-token",
}
with patch.dict(os.environ, env, clear=True):
result = gitea_get_profile(remote="prgs")
self.assertEqual(result["profile_name"], "gitea-reviewer")
self.assertEqual(result["allowed_operations"], ["read", "review", "approve"])
self.assertEqual(result["authenticated_username"], "reviewer-bot")
self.assertEqual(result["identity_status"], "verified")
self.assertEqual(result["server"], "https://gitea.prgs.cc")
self.assertEqual(result["token_source_name"], "GITEA_TOKEN")
# Read-only: only a GET to the user endpoint was issued.
self.assertEqual(mock_api.call_args[0][0], "GET")
self.assertTrue(mock_api.call_args[0][1].endswith("/api/v1/user"))
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_discovery_never_exposes_secrets(self, _auth, mock_api):
mock_api.return_value = {"id": 3, "login": "reviewer-bot"}
env = {"GITEA_PROFILE_NAME": "gitea-reviewer", "GITEA_TOKEN": "super-secret-token"}
with patch.dict(os.environ, env, clear=True):
result = gitea_get_profile(remote="prgs")
blob = repr(result).lower()
for secret in ("super-secret-token", "token dgvzd", "authorization", "basic ", FAKE_AUTH.lower()):
self.assertNotIn(secret, blob)
@patch("mcp_server.get_auth_header", return_value=None)
def test_discovery_identity_unavailable_fails_soft(self, _auth):
# No credentials -> _auth raises inside the tool; identity marked
# unavailable but the profile config is still returned (not raised).
with patch.dict(os.environ, {"GITEA_PROFILE_NAME": "gitea-author"}, clear=True):
result = gitea_get_profile(remote="prgs")
self.assertEqual(result["profile_name"], "gitea-author")
self.assertIsNone(result["authenticated_username"])
self.assertEqual(result["identity_status"], "unavailable")
def test_discovery_can_skip_identity(self):
with patch.dict(os.environ, {"GITEA_PROFILE_NAME": "gitea-author"}, clear=True):
result = gitea_get_profile(remote="prgs", resolve_identity=False)
self.assertEqual(result["identity_status"], "not_resolved")
self.assertIsNone(result["authenticated_username"])
def test_discovery_unknown_remote_marks_unknown(self):
with patch.dict(os.environ, {}, clear=True):
result = gitea_get_profile(remote="nope")
self.assertEqual(result["identity_status"], "unknown")
self.assertIsNone(result["remote"])
self.assertIn("remote_error", result)
if __name__ == "__main__":
unittest.main()