Promotes the accumulated dev work to main so a v0.2.0 tag can be cut. Tagging v0.2.0 on main triggers publish.yml, which builds the Python package with uv and publishes it to the Gitea PyPI registry.
Highlights
Dual transport on one shared core
Local stdio adapter — uvx aegis-gitea-mcp — single-user, authenticates with a Gitea PAT, no OAuth, no web stack.
Public HTTP/OAuth server unchanged in behavior.
The core imports no FastAPI/uvicorn; a subprocess boundary test enforces it.
Safe full-API coverage
gitea_request reaches the long tail of the Gitea API, gated by: a deterministic write classifier (render-only POSTs may only be downgraded to reads, never upgraded), a known-path gate (unknown prefix → deny), and the admin/credential denylist.
Resource-type-aware authorization (fail-closed)
Every non-repo call is classified (repository/org/user/admin/misc) and enforced on top of policy + WRITE_MODE. Org membership and site-admin are verified against Gitea; anything unverifiable is denied. Admin is default-deny.
Packaging & CI
Core install + [server] extra; console scripts aegis-gitea-mcp (stdio) and aegis-gitea-mcp-server (guarded). Version 0.2.0.
publish.yml: tag-gated uv build + publish to the Gitea registry, reusing the existing REGISTRY_TOKEN secret.
Also promotes the earlier raw-API dispatch work that was not yet on main.
⚠️ Breaking (packaging)
pip install aegis-gitea-mcp is now core-only and no longer pulls in FastAPI/uvicorn. Anyone running the HTTP server must install the extra: pip install 'aegis-gitea-mcp[server]' (and use the aegis-gitea-mcp-server entry point). Docker images are unaffected.
uv build produces sdist + wheel; the core wheel installs without FastAPI and runs the stdio console script.
After merge — cut the release
git checkout main && git pull
git tag v0.2.0 && git push origin v0.2.0 # triggers publish.yml
Scope
dev → main, 20 commits. Supersedes the closed#61 (closed unmerged; this is the clean replacement with full notes + labels).
## Release `dev → main` — v0.2.0
Promotes the accumulated `dev` work to `main` so a `v0.2.0` tag can be cut. Tagging `v0.2.0` on `main` triggers `publish.yml`, which builds the Python package with `uv` and publishes it to the Gitea PyPI registry.
### Highlights
**Dual transport on one shared core**
- Local **stdio** adapter — `uvx aegis-gitea-mcp` — single-user, authenticates with a Gitea PAT, no OAuth, no web stack.
- Public **HTTP/OAuth** server unchanged in behavior.
- The core imports no FastAPI/uvicorn; a subprocess boundary test enforces it.
**Safe full-API coverage**
- `gitea_request` reaches the long tail of the Gitea API, gated by: a deterministic write classifier (render-only POSTs may only be *downgraded* to reads, never upgraded), a known-path gate (unknown prefix → deny), and the admin/credential denylist.
**Resource-type-aware authorization (fail-closed)**
- Every non-repo call is classified (repository/org/user/admin/misc) and enforced on top of policy + `WRITE_MODE`. Org membership and site-admin are verified against Gitea; anything unverifiable is denied. Admin is default-deny.
**Packaging & CI**
- Core install + `[server]` extra; console scripts `aegis-gitea-mcp` (stdio) and `aegis-gitea-mcp-server` (guarded). Version `0.2.0`.
- `publish.yml`: tag-gated `uv build` + publish to the Gitea registry, reusing the existing `REGISTRY_TOKEN` secret.
- Also promotes the earlier raw-API dispatch work that was not yet on `main`.
### ⚠️ Breaking (packaging)
`pip install aegis-gitea-mcp` is now **core-only** and no longer pulls in FastAPI/uvicorn. Anyone running the **HTTP server** must install the extra: `pip install 'aegis-gitea-mcp[server]'` (and use the `aegis-gitea-mcp-server` entry point). Docker images are unaffected.
### Verification
- ruff + ruff-format + black + mypy (strict) clean.
- pytest: 346 passed, 1 skipped; coverage 83.70% (threshold 80%, unchanged).
- `uv build` produces sdist + wheel; the core wheel installs without FastAPI and runs the stdio console script.
### After merge — cut the release
```
git checkout main && git pull
git tag v0.2.0 && git push origin v0.2.0 # triggers publish.yml
```
### Scope
- `dev` → `main`, 20 commits. Supersedes the closed #61 (closed unmerged; this is the clean replacement with full notes + labels).
Adds the RawApiRequestArgs schema (extra=forbid), raw path normalization/
parsing helpers, a GiteaClient.raw_request that audits method+path only (never
the body), and the raw_api_request_tool handler. The handler derives a coarse
virtual tool name (gitea_request:METHOD:topsegment) plus repository/target_path
from the path and runs them back through the policy engine, enforces an
admin/credential sensitive-path denylist, and bounds responses. Two config
flags gate it: RAW_API_ENABLED (killswitch) and RAW_API_ALLOW_SENSITIVE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Registers gitea_request in AVAILABLE_TOOLS with write_operation=False
(deliberate: a static flag cannot describe a read-or-write tool; the handler
authorizes writes per-method) and maps the tool name to raw_api_request_tool in
the server handler registry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds docs/raw-api.md (two-layer policy, sensitive denylist, env vars, write-mode
warning), links it from index and api-reference, documents RAW_API_ENABLED /
RAW_API_ALLOW_SENSITIVE in .env.example, and adds commented virtual-tool-name
deny examples to policy.yaml.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers read allow + repository parsing, write denied without write-mode, write
allowed only for whitelisted repos, non-repo write denial, sensitive-path
denial (incl. GET) and override, cross-repo search handling, unknown-method and
traversal rejection before any network call, killswitch, response truncation,
and the raw path-parsing helpers and raw-aware extractors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The repo already has a write:package REGISTRY_TOKEN secret (used by docker.yml).
Reuse it for uv publish instead of requiring new GITEA_PACKAGE_* secrets:
authenticate as GITHUB_ACTOR with the token as password. Update packaging docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repo already has a write:package REGISTRY_TOKEN secret (used by docker.yml).
Reuse it for uv publish instead of requiring new GITEA_PACKAGE_* secrets:
authenticate as GITHUB_ACTOR with the token as password. Update packaging docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reserve stdout for the JSON-RPC stream: _configure_stderr_logging() pins all
logging to stderr (and rewrites any stray stdout handler) so a log line can
never corrupt the stdio protocol. Extract a pure, testable build_server() from
_serve(). Add end-to-end tests over the mcp in-memory transport (initialize +
tools/list + tools/call), covering a successful round trip and a policy denial
surfaced as an MCP error.
Record that no 'Generated with Claude Code' / Co-Authored-By / 'made by Claude'
attribution may appear in commits, PRs, releases, comments or docs. Add stdio
transport notes (stdout reserved for JSON-RPC, build_server vs _serve).
Add a package job to the test workflow: uv build, then verify a clean core
install excludes the web stack and the aegis-gitea-mcp stdio entry exits 2 with
an actionable message, and that the [server] extra pulls in fastapi/uvicorn and
imports the server entry. Catches packaging/console-script regressions in CI.
Gitea's act_runner does not reliably support the actions/upload-artifact@v4
backend. Drop the artifact upload from the test workflow (the package job's
purpose is to build and smoke-test, not to store wheels) and make the publish
workflow's upload best-effort (continue-on-error) so a flaky artifact backend
cannot block a release — the package is still published to the registry.
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.
Release
dev → main— v0.2.0Promotes the accumulated
devwork tomainso av0.2.0tag can be cut. Taggingv0.2.0onmaintriggerspublish.yml, which builds the Python package withuvand publishes it to the Gitea PyPI registry.Highlights
Dual transport on one shared core
uvx aegis-gitea-mcp— single-user, authenticates with a Gitea PAT, no OAuth, no web stack.Safe full-API coverage
gitea_requestreaches the long tail of the Gitea API, gated by: a deterministic write classifier (render-only POSTs may only be downgraded to reads, never upgraded), a known-path gate (unknown prefix → deny), and the admin/credential denylist.Resource-type-aware authorization (fail-closed)
WRITE_MODE. Org membership and site-admin are verified against Gitea; anything unverifiable is denied. Admin is default-deny.Packaging & CI
[server]extra; console scriptsaegis-gitea-mcp(stdio) andaegis-gitea-mcp-server(guarded). Version0.2.0.publish.yml: tag-gateduv build+ publish to the Gitea registry, reusing the existingREGISTRY_TOKENsecret.main.⚠️ Breaking (packaging)
pip install aegis-gitea-mcpis now core-only and no longer pulls in FastAPI/uvicorn. Anyone running the HTTP server must install the extra:pip install 'aegis-gitea-mcp[server]'(and use theaegis-gitea-mcp-serverentry point). Docker images are unaffected.Verification
uv buildproduces sdist + wheel; the core wheel installs without FastAPI and runs the stdio console script.After merge — cut the release
Scope
dev→main, 20 commits. Supersedes the closed #61 (closed unmerged; this is the clean replacement with full notes + labels).