fix: keep OAuth flow working on read-only container roots
docker / test (push) Successful in 29s
docker / lint (push) Successful in 34s
test / test (push) Successful in 32s
lint / lint (push) Successful in 36s
docker / docker-test (push) Successful in 8s
docker / docker-publish (push) Successful in 6s

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>
This commit is contained in:
2026-06-14 16:26:28 +02:00
parent 84bbff4acb
commit b1bc726a95
6 changed files with 82 additions and 20 deletions
+16 -3
View File
@@ -240,9 +240,15 @@ class OAuthClientRegistry:
"""Persisted OAuth client registry for dynamic client registration."""
def __init__(self, storage_path: Path) -> None:
"""Initialize registry storage."""
"""Initialize registry storage.
The storage directory is created lazily at write time (see ``_persist``)
rather than here, so that read-only operations — the OAuth ``authorize``
and ``token`` proxies only ever *read* the registry — never require a
writable filesystem. This keeps those endpoints working on hardened,
read-only container roots where only an explicit data volume is writable.
"""
self.storage_path = storage_path
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
self._clients: dict[str, OAuthClientRecord] = {}
self._loaded = False
@@ -276,7 +282,14 @@ class OAuthClientRegistry:
self._clients = clients
def _persist(self) -> None:
"""Write registrations atomically."""
"""Write registrations atomically.
Dynamic client registration is the only operation that writes to disk, so
the storage directory is created here (not at construction time). This
requires the configured ``DCR_STORAGE_PATH`` to live on a writable,
persistent volume — see the deployment notes in the README.
"""
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
client_id: record.model_dump(mode="json", exclude={"client_id"})
for client_id, record in self._clients.items()