Files
Gitea-Tools/README.md
T
sysadmin c3c48fb7c2 feat: audit-log Gitea MCP mutating actions with profile metadata (#18)
Add durable, opt-in audit logging for every mutating Gitea MCP action so an
operator can see which execution profile and authenticated Gitea user
performed (or was blocked from / failed) each mutation.

- New gitea_audit.py: pure, no-network module — recursive secret redaction
  (token/password/authorization keys; token/Basic/Bearer value runs),
  build_event (timestamp, action, result, profile, audit label, authenticated
  username, repo, issue/PR, target branch, head SHA, redacted request
  metadata), and an append-only JSON Lines sink.
- mcp_server.py: _audit helper + _audited context manager (simple mutations)
  and an _audit_pr_result decorator (gated review/merge tools, reading their
  own result dict) wired into create_issue, create_pr, edit_pr, close_issue,
  commit_files, delete_branch, create_label, set_issue_labels, mark_issue
  (label/unlabel), gitea_submit_pr_review, and gitea_merge_pr.
- Outcomes recorded as allowed/blocked/failed/succeeded; blocked and failed
  eligibility checks are logged, not just successes.

Off by default: records are written only when GITEA_AUDIT_LOG is set. When it
is unset every audit path short-circuits — no records, no extra API calls — so
existing tool behaviour and API call sequences are unchanged. Auditing never
raises; sink writes are best-effort. Tokens are never written.

Docs: README env table + audit note, .env.example placeholder.
Tests: tests/test_audit.py (19 cases) — redaction, event build, sink writes,
per-tool success/failure/blocked records, secret-free output, off-by-default
no-op, and audit-failure-never-breaks-action.

Closes #18

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:20:51 -04:00

291 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Gitea Tools
A collection of Python scripts and an MCP server to automate interactions with Gitea instances.
## Supported Instances
| Remote | Host | Org / Repo |
|----------------|----------------------------|-------------------------------------|
| `dadeschools` | `gitea.dadeschools.net` | `Contractor / Timesheet` |
| `prgs` | `gitea.prgs.cc` | `Scaled-Tech-Consulting / Timesheet`|
## Authentication
Authentication is configured via environment variables or a local `.env` file in the repository root (uses `python-dotenv`).
Create a `.env` file in the project root:
```bash
# Option A: Gitea Personal Access Tokens (Recommended)
GITEA_TOKEN_DADESCHOOLS="your_token_here"
GITEA_TOKEN_PRGS="your_token_here"
# Option B: Gitea Username & Password (fallback)
GITEA_USER_DADESCHOOLS="username"
GITEA_PASS_DADESCHOOLS="password"
GITEA_USER_PRGS="username"
GITEA_PASS_PRGS="password"
# Optional: Fallback to macOS Keychain (via git credential fill)
# GITEA_USE_KEYCHAIN=1
```
## MCP Server (Recommended)
The Gitea-Tools MCP server exposes all functionality as structured tool calls.
Any MCP-compatible agent (Antigravity, Claude Code, etc.) can call these tools natively.
### Available Tools
| Tool | Description |
|------|-------------|
| `gitea_create_issue` | Create an issue with title, body, remote |
| `gitea_create_pr` | Open a pull request with title, head, base |
| `gitea_edit_pr` | Edit details of an existing pull request |
| `gitea_list_prs` | List pull requests with state/remote |
| `gitea_view_pr` | Get full details of a single pull request |
| `gitea_merge_pr` | Gated merge: merge/squash/rebase only after identity+profile+eligibility gates pass, explicit `confirmation="MERGE PR <n>"`, optional head-SHA and changed-files pinning (no self-merge, no force) |
| `gitea_review_pr` | Legacy wrapper for `gitea_submit_pr_review` (merging disabled) |
| `gitea_delete_branch` | Delete a remote branch |
| `gitea_close_issue` | Close an issue by number |
| `gitea_list_issues` | List issues with state/label filters |
| `gitea_view_issue` | Get full details of a single issue |
| `gitea_whoami` | Read-only: identify the authenticated Gitea account (safe metadata only) |
| `gitea_get_profile` | Read-only: describe the active runtime execution profile (safe metadata only) |
| `gitea_check_pr_eligibility` | Read-only: check if the current identity/profile may review/approve/request_changes/merge a PR |
| `gitea_submit_pr_review` | Gated review mutation: comment/approve/request_changes, only after identity+profile+eligibility gates pass (no merge, no self-approval) |
| `gitea_mark_issue` | Claim/release an issue (start/done) |
| `gitea_list_labels` | List all available labels in a repository |
| `gitea_create_label` | Create a new label with custom color |
| `gitea_set_issue_labels` | Replace all labels on an issue |
| `gitea_get_file` | Retrieve file content and SHA metadata |
| `gitea_commit_files` | Commit changes to multiple files atomically |
| `gitea_mirror_refs` | Mirror branches + tags between instances |
### Setup
#### 1. Install dependencies
```bash
cd /Users/jasonwalker/Development/Gitea-Tools
python3 -m venv venv # skip if venv already exists
source venv/bin/activate
pip install "mcp[cli]"
```
#### 2. Configure your AI client
The MCP server uses **stdio transport** — each client starts it as a subprocess.
Add the config below to your client, then restart it.
<details>
<summary><strong>Antigravity (Google)</strong></summary>
Add to `~/.gemini/antigravity-ide/mcp_config.json` inside `"mcpServers"`:
```json
"gitea-tools": {
"command": "/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3",
"args": ["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"],
"env": {}
}
```
Restart Antigravity to load the server. Tools appear as lazy-loaded MCP tools
(call via `call_mcp_tool` with `ServerName: "gitea-tools"`).
</details>
<details>
<summary><strong>Claude Code (Anthropic)</strong></summary>
Add to `~/.claude.json` (global) or `.mcp.json` in the project root:
```json
{
"mcpServers": {
"gitea-tools": {
"command": "/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3",
"args": ["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"]
}
}
}
```
Restart Claude Code. Tools appear as `mcp__gitea-tools__gitea_create_issue`, etc.
</details>
<details>
<summary><strong>Any MCP-compatible client</strong></summary>
The server is a standard MCP stdio server. Point your client at:
- **Command:** `/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3`
- **Args:** `["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"]`
- **Transport:** `stdio`
No environment variables needed — auth is handled via macOS keychain.
</details>
<details>
<summary><strong>Runtime profiles (multiple env-configured entries)</strong></summary>
The same server can run as **separate MCP entries**, each authenticating as its
own Gitea token and carrying its own profile name. This keeps roles task-scoped:
*the profile is the role, not the LLM.* Point each entry at a different
gitignored env file.
```json
{
"mcpServers": {
"gitea-tools-reviewer": {
"command": "/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3",
"args": ["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"],
"env": {
"GITEA_PROFILE_NAME": "gitea-reviewer",
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"
}
},
"gitea-tools-merger": {
"command": "/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3",
"args": ["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"],
"env": {
"GITEA_PROFILE_NAME": "gitea-merger",
"GITEA_ALLOWED_OPERATIONS": "read,merge"
}
}
}
}
```
Recognized environment fields (see [`.env.example`](.env.example) for placeholders):
| Variable | Purpose |
|----------|---------|
| `GITEA_TOKEN` | API token for this runtime. Read only by the auth layer; **never** returned, logged, or committed. |
| `GITEA_PROFILE_NAME` | Non-secret label for the running profile (e.g. `gitea-reviewer`). Surfaced by `gitea_whoami`. |
| `GITEA_ALLOWED_OPERATIONS` | Optional, comma-separated operation categories (descriptive metadata only for now). |
| `GITEA_FORBIDDEN_OPERATIONS` | Optional, comma-separated categories this profile must not perform (descriptive). |
| `GITEA_AUDIT_LABEL` | Optional short label for this runtime, for audit purposes. |
| `GITEA_TOKEN_SOURCE` | Optional *name* of the token source (e.g. an env var name). A name only — never the token value. |
| `GITEA_BASE_URL` | Optional informational base URL. |
| `GITEA_AUDIT_LOG` | Optional path to an audit log file. When set, mutating actions append one redacted JSON record each (profile + authenticated user + outcome). Unset ⇒ auditing off (no records, no extra API calls). |
Notes:
- This provides **one token + one profile per process**. It does not implement
multi-token switching inside a single runtime, nor any approve/merge/eligibility
gating — those are later roadmap items (#14#18).
- Profile name and allowed operations are **metadata only**; the token value is
never part of any tool output. `gitea_whoami` returns the profile name, and
`gitea_get_profile` returns the full non-secret profile metadata so a workflow
can inspect which runtime it is talking to before deciding to act.
- See [`docs/gitea-execution-profiles.md`](docs/gitea-execution-profiles.md) for
the full profile model.
- **Audit logging (#18):** mutating actions emit a durable, redacted JSON audit
record — timestamp, action, result (`allowed`/`blocked`/`failed`/`succeeded`),
profile name + audit label, authenticated username, target repo/issue/PR,
branch and head SHA where applicable — when `GITEA_AUDIT_LOG` is set. Auditing
is off by default and never adds API calls or breaks the action when off.
See [`gitea_audit.py`](gitea_audit.py).
</details>
<details>
<summary><strong>Codex / non-MCP tools</strong></summary>
OpenAI Codex and other tools that don't support MCP can use the CLI scripts
directly. See the [CLI Scripts](#cli-scripts) section below.
```bash
# Example: Codex can shell out to the scripts
python3 /Users/jasonwalker/Development/Gitea-Tools/create_issue.py \
--remote prgs --title "Bug report" --body "Details here"
```
</details>
## CLI Scripts
The MCP tools can also be used as standalone CLI scripts:
| Script | Description |
|---------------------|--------------------------------------------------------------------|
| `create_issue.py` | Create an issue (`--remote`, `--title`, `--body`, `--body-file`) |
| `create_pr.py` | Open a Pull Request (`--remote`, `--title`, `--head`, `--base`) |
| `edit_pr.py` | Edit a Pull Request (`--title`, `--body`, `--body-file`, etc.) |
| `review_pr.py` | Review/sign-off on a pull request (`--merge` is disabled — fails closed; merge only via gated `gitea_merge_pr`) |
| `close_issue.py` | Close a specific issue |
| `mark_issue.py` | Claim/release an issue via `status:in-progress` label |
| `manage_labels.py` | Create label set and apply label mappings (`--dry` to preview) |
| `mirror_refs.sh` | Mirror branches + tags between dadeschools ⇄ prgs |
### Quick Examples
```bash
# Create an issue
./create_issue.py --title "Fix PDF output" --body "Blank on Safari"
# Create an issue on the prgs instance
./create_issue.py --remote prgs --title "Add tests" --body-file description.md
# Create a PR
./create_pr.py --title "feat: add validation" --head feat/validation --body "Closes #12"
# Edit a PR's description or title
./edit_pr.py 155 --body "Updated description wording"
# Review and approve a PR (review only — CLI merge is disabled; use the
# gated gitea_merge_pr MCP workflow to merge)
./review_pr.py --pr-number 12 --event APPROVE --body "Approved"
# Close issue #5
./close_issue.py 5
# Claim an issue before working on it
./mark_issue.py 10 start
# Release when done
./mark_issue.py 10 done
# Mirror refs (dry-run by default)
./mirror_refs.sh
# Actually push the refs
./mirror_refs.sh --apply
```
Use `--help` on any Python script or shell script for full usage details.
## Architecture
```
gitea_auth.py ← shared auth & API helpers (get_credentials, api_request)
mcp_server.py ← MCP server (FastMCP, stdio transport)
create_issue.py ← CLI: create issues
create_pr.py ← CLI: create PRs
edit_pr.py ← CLI: edit PRs
review_pr.py ← CLI: review PRs
manage_labels.py ← CLI: label management
close_issue.py ← CLI: close issues
mark_issue.py ← CLI: claim/release issues
mirror_refs.sh ← CLI: ref mirroring
```
## Tests
```bash
# Run with the venv (includes MCP SDK)
source venv/bin/activate
python3 -m pytest tests/ -v
```
| Test file | Covers |
|--------------------------|---------------------------------------------------------|
| `test_mcp_server.py` | All 7 MCP tools: create, list, view, close, mark, PR, mirror |
| `test_create_issue.py` | CLI arg parsing, remote resolution, payload, auth, errors |
| `test_create_pr.py` | CLI arg parsing, remote resolution, payload, auth, errors |
| `test_credentials.py` | `get_credentials()`, `get_auth_header()`, `repo_api_url()` |
| `test_manage_labels.py` | Label create/skip, dry run, mapping, constant validation |
| `test_python_cli.py` | `close_issue.py` + `mark_issue.py` CLI validation |
| `test_mirror_refs.py` | Flags, safety defaults, local integration tests |
All tests mock network and keychain access — no real API calls are made.