feat: harden gateway with policy engine, secure tools, and governance docs

This commit is contained in:
2026-02-14 16:05:56 +01:00
parent e17d34e6d7
commit 5969892af3
55 changed files with 4711 additions and 1587 deletions

View File

@@ -1,6 +1,8 @@
"""MCP protocol implementation for AegisGitea."""
"""MCP protocol models and tool registry."""
from typing import Any, Dict, List, Optional
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
@@ -10,153 +12,366 @@ class MCPTool(BaseModel):
name: str = Field(..., description="Unique tool identifier")
description: str = Field(..., description="Human-readable tool description")
input_schema: Dict[str, Any] = Field(
..., alias="inputSchema", description="JSON Schema for tool input"
)
model_config = ConfigDict(
populate_by_name=True,
serialize_by_alias=True,
)
input_schema: dict[str, Any] = Field(..., description="JSON schema describing input arguments")
write_operation: bool = Field(default=False, description="Whether tool mutates data")
class MCPToolCallRequest(BaseModel):
"""Request to invoke an MCP tool."""
tool: str = Field(..., description="Name of the tool to invoke")
arguments: Dict[str, Any] = Field(default_factory=dict, description="Tool arguments")
correlation_id: Optional[str] = Field(None, description="Request correlation ID")
arguments: dict[str, Any] = Field(default_factory=dict, description="Tool argument payload")
correlation_id: str | None = Field(default=None, description="Request correlation ID")
model_config = ConfigDict(extra="forbid")
class MCPToolCallResponse(BaseModel):
"""Response from an MCP tool invocation."""
"""Response returned from MCP tool invocation."""
success: bool = Field(..., description="Whether the tool call succeeded")
result: Optional[Any] = Field(None, description="Tool result data")
error: Optional[str] = Field(None, description="Error message if failed")
correlation_id: str = Field(..., description="Request correlation ID")
success: bool = Field(..., description="Whether invocation succeeded")
result: Any | None = Field(default=None, description="Tool result payload")
error: str | None = Field(default=None, description="Error message for failed request")
correlation_id: str = Field(..., description="Correlation ID for request tracing")
class MCPListToolsResponse(BaseModel):
"""Response listing available MCP tools."""
"""Response listing available tools."""
tools: List[MCPTool] = Field(..., description="List of available tools")
tools: list[MCPTool] = Field(..., description="Available tool definitions")
# Tool definitions for AegisGitea MCP
def _tool(
name: str, description: str, schema: dict[str, Any], write_operation: bool = False
) -> MCPTool:
"""Construct tool metadata entry."""
return MCPTool(
name=name,
description=description,
input_schema=schema,
write_operation=write_operation,
)
TOOL_LIST_REPOSITORIES = MCPTool(
name="list_repositories",
description="List all repositories visible to the AI bot user. "
"Only repositories where the bot has explicit read access will be returned. "
"This respects Gitea's dynamic authorization model.",
input_schema={
"type": "object",
"properties": {},
"required": [],
},
)
TOOL_GET_REPOSITORY_INFO = MCPTool(
name="get_repository_info",
description="Get detailed information about a specific repository, "
"including description, default branch, language, and metadata. "
"Requires the bot user to have read access.",
input_schema={
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "Repository owner username or organization",
},
"repo": {
"type": "string",
"description": "Repository name",
},
AVAILABLE_TOOLS: list[MCPTool] = [
_tool(
"list_repositories",
"List repositories visible to the configured bot account.",
{"type": "object", "properties": {}, "required": []},
),
_tool(
"get_repository_info",
"Get metadata for a repository.",
{
"type": "object",
"properties": {"owner": {"type": "string"}, "repo": {"type": "string"}},
"required": ["owner", "repo"],
"additionalProperties": False,
},
"required": ["owner", "repo"],
},
)
TOOL_GET_FILE_TREE = MCPTool(
name="get_file_tree",
description="Get the file tree structure for a repository at a specific ref. "
"Returns a list of files and directories. "
"Non-recursive by default for safety (max depth: 1 level).",
input_schema={
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "Repository owner username or organization",
},
"repo": {
"type": "string",
"description": "Repository name",
},
"ref": {
"type": "string",
"description": "Branch, tag, or commit SHA (defaults to 'main')",
"default": "main",
},
"recursive": {
"type": "boolean",
"description": "Whether to recursively fetch entire tree (use with caution)",
"default": False,
),
_tool(
"get_file_tree",
"Get repository tree at a selected ref.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"ref": {"type": "string", "default": "main"},
"recursive": {"type": "boolean", "default": False},
},
"required": ["owner", "repo"],
"additionalProperties": False,
},
"required": ["owner", "repo"],
},
)
TOOL_GET_FILE_CONTENTS = MCPTool(
name="get_file_contents",
description="Read the contents of a specific file in a repository. "
"File size is limited to 1MB by default for safety. "
"Returns base64-encoded content for binary files.",
input_schema={
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "Repository owner username or organization",
},
"repo": {
"type": "string",
"description": "Repository name",
},
"filepath": {
"type": "string",
"description": "Path to file within repository (e.g., 'src/main.py')",
},
"ref": {
"type": "string",
"description": "Branch, tag, or commit SHA (defaults to 'main')",
"default": "main",
),
_tool(
"get_file_contents",
"Read a repository file with size-limited content.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"filepath": {"type": "string"},
"ref": {"type": "string", "default": "main"},
},
"required": ["owner", "repo", "filepath"],
"additionalProperties": False,
},
"required": ["owner", "repo", "filepath"],
},
)
# Registry of all available tools
AVAILABLE_TOOLS: List[MCPTool] = [
TOOL_LIST_REPOSITORIES,
TOOL_GET_REPOSITORY_INFO,
TOOL_GET_FILE_TREE,
TOOL_GET_FILE_CONTENTS,
),
_tool(
"search_code",
"Search code in a repository.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"query": {"type": "string"},
"ref": {"type": "string", "default": "main"},
"page": {"type": "integer", "minimum": 1, "default": 1},
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
},
"required": ["owner", "repo", "query"],
"additionalProperties": False,
},
),
_tool(
"list_commits",
"List commits for a repository ref.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"ref": {"type": "string", "default": "main"},
"page": {"type": "integer", "minimum": 1, "default": 1},
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
},
"required": ["owner", "repo"],
"additionalProperties": False,
},
),
_tool(
"get_commit_diff",
"Get commit metadata and file diffs.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"sha": {"type": "string"},
},
"required": ["owner", "repo", "sha"],
"additionalProperties": False,
},
),
_tool(
"compare_refs",
"Compare two repository refs.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"base": {"type": "string"},
"head": {"type": "string"},
},
"required": ["owner", "repo", "base", "head"],
"additionalProperties": False,
},
),
_tool(
"list_issues",
"List repository issues.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"state": {"type": "string", "enum": ["open", "closed", "all"], "default": "open"},
"page": {"type": "integer", "minimum": 1, "default": 1},
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
"labels": {"type": "array", "items": {"type": "string"}, "default": []},
},
"required": ["owner", "repo"],
"additionalProperties": False,
},
),
_tool(
"get_issue",
"Get repository issue details.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"issue_number": {"type": "integer", "minimum": 1},
},
"required": ["owner", "repo", "issue_number"],
"additionalProperties": False,
},
),
_tool(
"list_pull_requests",
"List repository pull requests.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"state": {"type": "string", "enum": ["open", "closed", "all"], "default": "open"},
"page": {"type": "integer", "minimum": 1, "default": 1},
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
},
"required": ["owner", "repo"],
"additionalProperties": False,
},
),
_tool(
"get_pull_request",
"Get pull request details.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"pull_number": {"type": "integer", "minimum": 1},
},
"required": ["owner", "repo", "pull_number"],
"additionalProperties": False,
},
),
_tool(
"list_labels",
"List labels defined on a repository.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"page": {"type": "integer", "minimum": 1, "default": 1},
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
},
"required": ["owner", "repo"],
"additionalProperties": False,
},
),
_tool(
"list_tags",
"List repository tags.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"page": {"type": "integer", "minimum": 1, "default": 1},
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
},
"required": ["owner", "repo"],
"additionalProperties": False,
},
),
_tool(
"list_releases",
"List repository releases.",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"page": {"type": "integer", "minimum": 1, "default": 1},
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25},
},
"required": ["owner", "repo"],
"additionalProperties": False,
},
),
_tool(
"create_issue",
"Create a repository issue (write-mode only).",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"title": {"type": "string"},
"body": {"type": "string", "default": ""},
"labels": {"type": "array", "items": {"type": "string"}, "default": []},
"assignees": {"type": "array", "items": {"type": "string"}, "default": []},
},
"required": ["owner", "repo", "title"],
"additionalProperties": False,
},
write_operation=True,
),
_tool(
"update_issue",
"Update issue title/body/state (write-mode only).",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"issue_number": {"type": "integer", "minimum": 1},
"title": {"type": "string"},
"body": {"type": "string"},
"state": {"type": "string", "enum": ["open", "closed"]},
},
"required": ["owner", "repo", "issue_number"],
"additionalProperties": False,
},
write_operation=True,
),
_tool(
"create_issue_comment",
"Create issue comment (write-mode only).",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"issue_number": {"type": "integer", "minimum": 1},
"body": {"type": "string"},
},
"required": ["owner", "repo", "issue_number", "body"],
"additionalProperties": False,
},
write_operation=True,
),
_tool(
"create_pr_comment",
"Create pull request comment (write-mode only).",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"pull_number": {"type": "integer", "minimum": 1},
"body": {"type": "string"},
},
"required": ["owner", "repo", "pull_number", "body"],
"additionalProperties": False,
},
write_operation=True,
),
_tool(
"add_labels",
"Add labels to an issue or PR (write-mode only).",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"issue_number": {"type": "integer", "minimum": 1},
"labels": {"type": "array", "items": {"type": "string"}, "minItems": 1},
},
"required": ["owner", "repo", "issue_number", "labels"],
"additionalProperties": False,
},
write_operation=True,
),
_tool(
"assign_issue",
"Assign users to issue or PR (write-mode only).",
{
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"issue_number": {"type": "integer", "minimum": 1},
"assignees": {"type": "array", "items": {"type": "string"}, "minItems": 1},
},
"required": ["owner", "repo", "issue_number", "assignees"],
"additionalProperties": False,
},
write_operation=True,
),
]
def get_tool_by_name(tool_name: str) -> Optional[MCPTool]:
"""Get tool definition by name.
Args:
tool_name: Name of the tool to retrieve
Returns:
Tool definition or None if not found
"""
def get_tool_by_name(tool_name: str) -> MCPTool | None:
"""Get tool definition by name."""
for tool in AVAILABLE_TOOLS:
if tool.name == tool_name:
return tool