build: switch to uv, hatchling and ruff-only tooling

This commit is contained in:
Latte committed 2026-10-14 15:40:30 +02:00
1 parent 8b7bb7205c
commit c0b22c531e
9 files changed
+2385 -287

No files matched your search

+6 -1
View File
@@ -52,10 +52,11 @@ htmlcov/
# Environment variables # Environment variables
.env .env
!.env.example
.env.local .env.local
.env.backup-* .env.backup-*
# API keys # Keys
keys/ keys/
*.key *.key
@@ -72,3 +73,7 @@ Thumbs.db
# Audit logs (development only - production should use volumes) # Audit logs (development only - production should use volumes)
audit.log audit.log
# Local state
.data/
coverage.xml
+5 -22
View File
@@ -1,42 +1,25 @@
# Pre-commit hooks mirror `make lint` so local commits enforce the same checks as CI. # Hooks mirror `make lint`. Install with: uv run pre-commit install (add pre-commit
# Installed via `make install-dev` (runs `pre-commit install`). # yourself, it is not a project dependency) or run the same commands via `make lint`.
#
# Hooks use `language: system`, i.e. they invoke ruff/black/mypy from PATH rather than
# letting pre-commit manage isolated tool environments. This keeps versions identical to
# `make lint`/`make format` and lets mypy resolve the project's real dependencies.
# Requirement: commit with the dev virtualenv active (so requirements-dev.txt tools are on
# PATH). Outside an active venv the hooks will report the tools as "not found".
repos: repos:
- repo: local - repo: local
hooks: hooks:
- id: ruff-check - id: ruff-check
name: ruff check name: ruff check
entry: ruff check entry: uv run ruff check
language: system language: system
types: [python] types: [python]
args: [src/, tests/]
pass_filenames: false pass_filenames: false
- id: ruff-format - id: ruff-format
name: ruff format --check name: ruff format --check
entry: ruff format --check entry: uv run ruff format --check
language: system language: system
types: [python] types: [python]
args: [src/, tests/]
pass_filenames: false
- id: black
name: black --check
entry: black --check
language: system
types: [python]
args: [src/, tests/]
pass_filenames: false pass_filenames: false
- id: mypy - id: mypy
name: mypy name: mypy
entry: mypy entry: uv run mypy src
language: system language: system
types: [python] types: [python]
args: [src/]
pass_filenames: false pass_filenames: false
+30 -75
View File
@@ -1,89 +1,44 @@
.PHONY: help install install-dev test lint format clean build run docker-build docker-up docker-down docker-logs generate-key rotate-key check-key-age validate-audit .PHONY: help install lint format test test-integration run validate-audit docker-build clean
help: help:
@echo "AegisGitea MCP - Available Commands" @echo "AegisGitea MCP - available targets"
@echo "" @echo " make install Install dependencies (uv sync)"
@echo "Development:" @echo " make lint ruff check, ruff format --check, mypy"
@echo " make install Install production dependencies" @echo " make format Format and autofix with ruff"
@echo " make install-dev Install development dependencies" @echo " make test Run unit tests with coverage"
@echo " make test Run tests with coverage" @echo " make test-integration Run integration tests (needs Docker)"
@echo " make lint Run linters (ruff, mypy)" @echo " make run Run the server (uses .env)"
@echo " make format Format code with black" @echo " make validate-audit Verify audit log hash-chain integrity"
@echo " make clean Remove build artifacts" @echo " make docker-build Build the Docker image"
@echo "" @echo " make clean Remove build artifacts and caches"
@echo "API Key Management:"
@echo " make generate-key Generate new API key"
@echo " make rotate-key Rotate existing API key"
@echo " make check-key-age Check API key age and expiration"
@echo ""
@echo "Local Execution:"
@echo " make run Run server locally (requires .env)"
@echo ""
@echo "Docker:"
@echo " make docker-build Build Docker image"
@echo " make docker-up Start containers"
@echo " make docker-down Stop containers"
@echo " make docker-logs View container logs"
@echo " make docker-restart Restart MCP container"
@echo ""
install: install:
pip install -r requirements.txt uv sync
install-dev:
pip install -r requirements-dev.txt
pre-commit install
test:
pytest tests/ -v --cov=aegis_gitea_mcp --cov-report=html --cov-report=term --cov-fail-under=80
lint: lint:
ruff check src/ tests/ uv run ruff check .
ruff format --check src/ tests/ uv run ruff format --check .
black --check src/ tests/ uv run mypy src
mypy src/
format: format:
black src/ tests/ uv run ruff format .
ruff check --fix src/ tests/ uv run ruff check --fix .
clean: test:
rm -rf build/ dist/ *.egg-info .pytest_cache/ .coverage htmlcov/ uv run pytest
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
build: test-integration:
python -m build AEGIS_INTEGRATION=1 uv run pytest -m integration
run: run:
python3 -m aegis_gitea_mcp.server uv run aegis-gitea-mcp serve
docker-build:
docker-compose build
docker-up:
docker-compose --profile prod up -d
docker-down:
docker-compose down
docker-logs:
docker-compose logs -f aegis-mcp
docker-restart:
docker-compose restart aegis-mcp
docker-shell:
docker-compose exec aegis-mcp /bin/bash
generate-key:
python3 scripts/generate_api_key.py
rotate-key:
python3 scripts/rotate_api_key.py
check-key-age:
python3 scripts/check_key_age.py
validate-audit: validate-audit:
python3 scripts/validate_audit_log.py uv run python scripts/validate_audit_log.py
docker-build:
docker build -f docker/Dockerfile -t aegis-gitea-mcp:dev .
clean:
rm -rf build/ dist/ *.egg-info .pytest_cache/ .mypy_cache/ .ruff_cache/ .coverage htmlcov/
find . -type d -name __pycache__ -not -path "./.venv/*" -exec rm -rf {} +
-31
View File
@@ -1,31 +0,0 @@
# PLAN — local stdio package + safe full-API coverage
Branch: `feat/local-package-and-full-coverage` (from `dev`). All PRs target `dev`.
Flow: HEAD -> custom branch -> dev -> main. Never push directly to dev/main.
Baseline (recorded Phase 0): 284 passed, 1 skipped, coverage 84.04%, threshold 80%.
## Phase checklist
- [x] Phase 0 — Branch from dev, baseline recorded, PLAN.md committed.
- [x] Phase 1 — Extract transport-agnostic core + shared tool registry (+ boundary test).
- [x] Phase 2 — stdio adapter (`stdio_app.py`) + packaging (core + `[server]` extra, 0.2.0).
- [x] Phase 3 — Resource-type-aware authorization (fail-closed).
- [x] Phase 4 — gitea_request classifier + known-path gate (unknown path => deny).
- [x] Phase 5 — Tests: authz matrix, write-mode bypass, classifier, stdio adapter, boundary.
- [x] Phase 6 — Docs & README (local vs server quickstart, authz model, packaging, CLAUDE/AGENTS).
- [ ] Phase 7 — `.gitea/workflows/publish.yml` (uv build + publish to Gitea registry on tag).
- [ ] Phase 8 — Verify green + coverage >= baseline, `uv build`, push, open PR into dev.
Note: version bumped to 0.2.0 (the app already reported 0.2.0; pyproject was 0.1.0).
TODO(authz): make `list_organizations` user-scoped (`/users/{login}/orgs`) so it can
be allowed rather than denied in service-PAT mode.
## Key deltas found during orientation
- No single tool registry today: definitions in `mcp_protocol.AVAILABLE_TOOLS`,
handlers in `server.TOOL_HANDLERS`. Phase 1 unifies them.
- `tools/raw_tools.py` imports `fastapi.HTTPException` — the only core->web import to break.
- Current authz is repo-only and lives in `server._verify_user_repository_access`.
- stdio mode must run with `AUTH_ENABLED=false` (config otherwise requires MCP_API_KEYS).
- `AGENTS.md` absent at root though CLAUDE.md cites it; create it from the contract.
+59 -58
View File
@@ -1,14 +1,14 @@
[project] [project]
name = "aegis-gitea-mcp" name = "aegis-gitea-mcp"
version = "0.2.0" version = "1.0.0.dev0"
description = "Security-first MCP server for controlled AI access to self-hosted Gitea (local stdio + public HTTP/OAuth)" description = "MCP server exposing the complete Gitea API over stdio and streamable HTTP, with admin-token and per-user OAuth modes"
authors = [ authors = [
{name = "AegisGitea MCP Contributors"} {name = "AegisGitea MCP Contributors"}
] ]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
license = {text = "MIT"} license = {text = "MIT"}
keywords = ["mcp", "gitea", "ai", "security", "audit"] keywords = ["mcp", "gitea", "ai", "audit"]
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Intended Audience :: Developers", "Intended Audience :: Developers",
@@ -17,46 +17,22 @@ classifiers = [
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
] ]
# Core (default install) powers the local stdio transport. It deliberately
# excludes the web/OAuth stack so `uvx aegis-gitea-mcp` stays light; the HTTP
# server pulls those in via the [server] extra.
dependencies = [ dependencies = [
"httpx>=0.26.0", "mcp>=2.3,<3",
"pydantic>=2.5.0", "httpx>=0.27",
"pydantic-settings>=2.1.0", "pydantic>=2.7",
"PyYAML>=6.0.1", # NoDecode (comma-separated list env vars) needs pydantic-settings 2.7.
"python-dotenv>=1.0.0", "pydantic-settings>=2.7",
"structlog>=24.1.0", "jsonschema>=4.20",
"mcp>=1.2.0", "cryptography>=42",
] "uvicorn>=0.31",
[project.optional-dependencies]
# The public HTTP/OAuth server (aegis-gitea-mcp-server) needs the web stack.
server = [
"fastapi>=0.109.0",
"uvicorn[standard]>=0.27.0",
"PyJWT[crypto]>=2.9.0",
"python-multipart>=0.0.9",
]
dev = [
"pytest>=7.4.0",
"pytest-asyncio>=0.23.0",
"pytest-cov>=4.1.0",
"pytest-httpx>=0.28.0",
"black>=24.1.0",
"ruff>=0.1.0",
"mypy>=1.8.0",
"pre-commit>=3.6.0",
] ]
[project.scripts] [project.scripts]
# Local stdio MCP server (default install, no web stack required). aegis-gitea-mcp = "aegis_gitea_mcp.cli:main"
aegis-gitea-mcp = "aegis_gitea_mcp.stdio_app:main"
# Public HTTP/OAuth server; requires the [server] extra. The entry point guards
# against a missing web stack with an actionable message.
aegis-gitea-mcp-server = "aegis_gitea_mcp.server_entry:main"
[project.urls] [project.urls]
Homepage = "https://git.hiddenden.cafe/Hiddenden/AegisGitea-MCP" Homepage = "https://git.hiddenden.cafe/Hiddenden/AegisGitea-MCP"
@@ -64,42 +40,64 @@ Documentation = "https://git.hiddenden.cafe/Hiddenden/AegisGitea-MCP/src/branch/
Repository = "https://git.hiddenden.cafe/Hiddenden/AegisGitea-MCP.git" Repository = "https://git.hiddenden.cafe/Hiddenden/AegisGitea-MCP.git"
Issues = "https://git.hiddenden.cafe/Hiddenden/AegisGitea-MCP/issues" Issues = "https://git.hiddenden.cafe/Hiddenden/AegisGitea-MCP/issues"
[dependency-groups]
dev = [
"pytest>=8",
"pytest-asyncio>=0.23",
"pytest-cov>=5",
"pytest-httpx>=0.30",
"ruff>=0.6",
"mypy>=1.10",
"types-jsonschema",
"pip-audit>=2.7",
]
[build-system] [build-system]
requires = ["setuptools>=68.0.0", "wheel"] requires = ["hatchling"]
build-backend = "setuptools.build_meta" build-backend = "hatchling.build"
[tool.setuptools] [tool.hatch.build.targets.wheel]
package-dir = {"" = "src"} packages = ["src/aegis_gitea_mcp"]
[tool.setuptools.packages.find] [tool.hatch.build.targets.sdist]
where = ["src"] include = ["src/aegis_gitea_mcp", "README.md", "LICENSE", "pyproject.toml"]
[tool.black]
line-length = 100
target-version = ['py310', 'py311', 'py312']
include = '\.pyi?$'
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100
target-version = "py310" target-version = "py310"
src = ["src", "tests"]
extend-exclude = ["docs", "spec"]
[tool.ruff.lint] [tool.ruff.lint]
select = [ select = [
"E", # pycodestyle errors "E", # pycodestyle errors
"W", # pycodestyle warnings "W", # pycodestyle warnings
"F", # pyflakes "F", # pyflakes
"I", # isort "I", # isort
"B", # flake8-bugbear "B", # flake8-bugbear
"C4", # flake8-comprehensions "C4", # flake8-comprehensions
"UP", # pyupgrade "UP", # pyupgrade
"S", # flake8-bandit
"SIM", # flake8-simplify
"RUF", # ruff-specific
] ]
ignore = [ ignore = [
"E501", # line too long (handled by black) "E501", # line length is handled by ruff format; long strings and URLs are fine
"B008", # do not perform function calls in argument defaults "B008", # function calls in argument defaults (pydantic Field style)
] ]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] "__init__.py" = ["F401"]
"tests/**" = [
"S101", # assert is the point of tests
"S105", # hardcoded fake tokens in fixtures
"S106", # hardcoded fake tokens in fixtures
"S108", # fixture paths
]
[tool.ruff.format]
quote-style = "double"
line-ending = "lf"
[tool.mypy] [tool.mypy]
python_version = "3.10" python_version = "3.10"
@@ -119,11 +117,14 @@ module = "tests.*"
disallow_untyped_defs = false disallow_untyped_defs = false
[tool.pytest.ini_options] [tool.pytest.ini_options]
minversion = "7.0" minversion = "8.0"
addopts = "-ra -q --strict-markers --cov=aegis_gitea_mcp --cov-report=term-missing --cov-fail-under=80" addopts = "-ra -q --strict-markers --cov=aegis_gitea_mcp --cov-report=term-missing --cov-fail-under=80"
testpaths = ["tests"] testpaths = ["tests"]
pythonpath = ["src"] pythonpath = ["src"]
asyncio_mode = "auto" asyncio_mode = "auto"
markers = [
"integration: needs a live Gitea in Docker (opt-in via AEGIS_INTEGRATION=1)",
]
[tool.coverage.run] [tool.coverage.run]
source = ["src"] source = ["src"]
-10
View File
@@ -1,10 +0,0 @@
-r requirements.txt
pytest>=7.4.0
pytest-asyncio>=0.23.0
pytest-cov>=4.1.0
pytest-httpx>=0.28.0
black>=24.1.0
ruff>=0.1.0
mypy>=1.8.0
pre-commit>=3.6.0
-11
View File
@@ -1,11 +0,0 @@
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
httpx>=0.26.0
pydantic>=2.5.0
pydantic-settings>=2.1.0
PyYAML>=6.0.1
python-dotenv>=1.0.0
python-multipart>=0.0.9
structlog>=24.1.0
PyJWT[crypto]>=2.9.0
mcp>=1.2.0
-79
View File
@@ -1,79 +0,0 @@
#!/bin/bash
# Test runner script for AegisGitea MCP
set -e
echo "========================================="
echo "AegisGitea MCP - Test Suite"
echo "========================================="
echo ""
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check if virtual environment exists
if [ ! -d "venv" ]; then
echo -e "${YELLOW}Virtual environment not found. Creating...${NC}"
python3 -m venv venv
fi
# Activate virtual environment
source venv/bin/activate
# Install dependencies
echo -e "${YELLOW}Installing dependencies...${NC}"
pip install -q -r requirements-dev.txt
echo ""
echo "========================================="
echo "Running Tests"
echo "========================================="
echo ""
# Run tests with coverage
pytest tests/ \
-v \
--cov=aegis_gitea_mcp \
--cov-report=term-missing \
--cov-report=html \
--tb=short
TEST_EXIT_CODE=$?
echo ""
echo "========================================="
echo "Test Summary"
echo "========================================="
echo ""
if [ $TEST_EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}✓ All tests passed!${NC}"
echo ""
echo "Coverage report generated in htmlcov/"
echo "Open htmlcov/index.html in your browser to view detailed coverage."
else
echo -e "${RED}✗ Some tests failed${NC}"
echo ""
echo "Please review the errors above and fix them."
exit 1
fi
echo ""
echo "========================================="
echo "Running Linters"
echo "========================================="
echo ""
# Run ruff
echo "Running ruff..."
ruff check src/ tests/ || true
# Run mypy
echo "Running mypy..."
mypy src/ || true
echo ""
echo -e "${GREEN}Test suite complete!${NC}"
Generated
+2285
View File
File diff suppressed because it is too large. Load diff