# Daedalus ↔ Pallas ↔ Mnemosyne Integration — v2

**Status:** Approved design — supersedes
[`DAEDALUS_PALLAS_INTEGRATION_v1.md`](DAEDALUS_PALLAS_INTEGRATION_v1.md).
**Authoritative home:** `mnemosyne/docs/DAEDALUS_PALLAS_INTEGRATION_v2.md`
**Versioning:** subsequent major revisions ship as `..._v3.md` etc.
alongside this file. Cross-service docs (Daedalus, Pallas) link here.

---

## 1. Summary

This document describes the end-state authentication / authorization
model connecting three services:

* **Mnemosyne** — knowledge platform. Owns Libraries, users, and the
  MCP surface third-party clients query.
* **Daedalus** — workspace + file-lifecycle UI. Registers Pallas
  instances, syncs file content to Mnemosyne, drives chat. Acts on
  behalf of one Mnemosyne user per Daedalus instance.
* **Pallas** — FastAgent-backed MCP host that exposes agent teams
  (Kottos, Mentor, Iolaus, …) as HTTP MCP servers.

**What changed from v1:**

* **Single token model.** The two-token split in v1 (DRF `authtoken`
  for REST, `MCPToken` for `/mcp/`) is gone. One model —
  [`UserToken`](../mnemosyne/mcp_server/models.py) — authenticates both
  surfaces, managed from one UI at `/profile/tokens/`. The DRF
  `authtoken` app has been removed from `INSTALLED_APPS`.
* **Per-user authorization on the REST surface.** The Daedalus-facing
  endpoints (`/library/api/*`, `/mcp_server/api/teams/*`) are no longer
  open to any authenticated account. Each `Team` has an `owner` FK and
  each workspace-scoped `Library` has an `owner_username` property; the
  endpoints scope by these and return 404 for non-owners. The
  `daedalus-service` shared account has been retired.
* **Per-turn JWT path retired.** The legacy `iss=daedalus` JWT flow
  (v1 §5.1, §6.2) is gone. Mnemosyne now only validates one JWT shape:
  `typ=team`, `iss=mnemosyne`. The replay cache and the
  `_resolve_jwt_actor` service-user fallback are also gone.
* **Authorization headers normalised to `Bearer`.** DRF
  `TokenAuthentication` (and its `Token` keyword) is replaced by
  [`UserTokenAuthentication`](../mnemosyne/mcp_server/drf_auth.py),
  which accepts `Authorization: Bearer <plaintext>`. Anonymous
  requests get **401 + `WWW-Authenticate: Bearer`** (RFC 7235).

Everything else in v1 — the resolved-library abstraction, team JWT
shape, Pallas's static-bearer configuration, the workspace ↔ Team
attachment model in Daedalus, agent picker UX, signing-key model — is
unchanged.

---

## 2. Motivation

v1 closed the per-turn JWT forwarding hairball by introducing static
team JWTs. v2 finishes the cleanup pass: it deletes the per-turn JWT
path entirely (now that Daedalus has migrated off it), collapses the
remaining two-token muddle into a single `UserToken` system, and tightens
the REST surface so authentication-as-user is sufficient for access
control without a shared service account.

---

## 3. Architecture

### 3.1 Services and responsibilities

| Service | Role in auth model |
|---|---|
| **Mnemosyne** | Owns Libraries, Library memberships, `UserToken`s, Teams, `TeamWorkspaceAssignment`s, signing keys. Validates bearers. Resolves every authenticated request to a Library set. |
| **Daedalus** | Control plane. Registers Pallas instances as Teams in Mnemosyne. Manages workspace ↔ team attachments. Stores team JWTs for copying into Pallas deployment configs. Acts as a single Mnemosyne user via a `UserToken`. |
| **Pallas** | Stateless MCP host. Holds a static team JWT in `fastagent.secrets.yaml`. No custom auth-forwarding code. |

### 3.2 Two credential types

Every authenticated request to Mnemosyne presents a Bearer token of
exactly one of these shapes:

| # | Credential | `iss` | Issuer | Lifetime | Used on | Library scope source |
|---|---|---|---|---|---|---|
| 1 | **Opaque `UserToken`** | n/a | The Mnemosyne user, via `/profile/tokens/` | Until revoked / expiry | `/mcp/` and DRF REST | MCP: `allowed_libraries`. REST: ignored (owner-scoped). |
| 2 | **Team JWT** | `mnemosyne` | Mnemosyne (`/mcp_server/api/teams/`) | 10 years | `/mcp/` only | Live DB lookup via `TeamWorkspaceAssignment → Library` |

The v1 per-turn JWT (category 2 in v1) has been retired and is no
longer accepted by `resolve_mcp_jwt`.

### 3.3 Scope split by surface

A `UserToken` carries optional `allowed_libraries` / `allowed_tools`
fields. These are honoured **only on the MCP surface** (`/mcp/`):

* **`/mcp/`** — `MCPAuthMiddleware` enforces `allowed_libraries`
  (fail-closed: empty list = zero libraries) and `allowed_tools` (empty
  list = any tool). This is the surface third-party clients (Claude
  Desktop, Cline) use.
* **`/library/api/*`, `/mcp_server/api/teams/*`** — The DRF auth class
  resolves *who* is calling. Access is gated by `Team.owner`
  (mcp_server) and `Library.owner_username` (library workspaces). The
  scope claims are ignored. Daedalus tokens are therefore
  unrestricted; the user identity plus owner-scope is the access model.

The rationale: enforcing `allowed_libraries` on the REST endpoints
would force Daedalus to mint an effectively-unrestricted token (since
it manages the whole workspace lifecycle), which would defeat the
field. Owner-scope already encodes the right access pattern there.

### 3.4 Resolved-library abstraction (MCP)

Mnemosyne's MCP auth middleware populates a single
`resolved_libraries: list[str]` per request. Downstream code (search,
get_chunk, …) only reads that list.

```
Bearer → classify → dispatch
                     ├─ Opaque UserToken    → token.allowed_libraries (JSON list of UIDs)
                     └─ team JWT (typ=team) → live DB join:
                                                TeamWorkspaceAssignment.workspace_id
                                                → Library.workspace_id → Library.uid
                                 ↓
                   resolved_libraries: list[str]
                                 ↓
                         downstream tools
```

Fail-closed: empty resolution → no libraries visible.

---

## 4. Data model

### 4.1 Mnemosyne

#### `UserToken` (renamed from `MCPToken`)
[`mnemosyne/mcp_server/models.py`](../mnemosyne/mcp_server/models.py).
Per-user opaque bearer. Hashed at rest (SHA-256, 64-char hex).

```python
class UserToken(models.Model):
    user              = FK(User, related_name="api_tokens")
    token_hash        = CharField(64, unique=True, db_index=True)
    name              = CharField(100)
    is_active         = BooleanField(default=True)
    expires_at        = DateTimeField(null=True, blank=True)
    last_used_at      = DateTimeField(null=True, blank=True)
    allowed_tools     = JSONField(default=list, blank=True)
    allowed_libraries = JSONField(default=list, blank=True)
    created_at, updated_at = …
```

* Plaintext shown once at mint via
  [`UserTokenManager.create_token`](../mnemosyne/mcp_server/models.py);
  never persisted.
* Display masking via `get_masked_token()` returns `tok_…<hash[:8]>`.
* `allowed_*` fields apply only on `/mcp/` — see §3.3.

#### `LibraryMembership`
Unchanged from v1. Roles `owner` / `manager` / `reader` over Neo4j
Libraries (joined by `uid` string since Library is a neomodel node).

#### `Team`
v1 + new non-null `owner` FK:

```python
class Team(models.Model):
    id          = UUIDField(primary_key=True, editable=False)
    name        = CharField(200)
    owner       = FK(User, on_delete=PROTECT, related_name="teams")
    active      = BooleanField(default=True)
    active_jti  = UUIDField(null=True)
    created_at, updated_at = …
```

`Team.owner` is set on creation in
[`team_create`](../mnemosyne/mcp_server/api/teams.py) from
`request.user`. All other team endpoints filter by `(pk, owner=request.user)`;
non-owners receive 404, never 403, so a team's existence isn't
disclosed across users.

Soft-delete via `Team.active = False` is unchanged.

#### `TeamWorkspaceAssignment`
Unchanged from v1. Live-queried per request; `PUT /workspaces/`
replaces the assignment set.

#### `MCPSigningKey`
Unchanged. Signs team JWTs.

#### `Library.owner_username` (new neomodel property)
[`mnemosyne/library/models.py`](../mnemosyne/library/models.py). For
workspace-scoped libraries (i.e. those with `workspace_id` set), the
Mnemosyne username of the creating user. Null for global libraries.
Indexed.

```python
owner_username = StringProperty(required=False, index=True)
```

The workspace endpoints (`/library/api/workspaces/…`) set this on
create and require `lib.owner_username == request.user.username` for
all mutations and reads; non-owners get 404 on GET/PUT and 204 on
DELETE (idempotent).

### 4.2 Daedalus (informational — managed in the Daedalus repo)

Unchanged from v1 except:

* `vault_mnemosyne_daedalus_service_password` is **gone**. Daedalus
  authenticates to Mnemosyne with a `UserToken` plaintext minted at
  `/profile/tokens/`, stored in whatever secret the operator wires
  (suggestion: `vault_mnemosyne_user_token`).
* Daedalus's HTTP client sends `Authorization: Bearer <plaintext>` to
  every Mnemosyne endpoint (`/library/api/*`, `/mcp_server/api/teams/*`,
  `/mcp/`). The `Token <key>` keyword is no longer accepted anywhere.

### 4.3 Pallas
Unchanged from v1. Static `Authorization: Bearer <team-jwt>` in
`fastagent.secrets.yaml`.

---

## 5. JWT claim shapes

Only one JWT shape remains — the team JWT from v1 §5.2:

```json
{
  "iss":  "mnemosyne",
  "aud":  "mnemosyne",
  "sub":  "team:<pallas_instance_uuid>",
  "typ":  "team",
  "iat":  1715000000,
  "exp":  1976000000,
  "jti":  "uuid4"
}
```

[`mnemosyne/mcp_server/teams.py:mint_team_jwt`](../mnemosyne/mcp_server/teams.py).

### 5.1 Validator changes vs v1

[`mnemosyne/mcp_server/auth.py`](../mnemosyne/mcp_server/auth.py):

* `resolve_mcp_jwt` no longer accepts `iss=daedalus`. The `_JTI_CACHE`
  replay cache still exists but is exercised by no live code path —
  scheduled for removal in a follow-up cleanup commit.
* `_resolve_jwt_actor` resolves to `team.owner` (the Mnemosyne user
  that created the team) rather than a synthetic service user. Audit
  log / usage accounting now correctly attribute each turn to the
  acting user.

```python
def _resolve_jwt_actor(claims: dict):
    if claims.get("typ") != "team":
        raise MCPAuthError("Per-turn JWTs are no longer accepted; mint a team JWT.")
    team = Team.objects.select_related("owner").get(pk=claims["team_id"])
    if not team.active:
        raise MCPAuthError("Team JWT references an inactive team.")
    if not team.owner.is_active:
        raise MCPAuthError("Team owner is disabled.")
    return team.owner
```

---

## 6. Auth flow

### 6.1 Third-party MCP client with `UserToken`
1. Client sends `Authorization: Bearer <plaintext>` to `/mcp/`.
2. `MCPAuthMiddleware` hashes → looks up `UserToken` → validates
   active/expired/user-active.
3. `resolved_libraries = list(token.allowed_libraries or [])`.
4. Fails closed if empty.

### 6.2 Agent team (Kottos / Mentor / Iolaus / Daedalus-chat-team)
1. Pallas sends `Authorization: Bearer <team-jwt>` to `/mcp/`.
2. Middleware validates signature, `iss=mnemosyne`, `typ=team`.
3. Loads `Team` by UUID from `sub`. Verifies `active=True` and
   `jti == active_jti`.
4. Expands to `resolved_libraries` via `TeamWorkspaceAssignment` →
   `Library.workspace_id`.
5. The acting user (for audit, usage accounting) is `team.owner`.

### 6.3 Daedalus REST control / ingest
1. Daedalus sends `Authorization: Bearer <user-token-plaintext>` to
   `/library/api/*` or `/mcp_server/api/teams/*`.
2. DRF `UserTokenAuthentication` (first in the auth stack) resolves
   the token to its user.
3. Endpoint scopes by `Team.owner` (mcp_server) or
   `Library.owner_username` (library). Non-owner ⇒ 404.

### 6.4 Browser / web session
SessionAuthentication runs second; cookie-authenticated users hit the
DRF browsable API as themselves with no special handling.

### 6.5 Failure modes

| Condition | Response |
|---|---|
| No `Authorization` header | 401 + `WWW-Authenticate: Bearer` |
| `Authorization: Token …` (legacy DRF keyword) | 401 (not consumed by any auth class) |
| Invalid bearer plaintext | 401 + `WWW-Authenticate: Bearer` |
| Inactive / expired token | 401 |
| Disabled user | 401 |
| JWT signature invalid | 401 + `WWW-Authenticate: Bearer` |
| JWT `exp` past (+30s leeway) | 401 |
| JWT `iss` not `mnemosyne` | 401 |
| JWT `typ` not `team` (legacy per-turn) | 401 ("per-turn JWTs no longer accepted") |
| Team inactive / unknown / `jti` stale | 401 |
| Team endpoint, non-owner caller | 404 |
| Workspace endpoint, non-owner caller (GET/PUT) | 404 |
| Workspace endpoint, non-owner caller (DELETE) | 204 (idempotent) |

---

## 7. REST API — Mnemosyne team lifecycle

Endpoints under `/mcp_server/api/teams/` are authenticated as the
Mnemosyne user the team belongs to via a per-user `UserToken`
(`Authorization: Bearer <plaintext>`, minted at `/profile/tokens/`).
Each team has an `owner` FK; non-owners receive 404 (never 403) so a
team's existence isn't disclosed across users.

### 7.1 `POST /mcp_server/api/teams/`
Create a team. `Team.owner` is set to `request.user`.

**Request**
```json
{ "id": "a3f1…", "name": "Kottos" }
```

**Response 201** — fresh id
```json
{ "id": "a3f1…", "name": "Kottos", "jwt": "eyJhbGci…" }
```

**Response 200** — same id, same owner (idempotent; no new JWT issued).
**Response 409** — same id, different owner ("Team id is already in use.").

### 7.2 `DELETE /mcp_server/api/teams/{id}/`
Soft-delete (`active=False`, clear `active_jti`). Old JWT invalid on
next call. Non-owner ⇒ 404.

### 7.3 `PUT /mcp_server/api/teams/{id}/workspaces/`
Replace the team's workspace assignment set. Idempotent.

```json
{ "workspace_ids": ["ws_abc", "ws_def"] }
```

### 7.4 `POST /mcp_server/api/teams/{id}/rotate/`
Generate a fresh `jti` and JWT, replace `active_jti`. Old JWT invalid
immediately.

**Upsert-on-missing.** If no `Team` exists for `id`, rotate creates one
owned by the caller (with `name = str(id)`) and mints its first JWT —
the operator clicks "Rotate JWT" in Daedalus settings and things just
work even if Daedalus's `provision_teams` workflow never ran for this
PallasInstance. The placeholder name can be edited via admin.

| Response | Condition |
|---|---|
| **200** + `jwt` | Same-owner id (rotates) or fresh id (upserts + mints) |
| **409** | `id` exists under a different owner (`"Team id is already in use."`) |
| **409** | Team is inactive (soft-deleted) — explicit recreate required |

The upsert path logs `team_rotate upserted_missing team_id=… owner=…`
at INFO. Surfacing this in metrics is a useful drift signal: Daedalus
and Mnemosyne fell out of sync on team provisioning.

### 7.5 `GET /mcp_server/api/teams/{id}/`
Read-only detail (no JWT). Used by the Daedalus reconciler.

### 7.6 `/library/api/ingest/` and `/library/api/jobs/…`
Same owner-scope model as the workspace endpoints: every ingest write,
job read, retry, and list filter against
`Library.owner_username == request.user.username` (global libraries
with null `owner_username` remain shared). Cross-user calls get 404
with the same "not registered" wording as a genuinely missing
workspace — existence is not disclosed across users. The list endpoint
silently filters; a `library_uid` the caller has no access to returns
an empty list rather than 404.

---

## 8. Daedalus lifecycle hooks

Unchanged from v1 §8 except the HTTP client now sends
`Authorization: Bearer <UserToken-plaintext>` and Daedalus's config
exposes one `UserToken` plaintext (one per Mnemosyne user the Daedalus
instance acts on behalf of, in deployments that multiplex).

---

## 9. Operator workflows

### 9.1 Register a new Pallas deployment
Unchanged from v1 §9.1.

### 9.2 Attach a Pallas team to a workspace
Unchanged from v1 §9.2.

### 9.3 Retire a Pallas deployment
Unchanged from v1 §9.3.

### 9.4 Rotate a compromised team JWT
Unchanged from v1 §9.4.

### 9.5 Provision Mnemosyne integration on a fresh Daedalus instance
Replaces v1 §9.5 (`provision_teams`) and the deleted
`ensure_service_user` flow:

1. **Mint a `UserToken` for the Mnemosyne user** Daedalus will act as:
   `/profile/tokens/add/` (UI) or
   `python manage.py create_user_token --user <username> --name "Daedalus"`.
   Copy the plaintext (shown once).
2. **Stage the plaintext in Daedalus's config** as the bearer for all
   Mnemosyne calls.
3. **Run Daedalus's `provision_teams`** to materialize a `Team` row in
   Mnemosyne for every existing `PallasInstance`.
4. **Distribute team JWTs** to each Pallas deployment as v1 §9.5
   describes.

### 9.6 Issue a `UserToken` for a third-party MCP client
1. User logs in to Mnemosyne, navigates to `/profile/tokens/`, clicks
   "Generate API Token".
2. (Optional) opens the "Restrictions (optional)" section to set
   `allowed_tools` / `allowed_libraries` — these apply only on
   `/mcp/`; for purely REST use they can stay empty.
3. Plaintext is shown once on the response page.
4. User pastes plaintext into the third-party client's config (Claude
   Desktop, Cline, etc.) with `Authorization: Bearer …`.

The same UI and command (`create_user_token`) mint tokens for any
purpose — Daedalus, MCP clients, scripts, CI. There is no separate
"DRF token" category.

---

## 10. UX changes in Daedalus

Unchanged from v1 §10.

---

## 11. Migration

### 11.1 State at the start of v2

* Mnemosyne is not in a production deployment; migrations are reset on
  schema changes and the project assumes a clean DB on the next
  release.
* Daedalus has already migrated to `Authorization: Bearer <plaintext>`
  and is configured to use a per-user token; the v1 DRF-token shim is
  no longer used at runtime.
* No live Pallas deployments authenticate via per-turn JWT (the path
  is removed).

### 11.2 Order of operations

1. **Mnemosyne v2 deploys.** New `UserTokenAuthentication`, owner-scoped
   REST endpoints, retired per-turn JWT validation, removed
   `authtoken` app. Operator mints a `UserToken` for Daedalus's
   Mnemosyne account before deploy.
2. **Daedalus's config swap.** Operator points Daedalus at the new
   `UserToken` plaintext. (If Daedalus was still sending
   `Authorization: Token …`, switch to `Authorization: Bearer …` at
   the same time.)
3. **Existing Teams.** None expected at the v2 cutover (migrations are
   reset). If any existed, `Team.owner` would need backfill; not in
   scope.

### 11.3 Rollback
Mnemosyne v2 is a coordinated cutover with Daedalus's bearer-header
swap. Rolling Mnemosyne back to v1 without rolling Daedalus back too
means Daedalus's `Authorization: Bearer …` won't be recognised on
`/library/api/*` (v1 only accepted `Token`). Plan the deploy as a
single window.

---

## 12. Deprecated / removed in v2

### Mnemosyne
* `rest_framework.authtoken` (removed from `INSTALLED_APPS`).
  Generated migration drops the `authtoken_token` table on next migrate;
  on a reset schema there's nothing to drop.
* `rest_framework.authentication.TokenAuthentication` and
  `BasicAuthentication` (removed from
  `REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]`).
* "API Token" card on `/profile/settings/` (removed). The whole
  `api_token_regenerate` view + URL are gone.
* `mcp_server.management.commands.ensure_service_user` (deleted).
* `daedalus-service` user (no longer provisioned by Mnemosyne; no
  longer assumed by any endpoint).
* `MCP_JWT_SERVICE_USERNAME` setting (no longer read by
  `_resolve_jwt_actor`).
* Per-turn JWT path in
  [`mcp_server/auth.py`](../mnemosyne/mcp_server/auth.py) — accepted
  shapes shrink to `typ=team` only. `_JTI_CACHE` is now exercised by
  no live path; scheduled for cleanup.
* `MCPToken` (renamed to `UserToken`); `MCPTokenManager`,
  `MCPTokenAdmin`, `MCPTokenCreateForm`, `MCPTokenEditForm` (renamed
  in lockstep). The `mcp_…` masked-token prefix becomes `tok_…`.
* `create_mcp_token` management command (renamed `create_user_token`).
* `/profile/mcp-tokens/` URL prefix (renamed `/profile/tokens/`); URL
  names `mcp-token-*` (renamed `token-*`).

### Daedalus
* `vault_mnemosyne_daedalus_service_password` (no longer needed; the
  service user is gone).
* Any code path that distinguished DRF-`Token` from MCP-`Bearer` — one
  bearer header for everything now.

### Pallas
No changes from v1.

---

## 13. Security

### 13.1 Token lifetimes
* **`UserToken`**: until revoked (user) or `expires_at`. Rotation is
  manual via the `/profile/tokens/` dashboard.
* **Team JWT**: 10 years. Revocation via `Team.active`,
  `Team.active_jti`, or key rotation.

### 13.2 Revocation levers
1. `PUT /teams/{id}/workspaces/` with `[]` — team sees nothing, JWT
   still validates. Useful for pausing without redistributing tokens.
2. `DELETE /teams/{id}/` — team inactive, all its JWTs rejected.
3. `POST /teams/{id}/rotate/` — `active_jti` changes; leaked JWT
   stops working.
4. **Revoke a `UserToken`** — `/profile/tokens/{id}/revoke/` flips
   `is_active=False`; immediate effect for both `/mcp/` and REST.
5. `MCPSigningKey.retire()` — nuclear option for team JWTs.

### 13.3 At-rest protection
* `UserToken.token_hash`: SHA-256 of plaintext; plaintext never
  stored.
* `MCPSigningKey.secret_hex`: 256-bit hex secret stored in Mnemosyne
  DB only.
* `PallasInstance.team_jwt_encrypted`: Fernet-encrypted by Daedalus.

### 13.4 Audit attribution
Every authenticated request resolves to a real Mnemosyne user:

* Opaque `UserToken` → `token.user`.
* Team JWT → `team.owner`.

Both flow through to usage accounting (`LLMUsage`, search metrics) and
the audit log. The synthetic `daedalus-service` actor is gone; nothing
in the audit trail is attributed to a non-user account.

Notable audit events:

* `team_create created team_id=… name=…` — fresh team registered.
* `team_create idempotent_hit team_id=…` — same-owner re-POST.
* `team_create owner_conflict team_id=… caller=…` — id collision.
* `team_rotate team_id=… new_jti=…` — explicit rotation.
* `team_rotate upserted_missing team_id=… owner=…` — rotate created a
  missing team on the fly. Useful drift signal: Daedalus and
  Mnemosyne fell out of sync on team provisioning.
* `team_delete team_id=…` — soft-delete.

### 13.5 Isolation model
Unchanged from v1 §13.5.

---

## 14. Testing

### 14.1 Mnemosyne test surface (relevant to v2)
* `resolve_mcp_jwt` rejects `iss=daedalus` / non-`team` payloads.
* `_resolve_jwt_actor` resolves to `team.owner`; rejects per-turn JWTs
  and inactive owners. See
  [`test_auth.py::ResolveJWTActorTest`](../mnemosyne/mcp_server/tests/test_auth.py).
* `UserTokenAuthentication` issues 401 + `WWW-Authenticate: Bearer`
  for anonymous and rejected-token cases; 200 for valid bearer; stashes
  the `UserToken` on `request.auth`. See
  [`test_drf_auth.py`](../mnemosyne/mcp_server/tests/test_drf_auth.py).
* `Team` endpoints scope by `owner`; cross-user GET/DELETE/PUT return
  404; same-id different-owner POST/rotate returns 409. `rotate`
  upserts a missing team owned by the caller. See
  [`test_teams_api.py`](../mnemosyne/mcp_server/tests/test_teams_api.py).
* Ingest endpoints (`POST /library/api/ingest/`,
  `GET/POST /library/api/jobs/…`) scope by `Library.owner_username`.
  Cross-user writes/reads return 404; list silently filters. The
  Cypher-touching paths require Neo4j, so the scoping is exercised by
  the manual e2e plan in §14.3 rather than unit tests.
* `UserToken` model: hash-at-rest, `tok_…` masked prefix,
  `allowed_libraries` round-trip. See
  [`test_token.py`](../mnemosyne/mcp_server/tests/test_token.py),
  [`test_models.py`](../mnemosyne/mcp_server/tests/test_models.py).

### 14.2 Daedalus test surface
Unchanged from v1 §14.2 except:
* HTTP client uses `Authorization: Bearer …` against every Mnemosyne
  endpoint.
* Provisioning command depends on a configured `UserToken`, not the
  retired `daedalus-service` Basic-auth credential.

### 14.3 Integration
* End-to-end: MCP client with `UserToken` → search scoped to
  `token.allowed_libraries`.
* End-to-end: Pallas with team JWT → search scoped to team's attached
  workspaces.
* End-to-end: Daedalus REST call with `UserToken` → workspace
  mutation succeeds only for the owning user; cross-user attempts get
  404.
* End-to-end: ingest as one user, then a *different* user attempts
  `POST /library/api/ingest/`, `GET /jobs/{id}/`, `POST /jobs/{id}/retry/`
  and `GET /jobs/?library_uid=<theirs>` — first three return 404, the
  list returns an empty array.
* End-to-end: anonymous REST call → 401 + `WWW-Authenticate: Bearer`.
* End-to-end: `POST /mcp_server/api/teams/{fresh-uuid}/rotate/` on a
  team Mnemosyne has never seen → 200 + JWT, `Team` row created with
  `owner=request.user`. Second rotate on the same id → 200 with a
  fresh `active_jti`. Rotate on an id owned by a different user → 409.

---

## 15. Phased delivery

| # | Phase | Surface | Status |
|---|---|---|---|
| 1 | Design v1 | [`DAEDALUS_PALLAS_INTEGRATION_v1.md`](DAEDALUS_PALLAS_INTEGRATION_v1.md) | Superseded |
| 2 | Mnemosyne core | `LibraryMembership`, `MCPToken`, `Team`, `TeamWorkspaceAssignment`, `/mcp_server/api/teams/`, team JWT mint | Implemented (v1) |
| 3 | Pallas cleanup | Remove `_fastagent_patch.py` internals | Implemented (v1) |
| 4 | Daedalus integration | Lifecycle hooks, reconciler, `provision_teams`, attached-teams UI | Implemented (v1) |
| 5 | Per-user REST authorization | `Team.owner`, `Library.owner_username`, owner-scope on all Daedalus-facing endpoints, `_resolve_jwt_actor` → `team.owner` | Implemented (v2) |
| 6 | Token consolidation | Rename `MCPToken` → `UserToken`, `UserTokenAuthentication` DRF class, drop `authtoken` + DRF Token UI, retire per-turn JWT, `Bearer`-first auth stack | Implemented (v2) |
| 7 | Documentation | This file; updates to [`mnemosyne_integration.md`](mnemosyne_integration.md) and [`deploy.md`](deploy.md) | Implemented (v2) |

---

## 16. Open items (v2)

* `_JTI_CACHE` in [`auth.py`](../mnemosyne/mcp_server/auth.py) is dead
  code (the per-turn replay path is gone). Cleanup commit pending; not
  blocking.
* `BasicAuthentication` is removed from the DRF default stack. If any
  internal tooling relied on it, that path is now broken and will need
  an explicit re-add to the relevant viewset's `authentication_classes`
  rather than the global default.

---

## 17. Cross-references

* Mnemosyne MCP auth: [`mnemosyne/mcp_server/auth.py`](../mnemosyne/mcp_server/auth.py).
* Mnemosyne DRF auth class: [`mnemosyne/mcp_server/drf_auth.py`](../mnemosyne/mcp_server/drf_auth.py).
* Mnemosyne token model: [`mnemosyne/mcp_server/models.py`](../mnemosyne/mcp_server/models.py) (`UserToken`).
* Mnemosyne team REST: [`mnemosyne/mcp_server/api/teams.py`](../mnemosyne/mcp_server/api/teams.py).
* Mnemosyne workspace REST: [`mnemosyne/library/api/workspaces.py`](../mnemosyne/library/api/workspaces.py).
* Token self-service dashboard: [`mnemosyne/mcp_server/views.py`](../mnemosyne/mcp_server/views.py), [`urls.py`](../mnemosyne/mcp_server/urls.py).
* `create_user_token` management command: [`mnemosyne/mcp_server/management/commands/create_user_token.py`](../mnemosyne/mcp_server/management/commands/create_user_token.py).
* v1 design (superseded but kept for history): [`DAEDALUS_PALLAS_INTEGRATION_v1.md`](DAEDALUS_PALLAS_INTEGRATION_v1.md).
