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//! On a connected setup the daemon is also the **sole publisher** of write
50//! events for [`Database::on_write`](crate::Database::on_write) callbacks:
51//! a connected client's `Instance::put_entry` deliberately *does not* fire
52//! its local callback registry, because the daemon will round-trip a
53//! `Notification::DatabaseWrite` (carried in a `ServerFrame::Notification`
54//! envelope) back to every subscribed connection. A client subscribes to
55//! a tree lazily on the first `Database::on_write` registration via
56//! `DatabaseOp::SubscribeWrites`. Subscriptions live for the connection's
57//! lifetime; disconnecting implicitly unsubscribes everything. Because the
58//! daemon is the sole publisher and fires each tree's subscriptions with a
59//! synchronous channel send held under that tree's write lock, every
60//! subscriber — including the originating client — observes callbacks in
61//! the daemon's canonical order *per tree*, with full `previous_tips` (no
62//! client-side placeholder). See [`Database::on_write`](crate::Database::on_write)
63//! for the full ordering contract.
64//!
65//! ## V1 Limitations
66//!
67//! - **`enable_sync()` on remote Instance**: A silent no-op (returns `Ok(())`)
68//! rather than building a client-side sync module that would race the
69//! daemon's own sync. The daemon either already runs sync or it does not,
70//! and the client cannot change that over the current wire surface. Future:
71//! add an admin-gated `EnableSync` RPC that delegates to the server's
72//! Instance, and similarly for `sync()`, `flush_sync()`, etc.
73
74pub mod client;
75pub mod error;
76pub mod protocol;
77pub mod server;
78
79pub use client::RemoteConnection;
80pub use server::ServiceServer;
81
82use std::path::PathBuf;
83
84/// Default socket path for the Eidetica service.
85///
86/// Resolution order:
87/// 1. `EIDETICA_SOCKET` environment variable, if set.
88/// 2. `$XDG_RUNTIME_DIR/eidetica/service.sock`, if `XDG_RUNTIME_DIR` is set
89/// (the standard Linux convention).
90/// 3. `/tmp/eidetica-$USER/service.sock` as a last-resort fallback.
91///
92/// Used by the daemon CLI to choose where to bind and by
93/// [`default_socket_url`] to construct the equivalent `unix://` URL for
94/// `Instance::connect`.
95pub fn default_socket_path() -> PathBuf {
96 if let Ok(socket) = std::env::var("EIDETICA_SOCKET") {
97 return PathBuf::from(socket);
98 }
99 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
100 PathBuf::from(runtime_dir)
101 .join("eidetica")
102 .join("service.sock")
103 } else {
104 let user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
105 PathBuf::from(format!("/tmp/eidetica-{user}")).join("service.sock")
106 }
107}
108
109/// Default `unix://` URL for `Instance::connect`, derived from
110/// [`default_socket_path`].
111///
112/// Convenience for apps that want to connect to the local daemon's socket
113/// without writing the env / `$XDG_RUNTIME_DIR` resolution themselves:
114///
115/// ```ignore
116/// let instance = Instance::connect(eidetica::service::default_socket_url()).await?;
117/// ```
118pub fn default_socket_url() -> String {
119 format!("unix://{}", default_socket_path().display())
120}