Dev #72

Merged
Latte merged 71 commits from dev into main 2026-10-14 17:52:23 +00:00
5 changed files with 38 additions and 30 deletions
Showing only changes of commit 8ff67f5859 - Show all commits

No files matched your search

+21 -13
View File
@@ -19,7 +19,11 @@ Full access by default (ADR 0001). The server does not decide what is safe; the
owner restricts exposure with `READ_ONLY`, `TOOLSETS`, `DISABLED_TOOLS` and the
scope of the Gitea token. Everything else below is load-bearing:
- Every tool call produces one audit event in the hash-chained, append-only log.
- Every tool call is audited in the hash-chained, append-only log with a write-ahead
model: a `tool_call_started` entry is written before the call executes (the call is
refused with `audit_unavailable` when that entry cannot be written), and a
`tool_invocation` entry with the outcome is written after it. Anything that counts
calls in the log counts `tool_invocation` entries.
- No secrets or tokens in logs, errors, audit entries or results. Output is
passed through secret masking (`SECRET_DETECTION_MODE`, default `mask`).
`Identity.token` and every `SecretStr` setting never appear in `repr`.
@@ -62,18 +66,21 @@ src/aegis_gitea_mcp/
errors.py ToolError, GiteaAPIError(status, kind, message)
audit.py hash-chained audit log; security.py secret masking;
response_limits.py, rate_limit.py (HTTP), request_context.py, logging_utils.py
server.py, transports, oauth/ planned (chunks B4, B5), not present yet
scripts/ generate_catalog.py, validate_audit_log.py, ci/, check_docs.py (B11)
server.py the low-level SDK Server: tool list, call dispatcher, audit calls
transports.py stdio runner and the streamable HTTP app
auth.py static bearer verification and identity resolution
oauth/ per-user OAuth: provider, resolver, encrypted store, upstream, callback
scripts/ generate_catalog.py, validate_audit_log.py, check_docs.py, ci/
spec/ pinned Gitea swagger
tests/integration/ live Gitea in Docker (harness.py), opt-in
tests/integration/ live Gitea in Docker (harness.py), opt-in; sweep of every tool
tests/mcp/ MCP smoke tests (stdio, HTTP, container image), opt-in
tests/security/ security regression suite
```
Modules marked planned do not exist yet. Request flow (target):
client -> SDK transport and auth verifier -> `Identity` -> toolset/read-only
filters -> schema validation -> executor (Gitea call with the identity's token,
optional `sudo`) -> limits and masking -> result envelope (ADR 0005) -> audit.
The executor and the catalog exist; the server and transports that connect them
to MCP clients do not yet.
Request flow: client -> SDK transport and auth verifier -> `Identity` ->
toolset/read-only filters -> schema validation -> `tool_call_started` audit entry
-> executor (Gitea call with the identity's token, optional `sudo`) -> limits and
masking -> result envelope (ADR 0005) -> `tool_invocation` audit entry.
## Commands
@@ -87,13 +94,14 @@ make lint | format | test | test-integration | run | validate-audit | docker-bui
```
Live-Gitea integration tests carry the `integration` marker and run only with
`AEGIS_INTEGRATION=1` (`make test-integration`).
`AEGIS_INTEGRATION=1` (`make test-integration`); the MCP smoke tests under
`tests/mcp` carry `mcp_smoke`. `tests/integration/sweep_baseline.json` pins the
expected outcome of every tool, see `tests/integration/README.md`.
## Quality gates
Ruff (E,W,F,I,B,C4,UP,S,SIM,RUF), ruff format, mypy and pytest must be green
before every commit. Coverage threshold is 80% until the final chunk and 90%
from then on; never lower it. Tests must not write outside `tmp_path`
before every commit. The coverage threshold is 90%; never lower it. Tests must not write outside `tmp_path`
(the autouse fixture in `tests/conftest.py` isolates env, cwd, `DATA_DIR`,
`AUDIT_LOG_PATH` and singletons). Python 3.10+, line length 100, public
functions need docstrings and type hints. Documentation lives under `docs/`;
+13 -13
View File
@@ -12,12 +12,9 @@ HTTP, using the official `mcp` SDK v2 low-level `Server`. Full access by default
and the Gitea token scope. Auth modes: admin-token (`GITEA_TOKEN` / static
`MCP_BEARER_TOKEN`) and per-user OAuth on HTTP (user's own Gitea token).
The project is mid-rebuild (chunk plan in `docs/audit.md`). Built so far: tooling,
`config.py`, `identity.py`, `errors.py`, the generated catalog (`catalog/`), the
Gitea executor (`gitea/`), the setup command (`setup_cmd.py`, `setup_snippets.py`,
`setup_template.py`), the integration harness, CI and audit/security/limits
helpers. Planned: server and transports (B4), OAuth (B5), smoke tests (B9),
docs (B11).
The rebuild is complete: catalog, executor, server, both transports, per-user OAuth,
setup command, CI, security suite, MCP smoke tests and the live integration suite are in.
The remaining open items are tracked in `docs/roadmap.md`.
## Commands
@@ -27,7 +24,7 @@ make lint # ruff check, ruff format --check, mypy src
make format # ruff format + ruff check --fix
make test # uv run pytest (coverage gate)
make test-integration # AEGIS_INTEGRATION=1, needs Docker
make run # uv run aegis-gitea-mcp serve (not implemented until B4)
make run # uv run aegis-gitea-mcp serve
uv run python scripts/generate_catalog.py --check # catalog matches the spec
make validate-audit # verify audit hash chain
uv run pytest tests/test_config.py::test_defaults -v # single test
@@ -44,11 +41,12 @@ uv run pytest tests/test_config.py::test_defaults -v # single test
upload building, `SLASH_ROUTES`), `pagination.py`, `redaction.py`.
- `setup_cmd.py`, `setup_snippets.py`, `setup_template.py` (done): the `setup`
command, its client snippets and the built-in `.env` template.
- Server (B4, planned): low-level SDK `Server` with one `on_list_tools` and one
- `server.py` (done): low-level SDK `Server` with one `on_list_tools` and one
`on_call_tool` dispatcher, jsonschema validation, audit, `is_error` results for
tool failures, `gitea_api_request` escape-hatch tool.
- Auth (B5, planned): static bearer verifier and OAuth provider with encrypted
token store.
tool failures, `gitea_api_request` escape-hatch tool. `transports.py`: stdio
runner and streamable HTTP app.
- `auth.py` and `oauth/` (done): static bearer verifier and the OAuth provider
with encrypted token store.
- Core helpers (done): `config.py` (`Settings`, `get_settings()`,
`reset_settings()`), `identity.py`, `errors.py` (`ToolError`,
`GiteaAPIError`), `audit.py`, `security.py`, `response_limits.py`,
@@ -61,7 +59,9 @@ points `DATA_DIR`/`AUDIT_LOG_PATH` into `tmp_path`.
## Invariants
Kept: audit event per tool call; no secrets in logs/errors/results; no stack
Kept: write-ahead audit (a `tool_call_started` entry before the call runs, the
call is refused when it cannot be written, a `tool_invocation` entry with the
outcome after it; count calls by `tool_invocation`); no secrets in logs/errors/results; no stack
traces by default; `127.0.0.1` default bind and fail-closed HTTP auth off
loopback; untrusted-content labelling; schema validation with no extra
properties; response size bounds; validated `GITEA_URL` and strict
@@ -73,7 +73,7 @@ Details in `AGENTS.md`.
Python 3.10+, line length 100, ruff (E,W,F,I,B,C4,UP,S,SIM,RUF) plus ruff format,
strict-ish mypy (`disallow_untyped_defs`, relaxed in tests). Public functions need
docstrings and type hints. Coverage 80% now, 90% from the final chunk. All
docstrings and type hints. Coverage gate 90%. All
documentation goes under `docs/`; security-impacting changes update docs in the
same changeset. No em-dashes in text.
+1 -1
View File
@@ -21,7 +21,7 @@ GiteaExecutor (gitea/executor.py)
Gitea REST API (/api/v1)
```
Every tool call ends in one audit event, and every result is wrapped in the same envelope.
Every tool call is audited (a write-ahead `tool_call_started` entry, then one `tool_invocation` entry with the outcome), and every result is wrapped in the same envelope.
## Catalog
+2 -2
View File
@@ -26,12 +26,12 @@ because credentials belong in the identity, not in the URL.
## Behaviour
- It goes through the same executor as every generated tool: the identity's token, no redirects,
response limits, redaction and secret masking, the result envelope and one audit event.
response limits, redaction and secret masking, the result envelope and the same audit entries (`tool_call_started`, then `tool_invocation`).
- There is no allow list of paths and no write classification: with full access, the token's
scopes and the identity's Gitea permissions are the limit.
- `READ_ONLY=true` keeps the tool listed but it accepts only `GET`.
- `TOOLSETS` without `misc`, or `DISABLED_TOOLS=gitea_api_request`, removes it.
- The audit event records the method and the path, so it is visible which endpoint was called.
- The audit entry records the method and the path, so it is visible which endpoint was called.
## Example
+1 -1
View File
@@ -1,7 +1,7 @@
# Troubleshooting
Start with the result of the failing call: errors carry `error.status`, `error.kind` and
Gitea's sanitized message, and the audit log has one event per call (see
Gitea's sanitized message, and the audit log has a result entry (`tool_invocation`) per call, preceded by a `tool_call_started` entry (see
[audit-logging.md](audit-logging.md)).
## The server does not start