Commit Graph
75 Commits
Author SHA1 Message Date
Latte 0f5f5cdd95 fix: do not echo setting values in configuration errors; test: stdio and config redaction 2026-10-14 17:52:07 +02:00
Latte 984673e8cf fix: reject a trailing newline in sudo; test: malformed input suite 2026-10-14 17:49:37 +02:00
Latte 421a6507c0 fix: redact a data field in Action secret responses; test: secret echo suite 2026-10-14 17:46:11 +02:00
Latte fef80a5b21 test: security suite for auth, startup, redaction; fix: mask secrets in object keys 2026-10-14 17:44:20 +02:00
Latte 6f7a3c0e5b fix: refuse a token store directory owned by another user or left writable (SEC-13) 2026-10-14 17:36:21 +02:00
Latte d96c3ac604 fix: reject whitespace and control characters in base URLs and warn on plain http Gitea (SEC-10) 2026-10-14 17:34:15 +02:00
Latte 87f048cc4d fix: quota pending sign-ins and registrations per address, keep young clients, write the store off the loop (SEC-09) 2026-10-14 17:32:03 +02:00
Latte f57c237bed fix: rate limit /token and /revoke, bucket IPv6 by /64 and warn about untrusted forwarded headers (SEC-08) 2026-10-14 17:29:12 +02:00
Latte 782d416e46 fix: require a matching Origin on consent and name the binding cookie per flow (SEC-17, SEC-19) 2026-10-14 17:26:52 +02:00
Latte 8be859243d fix: add OAUTH_REDIRECT_ALLOWLIST for client registration and waive the open default (SEC-05) 2026-10-14 17:24:41 +02:00
Latte 79aa4f7fc0 fix: load ./.env only when private and owned, add --env-file and AEGIS_ENV_FILE (SEC-04) 2026-10-14 17:22:29 +02:00
Latte 7307214ab6 fix: create the audit log 0600 in a 0700 directory and refuse symlinks (SEC-11) 2026-10-14 17:18:29 +02:00
Latte fd5e4b71f3 fix: record the calling client and only schema-known names in audit entries (SEC-12, SEC-18) 2026-10-14 17:14:13 +02:00
Latte 43eebd3089 fix: write-ahead audit entry so calls stop when the audit log stops accepting writes (SEC-03) 2026-10-14 17:11:53 +02:00
Latte 5b622e64e2 fix: redact stored webhook authorization headers and other secret fields by key name (SEC-02) 2026-10-14 17:08:59 +02:00
Latte d5ca530db3 fix: cap and type-check the unauthenticated consent POST body (SEC-01) 2026-10-14 17:07:06 +02:00
Latte ce5ae6caa5 fix: redact identity secrets through a public hook and mask issued agm_ tokens
ci / gitleaks (push) Successful in 8s
ci / docs-check (push) Failing after 30s
ci / lint (push) Successful in 42s
ci / test-integration (push) Successful in 46s
ci / security (push) Failing after 27s
ci / test-mcp (push) Successful in 1m16s
ci / test-unit (push) Successful in 2m17s
ci / build-and-push (push) Successful in 17s
Identity gains a redact field that the executor honours, so the OAuth
resolver no longer needs an executor subclass overriding private
internals. Opaque agm_ tokens issued by our authorization server are now
masked by the secret patterns wherever they appear in content.
2026-10-14 16:45:36 +02:00
Latte 566d0b65dd feat: add per-user OAuth mode proxying sign-in to Gitea
Authorization server for MCP clients (SDK provider with dynamic client
registration, PKCE, refresh rotation and revocation) that signs users in
through Gitea and runs tool calls with the user's own Gitea token.

- consent page with CSRF protection and a browser-binding cookie on the
  Gitea callback (confused-deputy protection for the shared Gitea app)
- encrypted token store (Fernet, HKDF-derived key, 0600, atomic writes,
  symlink and corrupt-file refusal); our tokens stored only as hashes
- tokens bound to the MCP endpoint resource and the gitea scope; the
  static bearer keeps working next to OAuth
- code replay and refresh token reuse revoke the grant
- automatic upstream refresh with per-session locking; invalid_grant
  ends the session and never falls back to the admin token
- OAuth-only deployments (no GITEA_TOKEN, no bearer) may start
- rate limit on the OAuth endpoints, query strings dropped from access logs
2026-10-14 16:41:18 +02:00
Latte cfbf1fb893 fix: keep the embedded env template in sync with .env.example 2026-10-14 16:25:37 +02:00
Latte cf3d2e17d3 fix: fail closed on an unwritable audit log, bound the rate limiter and harden startup
The audit logger is opened at startup (exit 2 on failure) and obtained before
every call, which is refused with audit_unavailable otherwise. Audit records only
take validated path and sudo values and use the request id as correlation id. The
rate limiter evicts idle keys, caps tracked keys, audits the first excess per key
and window and uses the settings passed to the app. FORWARDED_ALLOW_IPS is now a
setting, and startup warns about a missing host allow-list and unavailable OAuth.
2026-10-14 16:24:51 +02:00
Latte a3222d5ebe fix: report an invalid --port with a short message instead of listing every choice 2026-10-14 16:24:51 +02:00
Latte c73a7cc3ce fix: serialize the admin login lookup so concurrent first calls send one request 2026-10-14 16:24:51 +02:00
Latte c9e2c481e7 feat: add MCP server core, stdio and streamable HTTP transports and serve command
Low-level SDK server built from the catalog plus gitea_api_request, with TOOLSETS/READ_ONLY/DISABLED_TOOLS filters, schema validation without value echo, one audit event per call, static bearer verifier, identity resolver hook, /health, DNS-rebinding protection, rate limiting and fail-closed startup checks.
2026-10-14 16:13:11 +02:00
Latte 517c675048 fix: harden setup against plain http, untrusted output and unsafe values
Require consent before sending the token over plain http to a non-loopback
host, strip control characters from server-supplied text, restrict env values
and tokens to printable ASCII, validate the URLs like other env values and
fsync the directory after the atomic replace.
2026-10-14 16:11:01 +02:00
Latte 6b11d29919 fix: annotate POST render endpoints read-only and irreversible POSTs destructive 2026-10-14 16:09:59 +02:00
Latte 156e963e21 fix: key slash encoding on route, redact more request secrets, bound JSON depth and uploads
Slash safety is decided per (path template, parameter) so single-segment
routes such as releases/tags/{tag} and editorconfig/{filepath} keep %2F.
Adds authorization_header, remote_password, mirror_password, mirror_token and
the AWS key fields to the redacted request fields. Deeply nested JSON from
Gitea is treated as text instead of raising RecursionError, and base64
uploads are capped before decoding.
2026-10-14 16:08:42 +02:00
Latte 9e50859133 feat: add Gitea request executor with result envelope, pagination and redaction 2026-10-14 15:55:56 +02:00
Latte 041b3ea7ff feat: generate the tool catalog from the pinned swagger
Adds scripts/generate_catalog.py (with --check), the naming rules, schema conversion, the committed tools.json for all 537 operations and the load_catalog() loader.
2026-10-14 15:51:01 +02:00
Latte 1d223e6799 feat: add the aegis-gitea-mcp setup command
Verifies the Gitea instance, token and scopes, registers or reuses the aegis-gitea-mcp OAuth app, generates missing secrets and writes a 0600 env file from the template. Prints client configs and deployment snippets without any secret value.
2026-10-14 15:50:02 +02:00
Latte b82e2866cb feat: new settings, CLI skeleton and interface skeletons
Adds the pydantic-settings Settings with validated GITEA_URL, OAuth scopes
and toolsets, the argparse CLI skeleton, catalog.model, identity and
GiteaAPIError. Test fixtures now isolate env, cwd and state paths in tmp.
2026-10-14 15:40:37 +02:00
Latte 4ac9626eb5 refactor!: remove legacy adapters, policy engine and API-key auth
Removes the FastAPI server, hand-rolled MCP protocol, tool registry and
tools, YAML policy, resource-type authz, API-key and OAuth proxy modules,
automation, observability, cache, the old Gitea client and stdio adapter,
the key-management scripts and their tests (ADR 0006).
2026-10-14 15:40:37 +02:00
Latte 5d4a98d06e feat(stdio): harden local MCP transport and add end-to-end tests
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.
2026-06-27 15:19:42 +02:00
LatteandClaude Opus 4.8 2859a7f917 style: apply ruff/black formatting to stdio_app
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:10:14 +02:00
LatteandClaude Opus 4.8 3392d8f69b feat(security): resource-type-aware authorization with fail-closed defaults
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>
2026-06-27 11:09:50 +02:00
LatteandClaude Opus 4.8 2d7f12d0d0 feat: safe full-API coverage via classified gitea_request dispatch
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>
2026-06-27 11:09:30 +02:00
LatteandClaude Opus 4.8 8902c4f642 feat: add local stdio adapter and uv-installable package with extras
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>
2026-06-27 10:56:16 +02:00
LatteandClaude Opus 4.8 7da0c46de8 refactor: extract transport-agnostic core and shared tool registry
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>
2026-06-27 10:49:46 +02:00
LatteandClaude Opus 4.8 8e41fd12af feat(raw-api): register gitea_request tool and wire server dispatch
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>
2026-06-26 12:26:56 +02:00
LatteandClaude Opus 4.8 2844c42ec8 feat(raw-api): add gitea_request schema, path parsing, client dispatch and handler
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>
2026-06-26 12:26:26 +02:00
LatteandClaude Opus 4.8 41749fd7b4 fix: harden get_issue parsing and surface real errors (#27); align CI image publish
get_issue raised 'NoneType' object is not iterable on issues whose
labels/assignees Gitea returns as null or with non-dict elements (the #13
class), which reached clients as an opaque JSON-RPC -32603 with no detail.

- read_tools: skip non-dict label/assignee entries in get_issue_tool
- server: detect a wrapped GiteaNotFoundError via the __cause__ chain and
  return 404 / JSON-RPC -32000 with a clear message; include the exception
  type name in masked internal errors so future masked failures are
  diagnosable without exposing messages or stack traces
- tests: cover non-dict collection elements and the not-found / typed-error
  responses
- ci: rewrite docker.yml to build, smoke-test and push the image to the
  Gitea container registry on merge to main/dev, matching the hiddenden.cafe
  pattern (only REGISTRY_TOKEN required)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:51:58 +02:00
LatteandClaude Opus 4.8 e08ba42697 feat: assign issues to milestones on create/update (#22)
lint / lint (pull_request) Successful in 35s
test / test (pull_request) Successful in 35s
docker / docker-test (pull_request) Successful in 8s
test / test (push) Successful in 23s
lint / lint (push) Successful in 23s
docker / test (pull_request) Successful in 29s
docker / lint (pull_request) Successful in 35s
docker / docker-publish (pull_request) Has been skipped
Add a `milestone` argument to `create_issue` and `update_issue` accepting
either a numeric milestone id or a title (resolved case-insensitively against
open and closed milestones, with a clear error for unknown titles). On
`update_issue`, `milestone: 0` clears the milestone. A BeforeValidator rejects
booleans so they are not silently coerced to an id.

Gitea Projects (Kanban boards) were investigated for #22 and are intentionally
left unsupported: Gitea 1.26.2 exposes no project endpoints in its REST API.
Documented this in api-reference.md and refreshed the (stale) write-mode tool
list to cover all 16 write tools.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:36:01 +02:00
Latte f53e1a3a5a feat: add structured logging helpers and instrument get_issue (#14)
docker / test (pull_request) Successful in 29s
test / test (push) Successful in 38s
docker / lint (pull_request) Successful in 39s
lint / lint (push) Successful in 39s
docker / docker-test (pull_request) Successful in 12s
docker / docker-publish (pull_request) Has been skipped
lint / lint (pull_request) Successful in 28s
test / test (pull_request) Successful in 22s
Adds reusable, secret-safe logging helpers to `logging_utils`:
- `log_event(logger, level, event, **context)` emits a named event with a
  sanitized `context` mapping (sensitive keys masked as `***`).
- `log_nullable_field(...)` records whether a parsed field is None plus its
  runtime type, without dumping its contents.
- `sanitize_context(...)` is the shared masking primitive.

The JSON formatter now serializes a record's `context` into the payload.

`get_issue_tool` is instrumented at DEBUG (`get_issue.start`,
`get_issue.payload_shape`, `get_issue.field_check` for labels/assignees/user)
so the nullable-field parsing that caused #13 is diagnosable going forward.

Adds tests for the helpers, the formatter, and the get_issue instrumentation,
and documents the pattern in docs/observability.md.
2026-06-22 15:40:36 +02:00
Latte 478aee9bed fix: tolerate null labels/assignees/user in get_issue (#13)
docker / test (pull_request) Successful in 32s
docker / lint (pull_request) Successful in 39s
lint / lint (pull_request) Successful in 34s
test / test (pull_request) Successful in 32s
docker / docker-publish (pull_request) Has been skipped
test / test (push) Successful in 25s
lint / lint (push) Successful in 27s
docker / docker-test (pull_request) Successful in 11s
Gitea may return JSON null for an issue's `labels`, `assignees`, or
`user` fields. `dict.get(key, [])` returns None when the key is present
with a null value (the default is only used for missing keys), so the
list comprehensions raised `'NoneType' object is not iterable` for
otherwise-valid issues. Coalesce with `or []` / `or {}` so empty/null
collections normalize to empty results.

Adds a regression test covering all three null fields.
2026-06-22 10:43:43 +02:00
LatteandClaude Opus 4.8 b62ed098bf feat: add 13 read tools (PR files/commits, comments, branches, releases, milestones, org/status/languages/topics)
test / test (push) Successful in 1m13s
lint / lint (push) Successful in 1m14s
docker / docker-publish (pull_request) Has been skipped
docker / test (pull_request) Successful in 22s
docker / lint (pull_request) Successful in 29s
lint / lint (pull_request) Successful in 31s
test / test (pull_request) Successful in 21s
docker / docker-test (pull_request) Successful in 23s
Expands the read surface so the MCP can inspect more of Gitea:

- list_pull_request_files, list_pull_request_commits, list_issue_comments
- list_branches, get_branch
- get_release, get_latest_release, list_milestones
- get_commit_status
- list_org_repositories, list_organizations
- get_repo_languages, list_repo_topics

Each: arg schema (extra=forbid; GitRef on branch/sha fields), Gitea client
method with url-encoded path segments, bounded handler, MCP registration
(read-only), server wiring, docs, and parametrized success tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:43:03 +02:00
LatteandClaude Opus 4.8 7837ff43ad feat: add PR/release/branch/milestone/comment write tools
Adds six opt-in write tools (write-mode + policy + per-user permission still
enforced; no destructive or admin actions):

- create_pull_request (POST /pulls)
- create_release / edit_release (POST/PATCH /releases)
- create_branch (POST /branches; create only, no deletion)
- create_milestone (POST /milestones)
- edit_issue_comment (PATCH /issues/comments/{id})

Each: arg schema (extra=forbid, GitRef on branch/ref-like fields), Gitea client
method with url-encoded path segments, handler that surfaces auth errors, MCP
registration (write_operation=True), server wiring, docs, and success tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:38:25 +02:00
LatteandClaude Opus 4.8 c282ffe359 feat: complete label management (name->id resolution, update/remove)
Resolves the long-standing problem that label tools passed names while Gitea's
API requires numeric label ids.

- gitea_client: add _resolve_label_ids() helper; create_issue and add_labels now
  resolve label names to ids (case-insensitive) and raise a clear "Unknown
  label(s)" error instead of a generic 500.
- New tools: remove_labels (by name) and update_label (located by current name).
- Register both write tools and document the name-based label contract.
- Tests: resolver mapping + unknown-label error, add_labels id translation,
  update_label and remove_labels handlers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:34:35 +02:00
LatteandClaude Opus 4.8 f0db219ee8 feat: add create_label write tool
Adds a create_label write-mode tool so labels can be created in a repository
through the MCP server (previously there was no way to define labels, which
blocked attaching labels to issues). Follows the full tool checklist:

- arguments.py: CreateLabelArgs (name, hex color, optional description/exclusive),
  with extra=forbid and a hex-color pattern.
- gitea_client.py: create_label() POSTing to /repos/{owner}/{repo}/labels with
  url-encoded path segments.
- write_tools.py: create_label_tool handler; normalizes the color to a leading
  '#', bounds text output, and lets auth/authz errors surface.
- mcp_protocol.py: register create_label (write_operation=True).
- server.py: wire create_label into TOOL_HANDLERS.
- docs/api-reference.md: document create_label.
- tests: success path, color normalization, and invalid-color rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:24:33 +02:00
LatteandClaude Opus 4.8 e873d0325b feat: scope list_repositories to the authenticated user in service-PAT mode
docker / test (push) Successful in 28s
docker / lint (push) Successful in 33s
lint / lint (push) Successful in 35s
test / test (push) Successful in 33s
docker / docker-test (push) Successful in 10s
docker / docker-publish (push) Successful in 6s
Previously list_repositories was blocked in service-PAT mode because it has no
repository target for the per-user permission check, so users could not list
their repositories at all (the connector surfaced a generic error).

list_repositories now returns only the repositories the signed-in user owns or
contributes to, instead of everything the bot token can see:
- gitea_client.py: add list_user_repositories(login) — resolves the user id and
  queries /api/v1/repos/search with the uid filter.
- repository.py: list_repositories_tool uses the user-scoped path when a service
  PAT is configured and a user login is present; pure-OAuth mode still uses the
  user's own /user/repos.
- server.py: allow list_repositories through the service-PAT guard (it is scoped
  to the user in the handler); all other tools still require a repository target.
- README.md: document the new user-scoped behavior and its visibility caveat.

Tests: user-scoped client method (uid resolution + unknown user), PAT-mode tool
scoping, and conftest now clears the request context between tests to prevent
contextvar login leakage across files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:07:19 +02:00
LatteandClaude Opus 4.8 624a3c79ee fix: surface Gitea auth errors and document the service PAT
docker / test (push) Successful in 25s
test / test (push) Successful in 32s
lint / lint (push) Successful in 33s
docker / docker-publish (push) Successful in 6s
docker / lint (push) Successful in 30s
docker / docker-test (push) Successful in 10s
Two related issues made the connected MCP server return a bare "Internal
server error" for tools that need real Gitea API access (e.g.
list_repositories), while public-repo-by-path reads worked:

1. Gitea OIDC access tokens only carry openid/profile/email and cannot call
   the repository REST API, so pure-OAuth mode fails for most tools. A service
   PAT (GITEA_TOKEN) is required in practice; per-user permission is still
   enforced before each call, so this does not weaken authorization.
2. The tool handlers caught GiteaError broadly and re-raised it as RuntimeError.
   Because GiteaAuthenticationError/GiteaAuthorizationError subclass GiteaError,
   a clean 401/403 was masked as a generic internal error and the server's
   re-authorization guidance never fired.

Changes:
- read_tools.py / repository.py / write_tools.py: re-raise the auth/authz
  subclasses before the broad GiteaError catch so server.py returns actionable
  guidance instead of a generic 500.
- .env.example + README.md: document GITEA_TOKEN as a least-privilege bot PAT,
  explain why it's needed and that OAuth remains authoritative, and note that
  list_repositories is intentionally unavailable in service-PAT mode.
- tests: assert tool handlers propagate auth errors unwrapped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:47:10 +02:00
LatteandClaude Opus 4.8 b1bc726a95 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>
2026-06-14 16:28:52 +02:00