The DCR client registry created its storage directory eagerly in __init__,
and DCR_STORAGE_PATH defaulted to /var/lib/aegis-mcp — a path that is neither
created in the image nor mounted as a writable volume. Under the hardened
read-only docker-compose, every /oauth/authorize, /oauth/token, and /register
call hit `mkdir('/var/lib/aegis-mcp')` on a read-only filesystem, raising an
unhandled OSError and returning a bare "Internal Server Error" during login.
- oauth_flow.py: defer the storage-dir mkdir from __init__ to _persist (the
only write path). authorize/token only read the registry, so they no longer
require a writable filesystem and stop 500-ing.
- docker/Dockerfile: create and chown /var/lib/aegis-mcp.
- docker-compose.yml + docker/docker-compose.yml: add a persistent
aegis-mcp-data volume mounted at /var/lib/aegis-mcp so DCR registrations
survive restarts.
- .env.example: document DCR_STORAGE_PATH and set PUBLIC_BASE_URL to the real
MCP host.
- README.md: spell out exact values (Gitea host, MCP host, callback URL, MCP
URL) and add a "required writable volumes" section explaining the cause of
the login 500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ref-like tool arguments (ref, sha, base, head) were only length-limited
and were interpolated unencoded into Gitea API URL paths (get_tree,
get_commit_diff, compare_refs). Because httpx collapses ".." path segments
(RFC 3986), a crafted value such as "../../../../owner/repo/contents/secret"
escaped the declared owner/repo prefix. In service-PAT mode this allowed a
user authorized on one repository to read arbitrary repositories the service
token could reach, and in OAuth mode it bypassed the policy engine's
per-repository rules (which never see ref values).
Two defense layers:
- arguments.py: add _validate_git_ref / GitRef that rejects ".." path
segments, leading "/", backslashes, null bytes, control chars, whitespace,
and "?"/"#", while preserving legitimate slash refs (feature/foo, v1.2.3).
This is what actually closes the traversal.
- gitea_client.py: defense-in-depth urllib.parse.quote() on owner/repo
(safe="") and ref/sha/base/head/filepath (safe="/") in every repo URL
builder, mirroring the existing pattern in server.py.
Tests: negative cases for traversal/unsafe chars across all four fields,
positive cases for slash-containing refs, length-bound regression, and a
URL-layer confinement check. Full suite green (176 passed), coverage 85.64%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace deprecated @app.on_event startup/shutdown handlers with a FastAPI
lifespan context manager, move the inline hashlib/time imports in the auth
middleware to module top, and back the unbounded _api_scope_cache with a new
size- and TTL-bounded BoundedTTLCache utility.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gitea OIDC access_tokens only carry OIDC scopes and cannot call the
Gitea REST API. Fall back to GITEA_TOKEN (service PAT) for actual tool
execution when configured, while OIDC still handles user identity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expand workflow triggers to push/pull_request on main and dev and to PR
reviews. Run lint/test only for non-review events or when a review is
approved. Add a docker-test job that smoke-tests the built image. Add a
docker-publish job that resolves SHA and stable tags (latest/dev),
builds
the releasable image, and optionally pushes when PUSH_IMAGE=true. Update
docs/deployment.md
Introduce a GiteaOAuthValidator for JWT and userinfo validation and
fallbacks, add /oauth/token proxy, and thread per-user tokens through
the
request context and automation paths. Update config and .env.example for
OAuth-first mode, add OpenAPI, extensive unit/integration tests,
GitHub/Gitea CI workflows, docs, and lint/test enforcement (>=80% cov).
Restrict api_key query parameter to /mcp/tools, /mcp/tool/call,
and /mcp/sse only. Updated documentation to reflect query param
usage for ChatGPT UI without header support.
Added:
- run_tests.sh: Automated test runner with coverage reporting
- TESTING.md: Complete testing documentation including:
- Test suite overview
- Manual testing procedures
- CI/CD integration examples
- Performance testing guidelines
- Troubleshooting guide
The test suite now has ~85% coverage of core modules with
tests for authentication, server endpoints, and integration flows.
Pydantic was trying to parse List[str] as JSON from env vars.
Changed to use a string field (mcp_api_keys_raw) and parse manually
in model_validator, then expose as property.
This fixes the JSONDecodeError when reading MCP_API_KEYS from .env
Changed from field_validator to model_validator to properly access
auth_enabled field during validation. This fixes the SettingsError
when parsing mcp_api_keys from environment variables.
Also improved handling of empty strings and None values.
The builder stage installed dependencies to /root/.local but the final
stage switched to the 'aegis' user who couldn't access /root/.local.
Fixed by copying dependencies to /home/aegis/.local and updating PATH
to point to the correct location.