Refactors AegisGitea-MCP into a transport-agnostic core + two thin adapters and ships a local stdio package alongside the existing public HTTP/OAuth server, with safe full-API coverage and resource-type-aware authorization. The public HTTP/OAuth server behavior is unchanged.
Core / adapter split.registry.py is the single name→handler source of truth consumed by both adapters. Core handlers raise the transport-agnostic errors.ToolError (no more fastapi.HTTPException in the core); the HTTP adapter maps it back to HTTPException. A subprocess boundary test asserts the core imports no fastapi/uvicorn/starlette.
Local stdio package.stdio_app.py (official mcp SDK) runs uvx aegis-gitea-mcp — single-user, PAT-owner identity, no OAuth. It resolves the PAT owner (GET /user), pins request context, and runs the same policy + WRITE_MODE + secret sanitization + audit; it skips only the per-user repo probe (the operator is the token owner). Audit log falls back to a per-user state path.
Packaging. Core install (httpx/pydantic/mcp/…) + [server] extra (fastapi/uvicorn/PyJWT/python-multipart). Console scripts aegis-gitea-mcp (stdio) and aegis-gitea-mcp-server (guarded HTTP entry). Version → 0.2.0. uv build produces sdist + wheel.
Authorization model (every decision fails closed)
Enforced in service-PAT mode on top of policy + WRITE_MODE (pure-OAuth mode defers to the user's token at Gitea):
signed-in user must be a Gitea-verified member; unverifiable → deny
user_owned (/users/{x}, /packages/{owner})
allow only if owner == caller or caller is a member org; else deny
user_self (/user, /notifications)
deny — data is the bot's, not the caller's
misc_global
reads allow, writes deny
admin
default-deny; allowed only with RAW_API_ALLOW_SENSITIVE=trueand a verified site admin
unknown / unverifiable / transport error
deny, audited
gitea_request adds a deterministic write classifier (a render-only override table can only downgrade provably side-effect-free POSTs to reads, never upgrade — a write can't slip past WRITE_MODE), a known-path gate (unknown prefix → deny), and the pre-existing admin/credential denylist.
uv build → sdist + wheel; uvx --from ./dist/<wheel> aegis-gitea-mcp installs 34 core deps (no FastAPI) and runs the console script end-to-end.
Assumptions / deferred
Version set to 0.2.0 per the task brief (the app already reported 0.2.0; pyproject was 0.1.0).
TODO(authz):list_organizations (/user/orgs) stays denied in service-PAT mode because it returns the bot's orgs (fail-closed, safe). Follow-up: make it user-scoped via /users/{login}/orgs so it can be allowed.
structlog retained as a core dep per the brief though currently unused in src.
Notes
Branch flow: HEAD → feature → dev → main. This PR targets dev; dev/main are expected to be protected. The publish workflow runs on a v* tag.
New .gitea/workflows/publish.yml builds with uv and publishes to the Gitea PyPI registry on tag, gated on lint + tests, using GITEA_PACKAGE_USER / GITEA_PACKAGE_TOKEN secrets (fails loudly if absent; public PyPI intentionally not published).
## Summary
Refactors AegisGitea-MCP into a **transport-agnostic core + two thin adapters** and ships a **local stdio package** alongside the existing public HTTP/OAuth server, with **safe full-API coverage** and **resource-type-aware authorization**. The public HTTP/OAuth server behavior is unchanged.
- **Core / adapter split.** `registry.py` is the single name→handler source of truth consumed by both adapters. Core handlers raise the transport-agnostic `errors.ToolError` (no more `fastapi.HTTPException` in the core); the HTTP adapter maps it back to `HTTPException`. A subprocess boundary test asserts the core imports no `fastapi`/`uvicorn`/`starlette`.
- **Local stdio package.** `stdio_app.py` (official `mcp` SDK) runs `uvx aegis-gitea-mcp` — single-user, PAT-owner identity, no OAuth. It resolves the PAT owner (`GET /user`), pins request context, and runs the same policy + `WRITE_MODE` + secret sanitization + audit; it skips only the per-user repo probe (the operator is the token owner). Audit log falls back to a per-user state path.
- **Packaging.** Core install (httpx/pydantic/mcp/…) + `[server]` extra (fastapi/uvicorn/PyJWT/python-multipart). Console scripts `aegis-gitea-mcp` (stdio) and `aegis-gitea-mcp-server` (guarded HTTP entry). Version → 0.2.0. `uv build` produces sdist + wheel.
## Authorization model (every decision fails closed)
Enforced in service-PAT mode on top of policy + `WRITE_MODE` (pure-OAuth mode defers to the user's token at Gitea):
| Resource type | Rule |
|---|---|
| `repository` | per-user collaborator permission; unparseable repo path → **deny** |
| `org` | signed-in user must be a Gitea-verified member; unverifiable → **deny** |
| `user_owned` (`/users/{x}`, `/packages/{owner}`) | allow only if owner == caller or caller is a member org; else **deny** |
| `user_self` (`/user`, `/notifications`) | **deny** — data is the bot's, not the caller's |
| `misc_global` | reads allow, writes **deny** |
| `admin` | **default-deny**; allowed only with `RAW_API_ALLOW_SENSITIVE=true` **and** a verified site admin |
| `unknown` / unverifiable / transport error | **deny**, audited |
`gitea_request` adds a deterministic write classifier (a render-only override table can only *downgrade* provably side-effect-free POSTs to reads, never upgrade — a write can't slip past `WRITE_MODE`), a known-path gate (unknown prefix → **deny**), and the pre-existing admin/credential denylist.
## Verification
- ruff + ruff-format + black + mypy(strict) clean.
- pytest: 346 passed, 1 skipped; coverage 83.70% (threshold 80%, unchanged; baseline was 284 / 84.04%).
- `uv build` → sdist + wheel; `uvx --from ./dist/<wheel> aegis-gitea-mcp` installs 34 core deps (no FastAPI) and runs the console script end-to-end.
## Assumptions / deferred
- Version set to **0.2.0** per the task brief (the app already reported 0.2.0; pyproject was 0.1.0).
- **TODO(authz):** `list_organizations` (`/user/orgs`) stays **denied** in service-PAT mode because it returns the bot's orgs (fail-closed, safe). Follow-up: make it user-scoped via `/users/{login}/orgs` so it can be allowed.
- `structlog` retained as a core dep per the brief though currently unused in `src`.
## Notes
- Branch flow: `HEAD → feature → dev → main`. This PR targets **`dev`**; `dev`/`main` are expected to be protected. The publish workflow runs on a `v*` tag.
- New `.gitea/workflows/publish.yml` builds with `uv` and publishes to the Gitea PyPI registry on tag, gated on lint + tests, using `GITEA_PACKAGE_USER` / `GITEA_PACKAGE_TOKEN` secrets (fails loudly if absent; public PyPI intentionally not published).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Introduce aegis_gitea_mcp.registry as the single name->handler source of
truth consumed by every transport adapter, moving TOOL_HANDLERS out of the
FastAPI server module. Add aegis_gitea_mcp.errors.ToolError so core handlers
no longer import fastapi.HTTPException; raw_tools now raises ToolError and the
HTTP adapter maps it back to HTTPException, preserving status codes and audit
behavior. Add a subprocess boundary test asserting the core imports without
pulling in fastapi/uvicorn/starlette.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add aegis_gitea_mcp.stdio_app: a single-user, local MCP server over stdio
(official mcp SDK) that serves the same tools from the shared registry,
resolves the PAT owner via GET /user and pins request context to it, and runs
policy + WRITE_MODE + secret sanitization + audit while skipping the per-user
repo probe (the operator is the trusted token owner). Audit log falls back to a
per-user state path when the container default is unwritable.
Packaging: split deps into core (httpx/pydantic/mcp/...) and a [server] extra
(fastapi/uvicorn/PyJWT/python-multipart); add console scripts aegis-gitea-mcp
(stdio) and aegis-gitea-mcp-server (guarded HTTP entry); bump to 0.2.0 and fix
repo URLs. mcp added to requirements for CI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a deterministic (method, path) read/write classifier with an explicit
render-only override table that can only downgrade provably side-effect-free
POSTs (markdown/markup) to reads, never the reverse — so a mutating call cannot
slip past the write-mode gate. Add a known-Gitea-prefix gate: gitea_request now
fails closed on any path whose top segment is not a recognized /api/v1 route
instead of passing unknown paths through. Expose raw_relative_segments for the
authorization layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add aegis_gitea_mcp.authz: classify every dispatched call (typed tools and
gitea_request) by resource type (repository/org/user_self/user_owned/
misc_global/admin/unknown) and enforce a type-specific rule in service-PAT
mode, on top of policy + WRITE_MODE. Every decision fails closed:
- org: signed-in user must be a verified org member (Gitea-checked).
- user_owned: owner must be the caller or a member org of the caller.
- user_self: token-owner-scoped endpoints denied (token is the bot's).
- admin: default-deny; allowed only with RAW_API_ALLOW_SENSITIVE opt-in AND a
verified site admin.
- misc_global: reads allowed, writes denied.
- unknown / unverifiable: denied and audited.
Wire it into the server's service-PAT dispatch: repository calls keep the
existing per-user collaborator check; non-repo calls (previously blanket-denied)
now go through the resource-type gate, opening the org/user/admin surface
safely. Verification results are cached briefly (fail-closed: positives only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover the stdio adapter: local-mode env bootstrap (OAuth off, API-key gate off,
per-user audit path), missing-env failure, PAT owner resolution, and dispatch
(unknown tool, write-mode policy denial, and the happy path pinning request
context to the PAT owner via the shared registry). Tidy the boundary-test
assertion so ruff and black agree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reframe the README around two transports and add a local stdio quickstart with
uvx/pip and Claude Desktop / Claude Code wiring. New docs: local-quickstart.md
and packaging.md (uv build/publish). Document resource-type-aware authorization
and classified gitea_request in security.md; stdio env vars + audit-log
fallback in configuration.md; local install in deployment.md; core+adapters in
architecture.md. Add the missing root AGENTS.md contract, update CLAUDE.md with
the core/adapter layout, fail-closed invariants, and the branching flow
(HEAD -> feature -> dev -> main). Update roadmap/todo and .env.example.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add .gitea/workflows/publish.yml: on a v* tag, gate on the existing lint + test
jobs, then build sdist+wheel with uv and publish to the self-hosted Gitea PyPI
registry using least-privilege Actions secrets (GITEA_PACKAGE_USER /
GITEA_PACKAGE_TOKEN). The job fails loudly when the secrets are absent rather
than publishing anonymously, uploads the built artifacts, and leaves a disabled
public-PyPI stub. Public PyPI is intentionally not published in this pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Latte
merged commit cf19a320b0 into dev2026-06-27 12:23:32 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Refactors AegisGitea-MCP into a transport-agnostic core + two thin adapters and ships a local stdio package alongside the existing public HTTP/OAuth server, with safe full-API coverage and resource-type-aware authorization. The public HTTP/OAuth server behavior is unchanged.
registry.pyis the single name→handler source of truth consumed by both adapters. Core handlers raise the transport-agnosticerrors.ToolError(no morefastapi.HTTPExceptionin the core); the HTTP adapter maps it back toHTTPException. A subprocess boundary test asserts the core imports nofastapi/uvicorn/starlette.stdio_app.py(officialmcpSDK) runsuvx aegis-gitea-mcp— single-user, PAT-owner identity, no OAuth. It resolves the PAT owner (GET /user), pins request context, and runs the same policy +WRITE_MODE+ secret sanitization + audit; it skips only the per-user repo probe (the operator is the token owner). Audit log falls back to a per-user state path.[server]extra (fastapi/uvicorn/PyJWT/python-multipart). Console scriptsaegis-gitea-mcp(stdio) andaegis-gitea-mcp-server(guarded HTTP entry). Version → 0.2.0.uv buildproduces sdist + wheel.Authorization model (every decision fails closed)
Enforced in service-PAT mode on top of policy +
WRITE_MODE(pure-OAuth mode defers to the user's token at Gitea):repositoryorguser_owned(/users/{x},/packages/{owner})user_self(/user,/notifications)misc_globaladminRAW_API_ALLOW_SENSITIVE=trueand a verified site adminunknown/ unverifiable / transport errorgitea_requestadds a deterministic write classifier (a render-only override table can only downgrade provably side-effect-free POSTs to reads, never upgrade — a write can't slip pastWRITE_MODE), a known-path gate (unknown prefix → deny), and the pre-existing admin/credential denylist.Verification
uv build→ sdist + wheel;uvx --from ./dist/<wheel> aegis-gitea-mcpinstalls 34 core deps (no FastAPI) and runs the console script end-to-end.Assumptions / deferred
list_organizations(/user/orgs) stays denied in service-PAT mode because it returns the bot's orgs (fail-closed, safe). Follow-up: make it user-scoped via/users/{login}/orgsso it can be allowed.structlogretained as a core dep per the brief though currently unused insrc.Notes
HEAD → feature → dev → main. This PR targetsdev;dev/mainare expected to be protected. The publish workflow runs on av*tag..gitea/workflows/publish.ymlbuilds withuvand publishes to the Gitea PyPI registry on tag, gated on lint + tests, usingGITEA_PACKAGE_USER/GITEA_PACKAGE_TOKENsecrets (fails loudly if absent; public PyPI intentionally not published).🤖 Generated with Claude Code