release: v0.2.0 — local stdio package, safe full-API coverage & resource-type authz #63

Merged
Latte merged 25 commits from dev into main 2026-06-27 13:43:56 +00:00
Owner

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

  • devmain, 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).
Latte added 20 commits 2026-06-27 13:07:56 +00:00
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>
test(raw-api): cover gitea_request handler and path parsing
docker / lint (push) Successful in 38s
docker / test (push) Successful in 33s
docker / test (pull_request) Successful in 32s
test / test (push) Successful in 40s
lint / lint (push) Successful in 42s
docker / lint (pull_request) Successful in 39s
test / test (pull_request) Successful in 39s
lint / lint (pull_request) Successful in 40s
docker / docker (pull_request) Successful in 31s
docker / docker (push) Successful in 44s
7f7aaab5a6
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>
Merge pull request 'Feat/raw api dispatch' (#58) from feat/raw-api-dispatch into dev
docker / test (push) Successful in 27s
docker / lint (push) Successful in 33s
lint / lint (push) Successful in 35s
test / test (push) Successful in 35s
docker / docker (push) Successful in 42s
aefb243a05
Reviewed-on: #58
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>
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>
ci: build and publish package to Gitea registry on tag
docker / test (push) Successful in 38s
lint / lint (push) Successful in 45s
docker / lint (push) Successful in 44s
test / test (push) Successful in 44s
docker / docker (push) Successful in 40s
docker / lint (pull_request) Successful in 40s
docker / test (pull_request) Successful in 34s
lint / lint (pull_request) Successful in 41s
test / test (pull_request) Successful in 40s
docker / docker (pull_request) Successful in 37s
499bf98d92
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>
Merge pull request 'feat: local stdio package + safe full-API coverage' (#59) from feat/local-package-and-full-coverage into dev
docker / test (push) Successful in 33s
docker / lint (push) Successful in 38s
lint / lint (push) Successful in 39s
test / test (push) Successful in 39s
docker / docker (push) Successful in 43s
cf19a320b0
Reviewed-on: #59
ci: reuse existing REGISTRY_TOKEN secret for package publish
docker / lint (push) Successful in 36s
lint / lint (push) Successful in 38s
docker / test (push) Successful in 30s
test / test (push) Successful in 37s
docker / docker (push) Successful in 39s
docker / test (pull_request) Successful in 36s
docker / lint (pull_request) Successful in 41s
lint / lint (pull_request) Successful in 43s
test / test (pull_request) Successful in 43s
docker / docker (pull_request) Successful in 47s
c551b3cfc3
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>
ci: reuse existing REGISTRY_TOKEN secret for package publish
docker / test (pull_request) Successful in 34s
test / test (pull_request) Successful in 43s
docker / docker (pull_request) Successful in 39s
docker / test (push) Successful in 34s
docker / lint (push) Successful in 40s
test / test (push) Successful in 42s
lint / lint (push) Successful in 44s
docker / lint (pull_request) Successful in 44s
lint / lint (pull_request) Successful in 42s
docker / docker (push) Successful in 46s
1ca5bcbc6b
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>
Merge pull request 'ci: reuse existing REGISTRY_TOKEN secret for package publish' (#60) from feat/publish-reuse-registry-token into dev
docker / test (push) Successful in 33s
docker / lint (push) Successful in 38s
lint / lint (push) Successful in 40s
test / test (push) Successful in 39s
docker / docker (push) Successful in 43s
docker / test (pull_request) Successful in 33s
docker / lint (pull_request) Successful in 39s
lint / lint (pull_request) Successful in 39s
test / test (pull_request) Successful in 39s
docker / docker (pull_request) Successful in 58s
da66200be7
Reviewed-on: #60
Merge branch 'dev' into feat/local-package-and-full-coverage
docker / test (push) Successful in 34s
docker / lint (push) Successful in 42s
docker / lint (pull_request) Successful in 43s
docker / test (pull_request) Successful in 32s
lint / lint (push) Successful in 45s
test / test (push) Successful in 45s
lint / lint (pull_request) Successful in 44s
test / test (pull_request) Successful in 35s
docker / docker (push) Successful in 46s
docker / docker (pull_request) Successful in 41s
83c7416677
Merge pull request 'ci: reuse existing REGISTRY_TOKEN secret for package publish' (#62) from feat/local-package-and-full-coverage into dev
docker / test (push) Successful in 31s
docker / lint (push) Successful in 37s
lint / lint (push) Successful in 39s
test / test (push) Successful in 38s
docker / docker (push) Successful in 43s
docker / test (pull_request) Successful in 33s
docker / lint (pull_request) Successful in 39s
lint / lint (pull_request) Successful in 39s
test / test (pull_request) Successful in 39s
docker / docker (pull_request) Successful in 38s
f660fa32c1
Reviewed-on: #62
Latte added 5 commits 2026-06-27 13:29:11 +00:00
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).
ci: build the package and smoke-test both install profiles
docker / test (pull_request) Successful in 34s
docker / test (push) Successful in 36s
docker / lint (pull_request) Successful in 39s
docker / lint (push) Successful in 40s
test / test (push) Successful in 42s
test / package (pull_request) Failing after 2m27s
lint / lint (pull_request) Successful in 43s
lint / lint (push) Successful in 43s
test / test (pull_request) Successful in 44s
docker / docker (pull_request) Successful in 1m10s
docker / docker (push) Successful in 34s
test / package (push) Failing after 2m5s
3d527f8690
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.
ci: stop artifact upload from failing the build on Gitea runners
docker / test (push) Successful in 34s
docker / lint (push) Successful in 40s
lint / lint (push) Successful in 43s
docker / lint (pull_request) Successful in 43s
docker / test (pull_request) Successful in 34s
test / test (push) Successful in 44s
lint / lint (pull_request) Successful in 44s
test / package (push) Successful in 1m8s
test / test (pull_request) Successful in 44s
test / package (pull_request) Successful in 53s
docker / docker (push) Successful in 1m5s
docker / docker (pull_request) Successful in 43s
4db37d200e
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.
Merge pull request 'feat: harden local stdio MCP, CI package smoke, CLAUDE.md conventions' (#64) from feat/local-mcp-hardening-and-ci into dev
docker / lint (push) Successful in 33s
docker / test (push) Successful in 31s
docker / test (pull_request) Successful in 36s
docker / lint (pull_request) Successful in 43s
lint / lint (push) Successful in 45s
test / test (push) Successful in 44s
lint / lint (pull_request) Successful in 43s
test / test (pull_request) Successful in 45s
test / package (pull_request) Successful in 1m0s
docker / docker (pull_request) Successful in 49s
test / package (push) Successful in 1m53s
docker / docker (push) Successful in 42s
2bb74807bc
Reviewed-on: #64
Latte scheduled this pull request to auto merge when all checks succeed 2026-06-27 13:34:50 +00:00
Latte scheduled this pull request to auto merge when all checks succeed 2026-06-27 13:35:32 +00:00
Latte merged commit 83e5a0df14 into main 2026-06-27 13:43:56 +00:00
Sign in to join this conversation.