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.
This commit is contained in:
@@ -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 <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": "<workspace_uuid>",
|
||||
"libs": [],
|
||||
"iat": 1746000000,
|
||||
"exp": 1746000600,
|
||||
"jti": "<uuid4>"
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user