Files
AegisGitea-MCP/docs/audit.md
T
Latte 8b7bb7205c docs: phase 0 audit, chunk plan, ADRs and pinned Gitea 28.1.0 swagger
Inventory and gap analysis of the 0.2.0 codebase against the full Gitea API
and the official gitea-mcp toolset, the B1..B11 + security chunk plan, ADRs
0001-0006 for the full-access rebuild, SDK/OAuth research notes, and the
instance swagger pinned for tool generation. The audit-logging doc moves to
docs/audit-logging.md.
2026-10-14 15:34:48 +02:00

19 KiB

Codebase audit and chunk plan

Date: 2026-10-14. Baseline: dev at 2b99644 (package version 0.2.0). Target: a complete, full-access Gitea MCP server covering the entire instance API (Gitea 28.1.0 swagger, pinned at spec/gitea-swagger.v1.json), served over stdio and streamable HTTP, with admin-token and per-user OAuth modes.

Supporting research (SDK signatures, Gitea OAuth facts, client requirements, all with source URLs): research/sdk-oauth-gitea.md. Design decisions: adr/.

1. Inventory of the current codebase

1.1 MCP tools (44)

28 read tools, 15 write tools, 1 hybrid escape hatch. Definitions live in mcp_protocol.py (AVAILABLE_TOOLS), handlers in registry.py and tools/*, HTTP calls in gitea_client.py.

Group Tools Gitea endpoints
Repository read list_repositories, get_repository_info, get_file_tree, get_file_contents, search_code, list_commits, get_commit_diff, compare_refs, list_branches, get_branch, list_tags, get_repo_languages, list_repo_topics, get_commit_status /user/repos, /repos/search, R, R/git/trees, R/contents, R/search (does not exist in the swagger), R/commits, R/git/commits, R/compare, R/branches, R/tags, R/languages, R/topics, R/commits/{sha}/status
Issues / PRs read list_issues, get_issue, list_issue_comments, list_labels, list_milestones, list_pull_requests, get_pull_request, list_pull_request_files, list_pull_request_commits R/issues*, R/labels, R/milestones, R/pulls*
Releases / orgs read list_releases, get_release, get_latest_release, list_organizations, list_org_repositories R/releases*, /user/orgs, /orgs/{org}/repos
Write create_issue, update_issue, create_issue_comment, create_pr_comment, edit_issue_comment, add_labels, remove_labels, assign_issue, create_label, update_label, create_milestone, create_pull_request, create_release, edit_release, create_branch R/issues*, R/labels*, R/milestones, R/pulls, R/releases*, R/branches
Escape hatch gitea_request any known prefix, admin/credential segments denied

(R = /repos/{owner}/{repo}.)

1.2 Transports

  • stdio (stdio_app.py) is the only part using the mcp SDK, via the v1 low-level decorators (@server.list_tools()). The dependency is unbounded (mcp>=1.2.0), resolves to mcp==2.3.0, and under 2.x build_server() raises AttributeError. The stdio adapter is broken today.
  • HTTP (server.py, FastAPI) is hand-rolled: a minimal JSON-RPC dispatcher on POST /mcp with a fixed protocolVersion: "2024-11-05", no session id, no streamed responses, errors as JSON-RPC errors rather than isError results. GET /mcp is a heartbeat-only SSE stream. The README calls this "Streamable HTTP"; it is not a conforming implementation.
  • Extra routes: /, /health, /metrics (unauthenticated), OAuth metadata and proxy routes, /register (public, unthrottled, writes to disk on every call), /automation/*, /mcp/tools (public), /mcp/tool/call, plus FastAPI's /docs, /redoc, /openapi.json (public).

1.3 Configuration

config.py (pydantic-settings) has about 40 variables. Security defaults: WRITE_MODE=false, RAW_API_ALLOW_SENSITIVE=false, MCP_HOST=127.0.0.1 (only the literal 0.0.0.0 is blocked), EXPOSE_ERROR_DETAILS=false, SECRET_DETECTION_MODE=mask. AUTH_ENABLED, MCP_API_KEYS and OAUTH_MODE only affect config validation; runtime auth is always Gitea-OAuth validation. AUTOMATION_SCHEDULER_ENABLED is never read.

1.4 Authentication

  • auth.py (static API keys) is dead at runtime: MCP_API_KEYS is never compared against anything. The key-management scripts manage keys the server ignores.
  • oauth.py validates bearer tokens as Gitea OAuth tokens (JWT via JWKS, else /login/oauth/userinfo).
  • oauth_flow.py proxies the authorization code flow (PKCE S256, HMAC-signed state) and runs a small DCR registry on disk. It does not check the redirect_uri against the registered client's own redirect URIs.
  • Every call runs on a single service PAT (GITEA_TOKEN); per-user permissions are emulated with a collaborator-permission probe (server.py) and a resource-type gate (authz.py). The user's own Gitea token is never used for API calls.

1.5 Supporting modules

Module Role Fate under full access
audit.py Hash-chained JSONL audit log Keep. About 38 client methods log only a "pending" entry; the new dispatcher logs one complete event per call.
security.py Regex secret detection, mask/block Keep as output safety net.
response_limits.py Item and character caps Keep.
request_context.py, errors.py, logging_utils.py, cache.py Context vars, ToolError, JSON logging, TTL cache Keep or fold into the new core.
policy.py YAML allow/deny plus the write gate Retire (ADR 0006). Replaced by READ_ONLY, TOOLSETS, DISABLED_TOOLS.
authz.py Fail-closed emulation of per-user rights on a service PAT Retire. Per-user mode now uses the user's own Gitea token, so Gitea enforces permissions itself.
auth.py, oauth.py, oauth_flow.py Dead API keys, Gitea token validation, hand-rolled OAuth proxy Replace with the SDK auth stack (ADR 0004).
server.py, server_entry.py, mcp_protocol.py, registry.py, tools/* FastAPI adapter, custom protocol, 44 hand-written tools Replace with the generated catalog and the SDK server (ADR 0002, 0003).
automation.py Webhook ingest, placeholder jobs (45% covered) Remove; out of scope for an API bridge.
observability.py, rate_limit.py Unauthenticated /metrics, in-memory rate limits Rate limiting kept for HTTP; /metrics removed (not in scope).

1.6 Tests

351 tests in 21 files, Gitea always mocked with hand-made stubs (pytest-httpx is installed but unused). Full run on this machine: 336 passed, 15 failed, coverage 80.10%. Failures: 12 environment-dependent (default paths under /var/log and /var/lib, one test module shadows the reset_globals fixture), 3 real (mcp 2.x API break in the stdio tests). No live-Gitea integration tests, no MCP transport tests over HTTP.

1.7 CI (.gitea/workflows/)

lint.yml (ruff, black, mypy), test.yml (pytest at 80%, uv build package check), docker.yml (lint + test again, build, import smoke test, push to the Gitea registry on main/dev), publish.yml (lint + test again, uv publish to the Gitea PyPI registry on push to dev/main). Python 3.12 only, unpinned pip install -r requirements-dev.txt. No secret scan, dependency audit, security lint, integration tests or docs drift check. docker.yml runs docker image prune and docker builder prune on a shared host.

1.8 Dependencies and packaging

pyproject.toml core: httpx, pydantic, pydantic-settings, PyYAML, python-dotenv, structlog (never imported), mcp (unbounded). [server] extra: fastapi, uvicorn, PyJWT, python-multipart. Dev: pytest stack, black, ruff, mypy, pre-commit. No lockfile. The Dockerfile pip-installs requirements.txt without installing the package itself and sets a stray NODE_ENV. The compose file keeps MCP_HOST=127.0.0.1 inside the container, so the published port is unreachable unless the bind is widened.

1.9 Dead code and stale docs

  • Code: auth.py, AUTOMATION_SCHEDULER_ENABLED, observability.ToolTiming, arguments.RAW_API_METHODS, raw_method_is_write, most of tools/__init__.py re-exports, structlog, pytest-httpx, the API-key scripts. search_code targets a non-existent route. list_repositories is capped at 50 results.
  • Docs: PLAN.md phases 7 and 8 unchecked though done; CLAUDE.md says publish runs on a v* tag (it runs on push to dev/main); API keys are documented as working in four docs; write-mode.md, roadmap.md and todo.md claim merge and content writes are impossible while gitea_request can do them; the transport is described as streamable HTTP.

2. Gap analysis

2.1 Against the instance API (Gitea 28.1.0, 537 operations)

The existing tools cover 44 operations (about 8%). Every domain below is missing entirely or partially. Counts use the domain split of ADR 0003.

Domain Operations Covered today Missing highlights
repo 120 ~17 CRUD, contents writes, forks, mirrors, transfer, collaborators, topics writes, tags/refs writes, archives, LFS, git notes, templates, avatars, push mirrors, branch protection
issue 90 ~15 comments delete, reactions, pins, subscriptions, time tracking, stopwatches, dependencies, attachments, label/milestone delete, issue templates, search
user 73 0 profile, emails, SSH/GPG keys, follow, blocks, tokens, OAuth apps, starred/watched, settings, searches
actions 70 0 workflow runs/jobs/logs/artifacts, dispatch, repo/org/user secrets and variables, runners and registration tokens
org 36 2 CRUD, members, public members, blocks, labels, avatars, activity
admin 33 0 users (create/edit/delete/rename, keys, orgs, repos, badges), orgs, emails, cron list/run, system hooks, unadopted repos, runners, packages
pull 26 4 update, merge, reviews (create/submit/dismiss/delete), review comments, requested reviewers, diff/patch, update branch
hook 20 0 repo, org, user and system webhooks
misc 19 0 version, settings, markdown/markup, licenses, gitignore, labels templates, nodeinfo, signing keys, activitypub
team 15 0 team CRUD, members, repos
release 13 4 delete, by tag, attachments
package 9 0 list, get, delete, files, link/unlink
notification 7 0 list, mark read, threads
wiki 6 0 pages CRUD, revisions

Swagger operations that the API does not offer and therefore cannot get a tool: queue management, instance configuration writes, and instance-wide OAuth2 applications (UI only). This is recorded honestly in the README rather than faked with UI scraping.

2.2 Against the official gitea/gitea-mcp (v1.8.1, 59 tools)

gitea-mcp consolidates operations into *_read/*_write tools with a method enum. Every one of its methods maps to a swagger operation, so one tool per operation is a strict superset. gitea-mcp features we must match or exceed: read-only mode, toolset and tool-name filters, readOnlyHint/destructiveHint annotations, a health endpoint for HTTP. Features it lacks that we add: Sudo, admin domain, hooks, packages beyond delete, users/orgs administration, per-user OAuth with dynamic client registration (needed for Mistral Le Chat).

2.3 Platform gaps

  • Conforming streamable HTTP transport (current one is hand-rolled).
  • Working stdio on mcp 2.x.
  • Static bearer auth for the MCP endpoint (the existing API keys are dead).
  • Per-user OAuth where calls actually run under the user's Gitea identity.
  • Setup command, live-Gitea integration tests, MCP-level smoke tests, docs drift check, security scanning, gitleaks.
  • uv lockfile, ruff format instead of black, coverage at 90%.

3. Chunk plan

Every chunk ends with: tests green, ruff check, ruff format --check, mypy clean, checker verdict (acceptance criteria + API docs + security regression review), conventional commit on feat/full-api-coverage, PR into dev.

Dependency order: B1 first; B2, B3, B6, B7 in parallel; B4; then B5, B8, B9, B10, B11 in parallel; then the security phase (S1-S3).

Chunk Scope Acceptance criteria
B1 Foundation uv + uv.lock, mcp>=2.3,<3, ruff format (black removed), ruff S rules, new contract in AGENTS.md/CLAUDE.md, ADRs, delete legacy modules and their tests, interface skeletons (catalog/model.py, identity.py, new config.py). uv run pytest green on the remaining modules, lint clean, no import of removed modules anywhere.
B2 Catalog scripts/generate_catalog.py builds catalog/tools.json from the pinned swagger: names, domains, JSON input schemas (additionalProperties: false, owner/repo patterns), annotations, content types. Loader in catalog/__init__.py. --check mode. All 537 operations present, names unique, match ^gitea_[a-z0-9_]+$, at most 64 chars; schemas validate as Draft 2020-12; unit tests for naming, schema building, ref resolution.
B3 Executor gitea/executor.py: path templating with encoding, query/JSON/multipart (base64 files), Sudo header, pagination metadata (Link, X-Total-Count), text and binary responses, error mapping with sanitized messages, response limits, secret masking, untrusted-content envelope with identity. pytest-httpx unit tests for each branch, tokens never present in any error or result.
B4 Server core + transports Low-level SDK Server built from the catalog: list_tools with TOOLSETS/READ_ONLY/DISABLED_TOOLS, call_tool with jsonschema validation, audit, executor, is_error results. stdio entry, streamable HTTP app with static bearer verifier, /health, bind fail-closed, gitea_api_request raw tool. In-memory client tests for list/call/validation/errors; HTTP tests for 401 without/with bad token, refusal to start on a non-loopback bind without auth.
B5 Per-user OAuth OAuthAuthorizationServerProvider proxying Gitea (DCR on, PKCE to Gitea), /oauth/callback, encrypted token store (file, 0600), refresh handling, combined verifier (static bearer = admin identity, OAuth token = user identity), identity in result metadata. Unit tests with mocked Gitea OAuth endpoints: full code flow, refresh, expiry, revoke, store permissions and encryption, tokens never logged.
B6 Setup command aegis-gitea-mcp setup: reachability, admin and token-scope check (GET /user, GET /token), OAuth app registration via POST /user/applications/oauth2, writes .env from .env.example (0600), prints client configs and a compose snippet, never echoes secrets. Unit tests with mocked Gitea; integration test against the CI instance (in B8).
B7 Integration harness Pytest fixtures that start gitea/gitea:28.1.0 (digest-pinned) with unique container/network names and guaranteed cleanup, seed an admin user and a full-scope token, plus fake-data seed helpers. Marker integration, skipped unless enabled. Harness starts, seeds, tears down on this host and in CI; no bind mounts, no prune.
B8 Integration suite Scenario tests (org, repo, issue, PR, review, merge, release, secret, variable, webhook, cleanup) and a sweep that calls every tool once with fixture-derived arguments, asserting either success or a documented expected status. Per-user mode via minted user tokens. Setup command against the live instance. Every registered tool exercised; write tools have happy and failure paths; non-admin user gets Gitea's own 403 on admin tools; identity visible.
B9 MCP smoke tests Real SDK clients over stdio (subprocess) and streamable HTTP (uvicorn, and the built image) listing tools and calling a read and a write tool. Green locally and in CI.
B10 CI .gitea/workflows/: gitleaks, lint, test-unit, test-integration, test-mcp, build-and-push (SHA tags, digest output), docs-check, security; strict per-run cleanup, no prune. Dockerfile on uv, non-root, no secrets in layers. All eight jobs green on the PR.
B11 Docs README rewrite (honest scope, setup with time estimate, stdio and HTTP client configs for Claude, Claude Code, Mistral, local tools, token scopes, untrusted-content notice), generated tool inventory docs/tools.md with endpoint mapping, scripts/check_docs.py drift check, deployment and reverse-proxy guide. Docs check green; no stale claims left from section 1.9.
S1 Threat model docs/security.md: assets, trust boundaries, threats, mitigations. Reviewed by the checker.
S2 Fixes and negative tests Security test suite: auth negatives, bind refusal, token redaction in logs/errors/results, secrets never echoed, malformed input rejected, base URL validation. All green.
S3 Scans pip-audit, ruff S/bandit, gitleaks over history; findings fixed or waived with reasons in docs/security.md. Scans clean or waived.

4. Risks and open questions

Owner decisions taken before execution (2026-10-14):

  • Security contract reversed. All capabilities on by default; the owner restricts via configuration and token choice (ADR 0001).
  • Framework. Rebuild on the official SDK (ADR 0002).
  • Commit signing. Disabled by the owner on dev; commits are unsigned.
  • Tool count. One tool per swagger operation (537 plus the raw request tool), all on by default, with a toolset filter (ADR 0003).

Open risks:

  1. Tool-list size. 538 tools is a large tools/list payload. Claude Code handles it with tool search; claude.ai and Mistral Le Chat may degrade or truncate. Mitigation: TOOLSETS, READ_ONLY and DISABLED_TOOLS; the README gives a recommended slim profile for Le Chat. Needs owner testing against the real clients.
  2. "FastMCP" no longer exists. In mcp 2.x FastMCP was renamed MCPServer, and it cannot register tools from raw JSON schemas. We use the SDK's low-level Server, which is the same package and transport stack (ADR 0002).
  3. Gitea has no dynamic client registration. Per-user OAuth therefore needs our server to be the OAuth authorization server for MCP clients and a Gitea OAuth client upstream. The server holds per-user Gitea tokens, which makes the token store a high-value asset (encrypted at rest, ADR 0004).
  4. Gitea scope fallback. Gitea grants full access for unknown scope strings. The proxy sends a fixed, validated scope list.
  5. Literal "every endpoint gets an integration test". Some operations need state that is costly to build in CI: Actions runs need a registered runner executing a job, push mirrors and migrations need a reachable remote, package endpoints need an uploaded package, unadopted repositories need files on the Gitea volume. The sweep exercises every tool; where a full happy path is not reproducible in CI, the test asserts the documented Gitea response (for example 404 for a missing artifact) and the README marks it.
  6. Shared CI runner. Integration and MCP jobs need Docker from the job container. Per-run unique names and always() cleanup; the existing docker image prune / builder prune steps are removed.
  7. Breaking change. All tool names change (gitea_<domain>_<action>), the FastAPI server, YAML policy and API keys are removed. Version goes to 1.0.0.
  8. Admin coverage limits. The API has no queue management or instance configuration write endpoints, and no instance-wide OAuth app endpoint; setup registers the OAuth app as a user application owned by the admin.