P13 — AI Connections (pairing)
Add an "AI Connections" feature to FxFiles that lets a user pair an AI client (via the Model Context Protocol, MCP) with their encrypted AI workspace. Pairing produces a one-time connection bundle the user copies into their AI client.
Output contract
The bundle JSON must match the MCP's CapabilityBundle wire contract (fula-mcp/src/capability.rs, CapabilityBundleJson):
{
"endpoint": "<S3 gateway URL>",
"jwt": "<scoped gateway JWT>",
"workspace_secret_b64": "<base64 of 32 bytes>",
"mcp_secret_b64": "<base64 of 32 bytes>",
"owner_public_b64": "<base64 of 32 bytes>",
"user_id": "<FxFiles userId>",
"storage_api_url": "<credit host>"
}
Scope
- New module
lib/features/ai_connections/ (Riverpod Notifier<State> + NotifierProvider, models/services/providers/screens).
AiConnectionService:
generateMcpKeypair() — fresh X25519 keypair (32 secure-random bytes = secret; public derived via FFI).
deriveWorkspaceSecret() — blake3DeriveKey('fula:ai-workspace-secret:v1', KEK) (one-way; the MCP cannot recover the KEK).
ownerPublicKey() — the user's X25519 sharing public key (AuthService.getPublicKey()).
mintScopedJwt() — POST /api/mcp/tokens (P11 issuer) with the session JWT as bearer; parse token.
buildBundleJson() — pure builder of the exact contract above.
createConnection({label}) — orchestrates the above, persists ONLY the non-secret record, returns the bundle once.
- UI:
AiConnectionsScreen (list saved connections + "Create" → one-time copyable bundle dialog) + a Settings entry.
Security model
FxFiles persists only the non-secret record (MCP public key + label + createdAt). The bundle's secrets (mcp_secret_b64, workspace_secret_b64, scoped jwt) are never persisted — the bundle is shown ONCE for the user to copy. Losing it means re-pairing.
Tests
test/unit/core/services/ai_connection_service_test.dart: bundle JSON has exactly the contract keys; each *_b64 decodes to 32 bytes; the mint POST sends the right body + parses the JWT (injected http.Client); the persisted record carries no secrets.
P13 — AI Connections (pairing)
Add an "AI Connections" feature to FxFiles that lets a user pair an AI client (via the Model Context Protocol, MCP) with their encrypted AI workspace. Pairing produces a one-time connection bundle the user copies into their AI client.
Output contract
The bundle JSON must match the MCP's
CapabilityBundlewire contract (fula-mcp/src/capability.rs,CapabilityBundleJson):{ "endpoint": "<S3 gateway URL>", "jwt": "<scoped gateway JWT>", "workspace_secret_b64": "<base64 of 32 bytes>", "mcp_secret_b64": "<base64 of 32 bytes>", "owner_public_b64": "<base64 of 32 bytes>", "user_id": "<FxFiles userId>", "storage_api_url": "<credit host>" }Scope
lib/features/ai_connections/(RiverpodNotifier<State>+NotifierProvider, models/services/providers/screens).AiConnectionService:generateMcpKeypair()— fresh X25519 keypair (32 secure-random bytes = secret; public derived via FFI).deriveWorkspaceSecret()—blake3DeriveKey('fula:ai-workspace-secret:v1', KEK)(one-way; the MCP cannot recover the KEK).ownerPublicKey()— the user's X25519 sharing public key (AuthService.getPublicKey()).mintScopedJwt()—POST /api/mcp/tokens(P11 issuer) with the session JWT as bearer; parsetoken.buildBundleJson()— pure builder of the exact contract above.createConnection({label})— orchestrates the above, persists ONLY the non-secret record, returns the bundle once.AiConnectionsScreen(list saved connections + "Create" → one-time copyable bundle dialog) + a Settings entry.Security model
FxFiles persists only the non-secret record (MCP public key + label + createdAt). The bundle's secrets (
mcp_secret_b64,workspace_secret_b64, scopedjwt) are never persisted — the bundle is shown ONCE for the user to copy. Losing it means re-pairing.Tests
test/unit/core/services/ai_connection_service_test.dart: bundle JSON has exactly the contract keys; each*_b64decodes to 32 bytes; the mint POST sends the right body + parses the JWT (injectedhttp.Client); the persisted record carries no secrets.