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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user