Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/service/
mod.rs

1//! Local service (daemon) mode for Eidetica.
2//!
3//! This module enables running Eidetica as a local daemon that serves an Instance
4//! to multiple client processes over a Unix domain socket. The primary motivation is
5//! shared storage: multiple CLI tools and applications can operate on the same
6//! Eidetica data without each process opening its own backend.
7//!
8//! ## Architecture
9//!
10//! The RPC boundary sits at the storage operation level. A `RemoteConnection` forwards
11//! all operations over a Unix socket to the daemon, backing the `RemoteBackend` seam impl.
12//! `Instance::connect(path)` loads `InstanceMetadata` from the remote backend,
13//! then constructs an Instance with no local secrets.
14//!
15//! ## Security Model
16//!
17//! Client-side signing. The daemon stores and serves encrypted key material and
18//! signed entries but never holds plaintext user signing keys or passwords.
19//!
20//! - **User keys stay client-side**: clients fetch encrypted `UserCredentials` from
21//!   the daemon, derive the key-encryption-key locally (Argon2id), decrypt the user's
22//!   signing key in-process, and sign entries before sending them to the daemon for
23//!   storage. The signing key never crosses the socket.
24//! - **Authentication via challenge-response**: when the daemon needs to prove a
25//!   connecting client controls a user account, the daemon issues a fresh random
26//!   challenge per session and the client signs it with the user's root key. The
27//!   daemon verifies against the user's public key from its auth tables. No password
28//!   is sent over the wire; successful decryption of the user's signing key on the
29//!   client *is* password verification.
30//! - **Encrypted stores remain opaque to the daemon**: per-database encrypted CRDTs
31//!   (e.g. `PasswordStore`) merge as `Vec<EncryptedBlob>` โ€” the daemon participates
32//!   in storage and sync without ever holding a content encryption key. Clients
33//!   decrypt and merge in-process and may write the result back as an encrypted
34//!   cache entry.
35//! - **Filesystem permissions**: the socket directory is owner-only (mode 0700) and
36//!   the socket itself is mode 0600 as an additional access-control layer.
37//!
38//! See the brain note "Service Architecture" ยง Security Model for the design rationale,
39//! including why daemon-side signing (the earlier draft) was rejected and the
40//! deferred work that grew out of that decision (hardware-backed `PrivateKey::Remote`,
41//! async `sign()`, OS-keyring caching of derived encryption keys).
42//!
43//! ## Write Coordination
44//!
45//! Client writes travel as `DatabaseOp::SubmitSignedEntry` โ€” the daemon stores
46//! the entry `Unverified`, then runs its own verification pass before the
47//! entry is exposed on any default read.
48//!
49//! ## V1 Limitations
50//!
51//! - **No server-push notifications**: Clients see latest state on each request
52//!   but are not notified when the daemon receives entries from sync peers, or
53//!   when another client writes through the same daemon. As a consequence,
54//!   [`Database::on_write`](crate::Database::on_write) on a connected
55//!   [`Instance`](crate::Instance) observes only writes this client's own
56//!   commit path produced โ€” see that method's doc for the full caveat list.
57//!   Future: bidirectional framing with a `Notification` variant alongside
58//!   `Response`, plus a client-side reader task that routes notifications to
59//!   the local callback registry.
60//!
61//! - **`enable_sync()` on remote Instance**: A silent no-op (returns `Ok(())`)
62//!   rather than building a client-side sync module that would race the
63//!   daemon's own sync. The daemon either already runs sync or it does not,
64//!   and the client cannot change that over the current wire surface. Future:
65//!   add an admin-gated `EnableSync` RPC that delegates to the server's
66//!   Instance, and similarly for `sync()`, `flush_sync()`, etc.
67
68pub mod client;
69pub mod error;
70pub mod protocol;
71pub mod server;
72
73pub use client::RemoteConnection;
74pub use server::ServiceServer;
75
76use std::path::PathBuf;
77
78/// Default socket path for the Eidetica service.
79///
80/// Resolution order:
81/// 1. `EIDETICA_SOCKET` environment variable, if set.
82/// 2. `$XDG_RUNTIME_DIR/eidetica/service.sock`, if `XDG_RUNTIME_DIR` is set
83///    (the standard Linux convention).
84/// 3. `/tmp/eidetica-$USER/service.sock` as a last-resort fallback.
85///
86/// Used by the daemon CLI to choose where to bind and by
87/// [`default_socket_url`] to construct the equivalent `unix://` URL for
88/// `Instance::connect`.
89pub fn default_socket_path() -> PathBuf {
90    if let Ok(socket) = std::env::var("EIDETICA_SOCKET") {
91        return PathBuf::from(socket);
92    }
93    if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
94        PathBuf::from(runtime_dir)
95            .join("eidetica")
96            .join("service.sock")
97    } else {
98        let user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
99        PathBuf::from(format!("/tmp/eidetica-{user}")).join("service.sock")
100    }
101}
102
103/// Default `unix://` URL for `Instance::connect`, derived from
104/// [`default_socket_path`].
105///
106/// Convenience for apps that want to connect to the local daemon's socket
107/// without writing the env / `$XDG_RUNTIME_DIR` resolution themselves:
108///
109/// ```ignore
110/// let instance = Instance::connect(eidetica::service::default_socket_url()).await?;
111/// ```
112pub fn default_socket_url() -> String {
113    format!("unix://{}", default_socket_path().display())
114}