From 8d650c05706ac9645157212509919aacace48043 Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Mon, 4 May 2026 15:06:34 -0400 Subject: [PATCH] docs(mnemosyne): update Phase 3 status to implemented Mark per-turn JWT access control as implemented in the Mnemosyne integration docs. Update Phase 2/3 status tables, replace deferred language with concrete implementation details, and document the `MCPSigningKey` model, `resolve_mcp_jwt`, and `_scope_from_claims` components now live in the MCP server. --- docs/mnemosyne_integration.md | 51 ++++++++++++++++++++++++----------- mnemosyne/mcp_server/admin.py | 11 +++++++- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/docs/mnemosyne_integration.md b/docs/mnemosyne_integration.md index d9eee56..c17e5b2 100644 --- a/docs/mnemosyne_integration.md +++ b/docs/mnemosyne_integration.md @@ -9,15 +9,15 @@ This document describes Mnemosyne's role in the Daedalus + Pallas architecture a Mnemosyne exposes two interfaces for the wider Ouranos ecosystem: 1. **REST API** (`/library/api/*`) — consumed by the Daedalus backend (HTTP Basic auth, service account `daedalus-service`) for workspace lifecycle and asynchronous file ingestion. Phase 1, **implemented**. -2. **MCP Server** (port 22091 internal, `/mcp/` via nginx on 23090) — exposes search, browse, and retrieval tools. Phase 5 of Mnemosyne's own roadmap, **implemented** with workspace_id scoping. Currently consumed by an internal validator (`validator/`) and ad-hoc clients; planned production consumer is Pallas FastAgents in Daedalus integration Phase 2 (deferred — see [Phase 3 of this doc](#3-phase-3-deferred-per-turn-token-access-control)). +2. **MCP Server** (port 22091 internal, `/mcp/` via nginx on 23090) — exposes search, browse, and retrieval tools. Phase 5 of Mnemosyne's own roadmap, **implemented** with workspace_id scoping and per-turn JWT access control. Consumed by Pallas FastAgents in production (Daedalus integration Phase 2, **implemented** — see [Phase 3 of this doc](#3-phase-3-per-turn-token-access-control-for-daedalus-integration)). ### Phase status | Phase | What | Status | |-------|------|--------| | 1. REST workspace + ingest API for Daedalus | `POST /workspaces/`, `DELETE /workspaces/{id}/`, `POST /ingest/`, `GET /jobs/{id}/` | **Implemented** | -| 2. MCP Server (Mnemosyne roadmap Phase 5) | `search`, `get_chunk`, `list_libraries`, `list_collections`, `list_items`, `get_health` | **Implemented** (workspace_id scoping in place; access-control to follow in Phase 3) | -| 3. Per-turn signed-token access control for Daedalus integration | Daedalus mints tokens carrying `{workspace_id, allowed_libraries}` claims; Mnemosyne validates and scopes search server-side | **Deferred** | +| 2. MCP Server (Mnemosyne roadmap Phase 5) | `search`, `get_chunk`, `list_libraries`, `list_collections`, `list_items`, `get_health` | **Implemented** (workspace_id scoping enforced in Cypher) | +| 3. Per-turn signed-token access control for Daedalus integration | Daedalus mints HS256 JWTs carrying `{ws, libs}` claims; Mnemosyne validates via `MCPSigningKey` and scopes search via `_scope_from_claims` | **Implemented** | --- @@ -367,19 +367,40 @@ mnemosyne_s3_operations_total{operation,status} counter - [x] ASGI mount + uvicorn deployment on port 22091; nginx proxies via `/mcp/` on 23090 - [x] Prometheus metrics (`mnemosyne_mcp_*`) -### Phase 3 — Per-turn token access control for Daedalus integration 📋 Deferred +### Phase 3 — Per-turn token access control for Daedalus integration ✅ Implemented -The Phase 2 MCP server is search-capable but currently has no token-based library-access scoping beyond `workspace_id` (which is parameter-level, not auth-level). The intended production access-control layer for the Daedalus integration is a per-turn signed token model: +Daedalus mints a short-lived HS256 JWT per chat turn and sends it as `Authorization: Bearer` to Pallas. Pallas forwards the token to outgoing Mnemosyne MCP calls (via `pallas/_fastagent_patch`). Mnemosyne validates the JWT and scopes every search to the workspace indicated by the `ws` claim. -- Daedalus mints short-lived tokens carrying `{sub: agent_id, workspace_id, allowed_libraries, exp}`. -- Pallas forwards the inbound bearer to its outgoing Mnemosyne MCP calls (requires a small upstream patch — see Daedalus-side §9.4). -- Mnemosyne's MCP token validator extracts the claims; search Cypher additionally filters `WHERE lib.uid IN $allowed_libraries`. -- Workspace libraries are auto-included in the per-turn token's allowed list when the agent is being invoked from that workspace. +**Mnemosyne-side components:** -Mnemosyne-side work for Phase 3: -- [ ] Extend `MCPToken` (or sibling) to carry signed claims `{workspace_id, allowed_libraries, exp}` -- [ ] Token validator reads claims, attaches them to the FastMCP request context -- [ ] `search` / `list_*` tools consult claim-derived allowed-library set in addition to existing parameter filters -- [ ] Document the JWT/signing format Daedalus mints to (likely HS256 with a shared secret in Vault, or RS256 against Daedalus's JWKS — TBD) +- [x] `MCPSigningKey` model — stores active HS256 secrets keyed by `kid`. Managed via `manage.py seed_signing_key --kid `. +- [x] `resolve_mcp_jwt(token_string)` in `mcp_server/auth.py` — validates signature, `exp`, `iss`, `jti` replay; returns claims dict. +- [x] `MCPAuthMiddleware.on_call_tool` — detects JWT shape (three dot-separated segments), routes to `resolve_mcp_jwt`, stores claims in FastMCP context state via `STATE_KEY_CLAIMS`. +- [x] `_scope_from_claims(claims, arg_workspace_id)` — claims trump tool args; returns `(ws, allowed_libraries)`. +- [x] `allowed_libraries` on `SearchRequest` — extends `_WORKSPACE_SCOPE_CLAUSE` to include user-managed libraries in addition to the workspace's own. -See the Daedalus-side spec [§9](../../daedalus/docs/mnemosyne_integration.md#9-phase-2--knowledge-library-access-control-deferred) for the full integration architecture. +**Token format (HS256):** + +```json +{ + "iss": "daedalus", + "sub": "chat", + "ws": "", + "libs": [], + "iat": 1746000000, + "exp": 1746000600, + "jti": "" +} +``` + +The `libs` claim is reserved for future user-managed library assignment (deferred). Currently always `[]`; the workspace's own library is always included via the `ws` claim. + +**Provisioning:** + +```bash +# On Mnemosyne host, once: +docker compose exec app python manage.py seed_signing_key --kid daedalus-1 +# Copy the printed hex → DAEDALUS_MNEMOSYNE_SIGNING_SECRET in Daedalus .env +``` + +See the Daedalus-side spec [§9](../../daedalus/docs/mnemosyne_integration.md#9-phase-2--workspace-scoped-mcp-search-implemented) for the full integration architecture. diff --git a/mnemosyne/mcp_server/admin.py b/mnemosyne/mcp_server/admin.py index 41e0a64..086a2d7 100644 --- a/mnemosyne/mcp_server/admin.py +++ b/mnemosyne/mcp_server/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import MCPToken +from .models import MCPSigningKey, MCPToken @admin.register(MCPToken) @@ -36,3 +36,12 @@ class MCPTokenAdmin(admin.ModelAdmin): # so the plaintext can be surfaced to the user. Adding via admin # would persist a hash with no plaintext ever shown. return False + + +@admin.register(MCPSigningKey) +class MCPSigningKeyAdmin(admin.ModelAdmin): + list_display = ["kid", "is_active", "created_at", "retired_at", "note"] + list_filter = ["is_active"] + search_fields = ["kid", "note"] + readonly_fields = ["created_at", "retired_at"] + fields = ["kid", "secret_hex", "is_active", "note", "created_at", "retired_at"]