diff --git a/AGENTS.md b/AGENTS.md index e4b0dff..c72cdf4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/`; diff --git a/CLAUDE.md b/CLAUDE.md index 33718a7..d97aa5a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/docs/architecture.md b/docs/architecture.md index 3b0bd14..41c6cad 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/raw-api.md b/docs/raw-api.md index 1318c6f..3aea84c 100644 --- a/docs/raw-api.md +++ b/docs/raw-api.md @@ -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 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 55114b4..d97492a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -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