Document the end-state auth/authz model unifying the three services around a bearer → resolved library set abstraction. Replaces the per-turn JWT forwarding scheme with static team JWTs held by Pallas deployments, eliminating custom transport code and the monkey-patch chain that caused opaque failures in agent teams. Also records the UX shift where Daedalus workspaces attach Teams (Pallas instances) rather than individual agents.
28 KiB
Daedalus ↔ Pallas ↔ Mnemosyne Integration — v1
Status: Draft / approved design
Authoritative home: mnemosyne/docs/DAEDALUS_PALLAS_INTEGRATION_v1.md
Versioning: subsequent major revisions ship as ..._v2.md, ..._v3.md
alongside this file rather than overwriting it. Cross-service docs
(Daedalus, Pallas) link here rather than duplicating.
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. Single-user per instance. Registers Pallas instances, syncs file content to Mnemosyne, drives chat.
- Pallas — FastAgent-backed MCP host that exposes agent teams (Kottos, Mentor, Iolaus, Daedalus-chat, …) as HTTP MCP servers.
The model replaces the per-turn JWT forwarding scheme with a unified
bearer → resolved library set abstraction. Every authenticated
Mnemosyne request resolves to a set of Library UIDs the caller may
read; the principal type (opaque MCPToken, Daedalus per-turn JWT,
team JWT) only determines how that set is derived.
It also records the UX shift in Daedalus: workspaces attach Teams (Pallas instances), not individual agents; the agent picker in chat is filtered by the workspace's attached teams.
2. Motivation
The previous design forwarded a Daedalus-minted per-turn JWT through
Pallas to Mnemosyne via a custom _DynamicBearerAuth, a ContextVar,
a YAML scanner (_refresh_forward_servers), a header-mutation
monkey-patch, and three trace wrappers in
pallas/pallas/_fastagent_patch.py. When something broke on this
path, tracebacks landed nowhere visible because fast-agent's
MCPAggregator._execute_on_server.try_execute swallowed exceptions
(except Exception as e: logger.error(…str(e)…); return error_factory(...)),
and we were monkey-patching under it.
The failure mode (agent teams like Harper going into infinite token-burning loops when Mnemosyne was unhappy) combined with the diagnostic opacity made this architecture unsustainable. Per-turn forwarding was also the wrong shape for non-interactive agent teams that have no user session per call.
This design eliminates forwarding. Each Pallas deployment carries a
static, long-lived team JWT in its own fastagent.secrets.yaml. No
custom transport code in Pallas. Authorization happens server-side in
Mnemosyne against live DB rows.
3. Architecture
3.1 Services and responsibilities
| Service | Role in auth model |
|---|---|
| Mnemosyne | Owns Libraries, Library memberships, MCPTokens, Teams, TeamWorkspaceAssignments, 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. |
| Pallas | Stateless MCP host. Holds a static team JWT in fastagent.secrets.yaml. No custom auth-forwarding code. |
3.2 Three credential types
Every Mnemosyne MCP call presents a Bearer token that falls into one of three categories:
| # | Credential | iss |
Issuer | Lifetime | Library scope source |
|---|---|---|---|---|---|
| 1 | Opaque MCPToken |
n/a | Mnemosyne admin | Until revoked | MCPToken.allowed_libraries (M2M, set at mint) |
| 2 | Per-turn JWT | daedalus |
Daedalus | Minutes | libs claim (baked in at mint) |
| 3 | Team JWT | mnemosyne |
Mnemosyne | 10 years | Live DB lookup via Team.workspaces → Library |
Category 2 is used only by Daedalus chat. Once Daedalus-chat itself registers as a Pallas Team in Phase 4, category 2 retires entirely and the design collapses to two credential types.
3.3 Resolved-library abstraction
Mnemosyne's auth middleware populates a single
resolved_libraries: set[str] per request. Downstream code (search,
get_document, list_libraries, etc.) only reads that set; it does not
care where the set came from.
Bearer → classify → dispatch
├─ Opaque MCPToken → allowed_libraries M2M
├─ per-turn JWT → claims["libs"]
└─ team JWT (typ=team) → live DB: team.workspaces → libraries
(filtered by Library.workspace_id)
↓
resolved_libraries: set[str]
↓
downstream tools
Fail-closed: if the resolution produces an empty set, the request sees no Libraries. There is no "empty means everything" path.
4. Data model
4.1 Mnemosyne additions
LibraryMembership (new)
class LibraryMembership(models.Model):
user = FK(User, related_name="library_memberships")
library_uid = CharField(max_length=64, db_index=True) # neo4j Library.uid
role = CharField(choices=[("owner","owner"),
("manager","manager"),
("reader","reader")])
created_at = DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("user", "library_uid", "role")
- owner — full control: rename, delete, reassign ownership, grant/revoke any role.
- manager — can grant
readerand scope Library into MCPTokens they own, but cannot delete the library or remove other owners. - reader — can read via their own MCPTokens; cannot grant to other users.
User can scope a Library into MCPToken.allowed_libraries iff they
have owner or manager role on it.
MCPToken.allowed_libraries (new M2M on existing model)
allowed_libraries = models.ManyToManyField(Library, blank=True)
Fail-closed: empty → token grants access to zero libraries. Admin form filters the picker by the current user's owned/managed memberships.
Team (new)
class Team(models.Model):
id = UUIDField(primary_key=True, editable=False)
# = Daedalus PallasInstance.id. Stays stable across redeploy /
# rehost of a given Pallas instance.
name = CharField(max_length=200) # display; mirrored from Daedalus
active = BooleanField(default=True)
active_jti = UUIDField(null=True) # current valid jti
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
TeamWorkspaceAssignment (new)
class TeamWorkspaceAssignment(models.Model):
team = FK(Team, related_name="workspace_assignments",
on_delete=CASCADE)
workspace_id = CharField(max_length=64) # matches Library.workspace_id
created_at = DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("team", "workspace_id")
No library-level assignment for teams. Teams gain access to all libraries of their assigned workspaces. If finer control is ever needed later, it layers on without disturbing this model.
MCPSigningKey (existing, unchanged)
Re-used to sign team JWTs. The same signing key can back both per-turn tokens (pre-retirement) and team tokens (long-lived).
4.2 Daedalus additions
PallasInstance.team_jwt_encrypted (new column, text, Fernet)
Stores the team JWT received from Mnemosyne at registration time.
Fernet-encrypted at rest using the same pattern as
daedalus/llm_manager/encryption.py. Displayed plaintext exactly once
in the admin detail page immediately after provisioning, so the
operator can copy it into fastagent.secrets.yaml on the Pallas
deployment.
PallasInstance.pallas_team_mnemosyne_status (new column)
NULL | "pending" | "provisioned" | "failed". Drives the reconciler;
analogous to the existing WorkspaceFile.mnemosyne_status.
workspace_pallas_assignments (new M2M table)
CREATE TABLE workspace_pallas_assignments (
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
pallas_instance_id TEXT NOT NULL REFERENCES pallas_instances(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (workspace_id, pallas_instance_id)
);
Starts empty on workspace create. Operator explicitly attaches Pallas instances (Teams) before any Mnemosyne-backed agent — including Daedalus chat — can search that workspace.
4.3 Nothing changes in Pallas
After cleanup, pallas/pallas/_fastagent_patch.py either becomes an
empty placeholder or is removed entirely. pallas/__init__.py no
longer invokes install(). Pallas deployments configure stock
fast-agent with a static Authorization: Bearer <team-jwt>.
5. JWT claim shapes
5.1 Per-turn JWT (category 2 — legacy, retires in Phase 4)
{
"iss": "daedalus",
"aud": "mnemosyne", // optional, not enforced
"sub": "daedalus-chat",
"iat": 1715000000,
"exp": 1715000600, // ≤ 10 minutes
"jti": "uuid4",
"ws": "ws_abc", // Daedalus workspace id
"libs": ["lib_xxx", "lib_yyy"] // user-managed libraries
}
Kept unchanged during Phase 2–3 so Daedalus chat continues to work while we ship the team infrastructure.
5.2 Team JWT (category 3 — new)
{
"iss": "mnemosyne",
"aud": "mnemosyne",
"sub": "team:<pallas_instance_uuid>", // UUID; Daedalus id
"typ": "team", // distinguishes from per-turn
"iat": 1715000000,
"exp": 1976000000, // +10 years
"jti": "uuid4"
}
No ws, no libs. Authorization is evaluated live against
TeamWorkspaceAssignment rows on every request.
5.3 Validator changes in mcp_server/auth.py
# Accept both issuers; distinguish paths by typ.
_JWT_ISS = {"daedalus", "mnemosyne"}
def resolve_mcp_jwt(token_string: str) -> dict:
... # validate signature, iat/exp, required claims including sub
typ = claims.get("typ")
if typ == "team":
# No replay cache — team tokens are reused on every request.
pass
else:
if _remember_jti(jti, float(exp)):
raise MCPAuthError("Token replay detected.")
return claims
Downstream, the middleware branches:
if claims.get("typ") == "team":
team = Team.objects.get(id=uuid_from_sub(claims["sub"]),
active=True,
active_jti=claims["jti"])
resolved_libraries = _libraries_for_team(team)
else:
resolved_libraries = claims["libs"]
_libraries_for_team(team) = all Library UIDs whose workspace_id
is in the team's TeamWorkspaceAssignment set.
6. Auth flow
6.1 Third-party MCP client with opaque MCPToken
- Client sends
Authorization: Bearer <plaintext>. - Middleware hashes → looks up
MCPToken→ validates active/expired. resolved_libraries = token.allowed_libraries.values_list("uid").- Fails closed if empty.
6.2 Daedalus chat per-turn JWT (legacy, retires Phase 4)
Unchanged from today. iss=daedalus, typ absent, libs carries the
workspace's user-managed libraries, ws carries the workspace id.
Mnemosyne validates against MCPSigningKey keyed by kid.
6.3 Agent team (Kottos / Mentor / Iolaus / post-migration Daedalus-chat)
- Pallas sends
Authorization: Bearer <team-jwt>(static, read fromfastagent.secrets.yaml). - Middleware validates signature → detects
typ=team. - Reads
Teamrow by UUID fromsub. Verifiesactive=Trueandjti == active_jti. Rejects otherwise. - Expands to
resolved_librariesviaTeamWorkspaceAssignment→Library.workspace_id. - Fails closed if the team has no workspaces attached.
6.4 Failure modes
| Condition | Response |
|---|---|
| JWT signature invalid | PermissionError("Invalid MCP token.") |
exp past (+30s leeway) |
PermissionError("Token has expired.") |
iss not in _JWT_ISS |
PermissionError("Invalid token issuer.") |
typ=team, team not found |
PermissionError("Invalid MCP token.") |
typ=team, team not active |
PermissionError("Token has been deactivated.") |
typ=team, jti stale |
PermissionError("Invalid MCP token.") |
| Opaque token not found | PermissionError("Invalid MCP token.") |
| Opaque token, inactive user | PermissionError("User account is disabled.") |
| Resolved library set empty | Tool call proceeds but returns empty — this is authorization, not authentication, and the caller is legitimately scoped to nothing. |
7. REST API — Mnemosyne team lifecycle
All endpoints live under /mcp_server/api/teams/ and are protected
by the existing daedalus-service HTTP Basic account (same auth as
/library/api/workspaces/ and /library/api/ingest/).
7.1 POST /mcp_server/api/teams/
Create a team.
Request
{
"id": "a3f1…", // UUID; mirrors Daedalus PallasInstance.id
"name": "Kottos"
}
Response 201
{
"id": "a3f1…",
"name": "Kottos",
"jwt": "eyJhbGci…" // shown once; not recoverable later
}
On id collision: idempotent — returns existing team without the
JWT. Caller must call /rotate to get a new one.
7.2 DELETE /mcp_server/api/teams/{id}/
Soft-delete. Sets active=False. Old JWT invalid on next call.
7.3 PUT /mcp_server/api/teams/{id}/workspaces/
Replace the team's workspace assignment set. Idempotent.
Request
{ "workspace_ids": ["ws_abc", "ws_def"] }
Response 200
{ "workspace_ids": ["ws_abc", "ws_def"] }
Non-existent workspaces silently accepted (they become active if/when
a Library with that workspace_id is later created). Mirrors the
Daedalus source of truth.
7.4 POST /mcp_server/api/teams/{id}/rotate/
Generate a fresh jti and JWT, replace active_jti. Old JWT invalid
immediately.
Response 200
{ "jwt": "eyJhbGci…" }
7.5 GET /mcp_server/api/teams/{id}/
Read-only team detail (no JWT). Used by Daedalus reconciler to confirm state.
Response 200
{
"id": "a3f1…",
"name": "Kottos",
"active": true,
"active_jti": "…", // for diagnostics, not a credential
"workspace_ids": ["ws_abc"]
}
8. Daedalus lifecycle hooks
Mirrors the pattern in daedalus/backend/daedalus/mnemosyne/lifecycle.py:
every hook is best-effort, logs errors without blocking the local
operation, and is retried by the reconciler.
8.1 on_pallas_registered(instance)
POST /mcp_server/api/teams/withid=instance.id,name=instance.name.- Encrypt JWT via Fernet; store on
instance.team_jwt_encrypted; setpallas_team_mnemosyne_status="provisioned". - Log
pallas_team_provisioned.
On failure: status "failed", reconciler retries.
8.2 on_pallas_deleted(instance_id)
DELETE /mcp_server/api/teams/{id}/. Row cascade locally; Mnemosyne
soft-deletes. Best-effort.
8.3 on_workspace_pallas_attached(workspace_id, instance_id)
- Read all current
workspace_pallas_assignmentswherepallas_instance_id=instance_id. PUT /mcp_server/api/teams/{id}/workspaces/with the resulting workspace_id list.
8.4 on_workspace_pallas_detached(workspace_id, instance_id)
Symmetric to 8.3.
8.5 on_workspace_deleted(workspace_id)
For every attached Pallas instance, recompute and push updated workspace list (so teams lose the deleted workspace).
8.6 Reconciler extension
Extends
daedalus/backend/daedalus/mnemosyne/reconciler.py:
- Re-runs
POST /teams/for instances with status NULL or"failed". - Re-syncs workspace assignments for all instances on every cycle (cheap idempotent PUT; guards against silent drift).
9. Operator workflows
9.1 Register a new Pallas deployment
- Operator adds entry in Daedalus admin:
POST /api/v1/pallaswith the Pallas registry URL. Daedalus fetches the registry, creates thePallasInstancerow, then calls MnemosynePOST /mcp_server/api/teams/. The JWT lands ininstance.team_jwt_encrypted. - Daedalus admin detail page surfaces the JWT plaintext once (decrypted client-side or via a one-shot "reveal" endpoint that logs the access). Operator copies it.
- On the Pallas deployment machine, operator pastes the JWT into
fastagent.secrets.yaml:Operator removes any stalemcp: servers: mnemosyne: transport: http url: https://mnemosyne.example.helu.ca/mcp/ headers: Authorization: Bearer eyJhbGci…forward_inbound_auth: truefrom the corresponding entry infastagent.config.yaml. Restart Pallas.
9.2 Attach a Pallas team to a workspace
- Daedalus workspace settings → "Attached Teams" → multi-select across registered Pallas instances → save.
- Daedalus fires
on_workspace_pallas_attached, Mnemosyne'sTeamWorkspaceAssignmentupdates. - Agent picker in chat immediately shows agents from that team for this workspace.
9.3 Retire a Pallas deployment
- Daedalus admin → delete PallasInstance.
- Daedalus calls
DELETE /mcp_server/api/teams/{id}/; row marked inactive in Mnemosyne. JWT rejected on next call. - Operator shuts down the Pallas deployment.
9.4 Rotate a compromised team JWT
- Daedalus admin → "Rotate team JWT" action on the PallasInstance.
- Daedalus calls
POST /mcp_server/api/teams/{id}/rotate/, re-encrypts and stores the new JWT. - Operator copies the new JWT into the Pallas deployment's
fastagent.secrets.yaml, restarts.
9.5 Provision existing Pallas instances (one-time migration)
After Mnemosyne phase 2 deploys:
$ daedalus manage.py provision_teams
Walks all existing PallasInstance rows, calls
POST /mcp_server/api/teams/ for each, stores + prints JWTs in a
table for the operator to distribute. Idempotent: rows already
provisioned are skipped.
9.6 Issue an MCPToken for a third-party MCP client
- Mnemosyne admin → MCPTokens → add. Pick user. Library picker is
filtered to libraries where that user has
ownerormanagermembership. - Submit. Plaintext shown once on the response page.
- Operator pastes the plaintext into the third-party client's config (Claude Desktop, Cline, etc.).
10. UX changes in Daedalus
10.1 Workspace → attached Teams
Today: workspaces accumulate AgentConnection rows across every
registered Pallas instance; the agent picker is long, sub-agents
share names with parents (e.g. two distinct "Harper" agents), and it
is unclear which workspace grants which agents Mnemosyne access.
New:
- Workspace settings has a new section Attached Teams, a
multi-select over registered
PallasInstancerows. Starts empty on workspace create. - Attaching/detaching a team triggers the lifecycle hook that updates
Mnemosyne's
TeamWorkspaceAssignmentfor that team.
10.2 Agent picker in chat
New behavior:
- Lists only agents belonging to teams attached to the current workspace.
- Displays each agent as
TeamName › AgentTitleso sub-agent name collisions resolve visually. - Empty state: "No teams attached to this workspace. Go to workspace settings → Attached Teams." (links inline).
10.3 Agent switching during chat
Unchanged mechanically: each chat turn may target a different
agent_server_id. New constraint: the target agent must belong to a
team currently attached to the workspace. The REST endpoint validates
this and returns 403 otherwise. The frontend picker already enforces
it as a UX affordance.
11. Migration
11.1 Current state (pre-migration)
- Mnemosyne is currently not in a released/working deployment; a fresh rollout is possible.
- Daedalus has existing
PallasInstancerows (registered viaPOST /api/v1/pallas) but none have a team JWT. - Kottos / Mentor / Iolaus each carry
forward_inbound_auth: trueinfastagent.config.yamland currently rely on the Pallas forwarding patch to pass Daedalus's per-turn JWT to Mnemosyne.
11.2 Order of operations (must follow)
- Mnemosyne phase 2 deploys. REST
/mcp_server/api/teams/is live; old per-turn JWT path still works. No consumers yet. - Daedalus phase 4 deploys. New columns + lifecycle hooks +
provision_teamscommand. On upgrade, migration creates columns with default NULL status; existing PallasInstances remain functional on the legacy (non-Mnemosyne) path. - Operator runs
provision_teams. Every existing PallasInstance gets a team in Mnemosyne and a stored JWT. - Operator distributes JWTs to each Pallas deployment
(Kottos / Mentor / Iolaus / Daedalus-chat). Each deployment
updates
fastagent.secrets.yaml, removesforward_inbound_auth: true, restarts. - Pallas phase 3 cleanup deploys. Forwarding infrastructure removed from Pallas codebase. Safe only after all deployments have switched to static JWTs.
- Daedalus per-turn token path retires.
mnemosyne/tokens.pyand its config (MNEMOSYNE_SIGNING_SECRET,MNEMOSYNE_SIGNING_KID,MNEMOSYNE_TOKEN_TTL_SECONDS) are removed after Daedalus chat's own team JWT is in place.
11.3 Rollback story
- Mnemosyne phase 2: safe to roll back — old per-turn JWT path untouched; new endpoints simply disappear.
- Daedalus phase 4: safe to roll back until
provision_teamshas run. After that, the JWTs are already distributed; rolling back means the JWTs go unused but nothing breaks. - Pallas phase 3: not safe to roll back independently — if any
deployment still has
forward_inbound_auth: truepointing at code that no longer exists, that deployment fails to start. Sequence correctly.
12. Deprecated / removed
At end-of-migration (after Phase 6):
Pallas (pallas/pallas/_fastagent_patch.py)
_DynamicBearerAuth(httpx Auth subclass)_CurrentBearerContextVar plumbing_refresh_forward_servers()YAML scanner_prepare_headers_and_auth_with_forward(the header-mutation monkey-patch)_send_request_with_trace,_session_call_tool_with_trace,_execute_on_server_with_trace(diagnostic wrappers installed because the forwarding path was opaque)install()function in_fastagent_patch;pallas/__init__.pyno longer auto-installs
Agent team configs (kottos/, mentor/, iolaus/)
forward_inbound_auth: trueunder any server stanza infastagent.config.yaml
Daedalus (daedalus/backend/daedalus/)
mnemosyne/tokens.py(per-turn JWT mint)- Config settings:
MNEMOSYNE_SIGNING_SECRET,MNEMOSYNE_SIGNING_KID,MNEMOSYNE_TOKEN_TTL_SECONDS,MNEMOSYNE_MCP_URL
Mnemosyne (mnemosyne/mcp_server/)
_JWT_ISSconstant-string (replaced by set containingdaedalusandmnemosyne;daedalusentry removed in a later version once per-turn path is deleted)- The
_JTI_CACHEreplay logic continues to exist for the per-turn path until that path retires; team JWTs bypass it entirely
13. Security
13.1 Token lifetimes
- Opaque MCPToken: until revoked (admin). Rotation is manual.
- Per-turn JWT: ≤ 10 minutes (existing
MNEMOSYNE_TOKEN_TTL_SECONDS). Retires with Phase 4 completion. - Team JWT: 10 years. Rationale: operator cannot tolerate a
silent expiry-induced outage in a year. Revocation is explicit via
Team.active,Team.active_jti, or key rotation.
13.2 Revocation levers, in order of granularity
PUT /teams/{id}/workspaces/with[]— team sees nothing, JWT still validates. Useful for pausing a deployment without redistributing tokens.DELETE /teams/{id}/— team marked inactive. All its tokens rejected. Restoring requires re-POST (new id) or admin DB edit.POST /teams/{id}/rotate/—active_jtichanges; the token that leaked stops working; the new JWT must be distributed.MCPSigningKey.retire()— nuclear option. All JWTs signed with that kid stop validating. Re-key + re-issue every team token.
13.3 At-rest protection
MCPToken.token_hash: SHA-256 of plaintext; plaintext never stored.MCPSigningKey.secret_hex: 256-bit hex secret stored in Mnemosyne DB only (not distributed).PallasInstance.team_jwt_encrypted: Fernet-encrypted by Daedalus'sSECRET_KEY(orMNEMOSYNE_FERNET_KEYif configured); ciphertext at rest.
13.4 Audit points
- Every auth failure increments
mcp_auth_failures_total{reason=…}. - Every team lifecycle action logs
pallas_team_{provisioned, deleted, rotated, workspaces_updated}in Daedalus with full correlation IDs. - Every bearer resolution logs the principal type + resolved library count at DEBUG (INFO until shakedown stabilizes).
13.5 Isolation model
Separation of "work" vs "personal" agents composes from three independent mechanisms:
- Per-token scope (Mnemosyne): each MCPToken carries its own
allowed_libraries. A personal token and a work token may belong to the same user yet see disjoint Library sets. - Per-workspace attachment (Daedalus): a Pallas instance only sees workspaces explicitly attached to it. Work Pallas and personal Pallas attach to disjoint workspaces.
- Per-Daedalus-instance (deployment): the strongest isolation is two Daedalus deployments pointing at distinct Mnemosyne accounts. Nothing the operator does in deployment A reaches any data accessible to deployment B.
For typical operator isolation, (1) + (2) suffice. (3) is the escape hatch for hard compartmentalization.
14. Testing
14.1 Mnemosyne test surface
resolve_mcp_jwtacceptsiss in {daedalus, mnemosyne}.typ=teambranch: bypasses replay cache; resolves team + active_jti.typ=team: rejects if team missing, inactive, orjtistale.LibraryMembership: owner can grant; reader cannot; grant form filters correctly.MCPToken.allowed_librariesempty → resolved library set empty.TeamWorkspaceAssignmentPUT is idempotent and replaces, not unions./mcp_server/api/teams/endpoints: create, delete, rotate, workspaces PUT, all authenticated asdaedalus-service.
14.2 Daedalus test surface
on_pallas_registeredpopulatesteam_jwt_encryptedand transitions status toprovisioned.on_workspace_pallas_attachedtriggers the correct PUT payload.- Agent-picker endpoint filters
AgentConnectionby attached Pallas instances. provision_teamsis idempotent.
14.3 Integration
- End-to-end: third-party MCP client with MCPToken → Mnemosyne
search scoped to
allowed_libraries. - End-to-end: Pallas agent with team JWT → Mnemosyne search scoped to team's attached workspaces.
- End-to-end: workspace detached from team → agent no longer sees that workspace's libraries (on next request, not stale-cached).
15. Phased delivery
| # | Phase | Surface | Deployable independently? |
|---|---|---|---|
| 1 | Design doc | This file | Yes — this document |
| 2 | Mnemosyne | LibraryMembership, MCPToken.allowed_libraries, Team, TeamWorkspaceAssignment, unified auth.py resolver, /mcp_server/api/teams/ REST, admin UIs, backfill, tests |
Yes — old per-turn JWT path untouched |
| 3 | Pallas cleanup | Remove _fastagent_patch.py internals, docs |
No — must wait until all deployments use static JWTs |
| 4 | Daedalus integration | workspace_pallas_assignments, team_jwt_encrypted, pallas_team_mnemosyne_status, lifecycle hooks, reconciler, provision_teams, admin API, agent-picker filter, register chat as team |
Yes — new columns nullable, legacy path still works |
| 5 | Daedalus frontend | Workspace settings attached-teams picker, agent picker namespacing | Yes — backwards-compatible once phase 4 ships |
| 6 | Agent team cutovers | Kottos / Mentor / Iolaus paste JWT, remove forward_inbound_auth, restart |
Yes — one at a time |
| 7 | Documentation | Mnemosyne README, Pallas README + docs/auth.md, Daedalus operator docs, cross-references to this file |
Yes |
16. Open items (v1)
None — all decisions are closed in this revision. Future revisions
that add scope (e.g., team-level library scoping finer than
workspace granularity; OAuth 2.1 for external MCP clients;
per-library audit logs) will ship as _v2.md alongside this file.
17. Cross-references
- Existing Mnemosyne per-turn JWT implementation:
mnemosyne/mnemosyne/mcp_server/auth.py,mnemosyne/mnemosyne/mcp_server/models.py(MCPToken,MCPSigningKey). - Existing Daedalus Mnemosyne integration:
daedalus/backend/daedalus/mnemosyne/(client.py,tokens.py,lifecycle.py,reconciler.py),daedalus/backend/daedalus/api/v1/pallas.py. - Existing Pallas auth-forwarding patch (to be removed):
pallas/pallas/_fastagent_patch.py,pallas/pallas/__init__.py.