OXYGENHUB

Coverage / crates/spectra-core/src/daemon.rs 93.0%

213 of 229 lines run, 33 of 92 functions · run #35 on main · run missed

1
//! The daemon's state (spec §7.1): the ledger it alone writes, the
2
//! principals kept current from the bus, the authenticator, and presence
3
//! with its timers. HTTP lives elsewhere (`spectra-mcp`, `radar`); this is
4
//! what they share.
5
6
use crate::authn::{Authenticator, Caller, Refusal};
7
use crate::event::{Auth, Event, Method, NewEvent};
8
use crate::keys::KeyPair;
9
use crate::ledger::{KeyResolver, Ledger};
10
use crate::presence::Presence;
11
use crate::principals::{Principal, Principals};
12
use crate::proof::Proof;
13
use anyhow::{bail, Context, Result};
14
use serde::{Deserialize, Serialize};
15
use std::path::{Path, PathBuf};
16
use std::sync::Arc;
17
use tokio::sync::{Mutex, RwLock};
18
19
/// Kinds the owner's CLI may hand the daemon, already signed over their
20
/// payload (spec §8.5, "One writer").
21
pub const OWNER_KINDS: &[&str] = &[
22
    "identity.rotated",
23
    "agent.added",
24
    "agent.disabled",
25
    "agent.scoped",
26
    "project.added",
27
    "project.linked",
28
    "project.unlinked",
29
    "brief.set",
30
];
31
32
pub struct Daemon {
33
    pub data: PathBuf,
34
    pub ledger: Ledger,
35
    pub principals: RwLock<Principals>,
36
    pub authn: Authenticator,
37
    pub presence: Mutex<Presence>,
38
    /// Per agent: empty `wait`s in a row, and until when `wait` is refused.
39
    pub idle_waits: Mutex<std::collections::HashMap<String, (u32, u64)>>,
40
}
41
42
impl Daemon {
43
    /// Open the data directory's ledger for writing and start the timers.
4422×
    pub async fn start(data: &Path) -> Result<Arc<Daemon>> {
4522×
        let db = data.join("spectra.db");
4622×
        if !db.exists() {
470×
            bail!("no Spectra in {} (run `spectra init`)", data.display());
4822×
        }
4922×
        let instance = KeyPair::instance(data, "spectra")?;
5022×
        let ledger = Ledger::open(&db, Some(instance)).await?;
5122×
        let principals = Principals::load(&ledger).await?;
5222×
        let d = Arc::new(Daemon {
5322×
            data: data.to_path_buf(),
5422×
            ledger,
5522×
            principals: RwLock::new(principals),
5622×
            authn: Authenticator::default(),
5722×
            presence: Mutex::new(Presence::default()),
5822×
            idle_waits: Mutex::new(Default::default()),
5922×
        });
6022×
        d.clone().watch_principals();
6122×
        d.clone().tick_presence();
6222×
        Ok(d)
6322×
    }
64
65
    /// Reload who is who whenever an identity or agent event is appended.
6622×
    fn watch_principals(self: Arc<Self>) {
6722×
        let mut rx = self.ledger.subscribe();
6822×
        tokio::spawn(async move {
69
            loop {
70177×
                match rx.recv().await {
7137×
                    Ok(e)
72157×
                        if e.kind.starts_with("identity.")
73148×
                            || e.kind.starts_with("agent.") && e.kind != "agent.presence" && e.kind != "agent.status" =>
74
                    {
7537×
                        if let Ok(p) = Principals::load(&self.ledger).await {
7635×
                            *self.principals.write().await = p;
770×
                        }
78
                    }
79120×
                    Ok(_) => {}
80
                    Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
810×
                        if let Ok(p) = Principals::load(&self.ledger).await {
820×
                            *self.principals.write().await = p;
830×
                        }
84
                    }
850×
                    Err(_) => return,
86
                }
87
            }
880×
        });
8922×
    }
90
91
    /// Record presence changes and quiet sessions every 15 s, and write a
92
    /// checkpoint when one is due by time.
9322×
    fn tick_presence(self: Arc<Self>) {
9422×
        tokio::spawn(async move {
9522×
            let mut every = tokio::time::interval(std::time::Duration::from_secs(15));
96
            loop {
9744×
                every.tick().await;
9822×
                let events = self.presence.lock().await.tick(crate::ids::now_ms());
9922×
                if !events.is_empty() {
1000×
                    let _ = self.ledger.append(events).await;
10122×
                }
10222×
                let _ = self.ledger.append(vec![]).await;
103
            }
104
        });
10522×
    }
106
107
    /// Authenticate a request (spec §8.5); an agent used from the wrong Unix
108
    /// user is recorded as `guard.impersonation`, and a first connection
109
    /// that binds an agent's uid as `agent.uid_bound`.
110148×
    pub async fn authenticate(&self, proof: Option<&Proof>, method: &str, body: &[u8], peer: Option<Peer>) -> Result<Caller, Refusal> {
111148×
        let now = crate::ids::now_ms();
112147×
        let r = {
113148×
            let p = self.principals.read().await;
114148×
            if p.owner().is_none() {
1151×
                return Err(Refusal::NotAPrincipal("this Spectra has no owner yet: its owner claims it with `spectra connect`".into()));
116147×
            }
117147×
            self.authn.authenticate(&p, proof, method, body, now, peer.map(|x| x.uid))
118
        };
11984×
        match &r {
1201×
            Err(Refusal::WrongUid { agent, bound, from }) => {
1211×
                let e = NewEvent::system(
1221×
                    "guard.impersonation",
1231×
                    serde_json::json!({ "agent": agent, "bound_uid": bound, "from_uid": from, "from_pid": peer.map(|x| x.pid), "method": method }),
124
                )
1251×
                .subject(agent.clone());
1261×
                let _ = self.ledger.append_one(e).await;
1271×
                tracing::warn!(agent, bound, from, "a key used from the wrong Unix user");
128
            }
1291×
            Err(Refusal::BoundOverNetwork { agent, bound }) => {
1301×
                let e = NewEvent::system(
1311×
                    "guard.impersonation",
1321×
                    serde_json::json!({ "agent": agent, "bound_uid": bound, "from_uid": null, "over": "tcp", "method": method }),
133
                )
1341×
                .subject(agent.clone());
1351×
                let _ = self.ledger.append_one(e).await;
1361×
                tracing::warn!(agent, bound, "a uid-bound key used over the network");
137
            }
1381×
            Ok(Caller { principal: Principal::Agent { name, .. }, learn_uid: Some(uid), .. }) => {
1391×
                let e = NewEvent::system("agent.uid_bound", serde_json::json!({ "name": name, "uid": uid })).subject(name.clone());
1401×
                let _ = self.ledger.append_one(e).await;
141
            }
142144×
            _ => {}
143
        }
144147×
        r
145148×
    }
146
147
    /// Note a request from an agent: its session, and the event that
148
    /// starts one when this is the first in ten minutes.
14962×
    pub async fn touch(&self, who: &Principal, new_session: bool) -> Option<String> {
15062×
        let Principal::Agent { name, .. } = who else { return None };
15162×
        let (id, started) = self.presence.lock().await.request(name, crate::ids::now_ms(), new_session);
15262×
        if let Some(e) = started {
15321×
            let _ = self.ledger.append_one(e).await;
15441×
        }
15562×
        Some(id)
15662×
    }
157
158
    /// Append events the owner's CLI signed over their payload, after
159
    /// checking each as `spectra admin verify` would.
16012×
    pub async fn accept_owner_events(&self, events: Vec<WireEvent>) -> Result<Vec<u64>> {
16112×
        let mut principals = self.principals.read().await.clone();
16212×
        let projects: Vec<String> = events
16312×
            .iter()
16418×
            .filter(|e| e.kind == "project.added")
16512×
            .filter_map(|e| e.payload.get("name").and_then(|n| n.as_str()).map(String::from))
16612×
            .collect();
16712×
        let mut out = Vec::new();
16818×
        for w in events {
16918×
            let n = w.into_new_event()?;
17018×
            if n.kind == "channel.created" {
171
                // A project's channel comes with its project, unsigned, as `project add` makes it.
1722×
                let p = &n.payload;
1732×
                let ok = p.get("kind").and_then(|k| k.as_str()) == Some("project")
1742×
                    && p.get("name").and_then(|x| x.as_str()).is_some_and(|x| projects.iter().any(|q| q == x));
1752×
                if !ok || n.auth.method != Method::System {
1760×
                    bail!("a channel is only made with its project");
1772×
                }
1782×
                out.push(n);
1792×
                continue;
18016×
            }
18116×
            if !OWNER_KINDS.contains(&n.kind.as_str()) {
1820×
                bail!("{} cannot be handed to the daemon", n.kind);
18316×
            }
18416×
            if n.actor != "human" {
1850×
                bail!("only the owner hands events to the daemon");
18616×
            }
18716×
            if n.kind == "brief.set" {
1884×
                crate::rules::check(n.payload.get("text").and_then(|t| t.as_str()).unwrap_or(""))?;
18912×
            }
190
            // Check it as verify will, against who is who including the
191
            // batch so far.
19216×
            let probe = probe_event(&n);
19316×
            principals.check(&probe).map_err(|why| anyhow::anyhow!("refused {}: {why}", n.kind))?;
19416×
            if n.kind != "identity.rotated" {
19512×
                let key = crate::identity::KeyId(n.auth.key.context("unsigned")?);
19612×
                if !matches!(principals.principal(&key, crate::ids::now_ms()), Ok(Principal::Owner { .. })) {
1970×
                    bail!("refused {}: not signed by the owner", n.kind);
19812×
                }
1994×
            }
20016×
            out.push(n);
201
        }
20212×
        let appended = self.ledger.append(out).await?;
20312×
        let _ = self.ledger.checkpoint().await;
204
        // Who is who has changed: know it before the next request arrives.
20512×
        *self.principals.write().await = Principals::load(&self.ledger).await?;
20618×
        Ok(appended.iter().map(|e| e.seq).collect())
20712×
    }
208
209
    /// The owner `spectra init --owner` named, while none is recorded.
21012×
    pub fn named_owner(&self) -> Option<String> {
21112×
        let text = std::fs::read_to_string(self.data.join("spectra.toml")).ok()?;
21212×
        let t: toml::Table = text.parse().ok()?;
21312×
        t.get("owner")?.get("key")?.as_str().map(|k| k.to_ascii_lowercase())
21412×
    }
215
216
    /// The owner claims a Spectra set up with `spectra init --owner`
217
    /// (spec §8.6): the named identity, signed by itself over its payload,
218
    /// then any successors up to the owner's current identity, each a
219
    /// rotation as `identity refresh` makes. Accepted only while no owner
220
    /// is recorded, and only from the key init named.
2216×
    pub async fn claim(&self, events: Vec<WireEvent>) -> Result<Vec<u64>> {
2226×
        let mut principals = self.principals.read().await.clone();
2236×
        if principals.owner().is_some() {
2241×
            bail!("this Spectra already has an owner");
2255×
        }
2265×
        let named = self.named_owner().context("this Spectra names no owner (spectra init --owner)")?;
2275×
        let mut out = Vec::new();
2287×
        for (i, w) in events.into_iter().enumerate() {
2297×
            let n = w.into_new_event()?;
2307×
            let want = if i == 0 { "identity.owner" } else { "identity.rotated" };
2317×
            if n.kind != want || n.actor != "human" {
2320×
                bail!("a claim is the owner's identity.owner, then their rotations");
2337×
            }
2347×
            if i == 0 {
2355×
                let key = n.payload.get("key").and_then(|k| k.as_str()).unwrap_or("").to_ascii_lowercase();
2365×
                if key != named || n.auth.key.map(hex::encode) != Some(named.clone()) {
2372×
                    bail!("this Spectra was set up for another owner");
2383×
                }
2392×
            }
2405×
            principals.check(&probe_event(&n)).map_err(|why| anyhow::anyhow!("refused the claim: {why}"))?;
2415×
            out.push(n);
242
        }
2433×
        if out.is_empty() {
2440×
            bail!("an empty claim");
2453×
        }
2463×
        let appended = self.ledger.append(out).await?;
2473×
        let _ = self.ledger.checkpoint().await;
2483×
        *self.principals.write().await = Principals::load(&self.ledger).await?;
2495×
        Ok(appended.iter().map(|e| e.seq).collect())
2506×
    }
251
252
    /// Write a checkpoint covering everything (at shutdown).
25322×
    pub async fn shutdown(&self) {
25422×
        let _ = self.ledger.checkpoint().await;
25522×
    }
256
}
257
258
/// The Unix peer of a socket connection.
259
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
260
pub struct Peer {
261
    pub uid: u32,
262
    pub pid: Option<i32>,
263
}
264
265
/// An event as the CLI hands it to the daemon: signed already.
266
#[derive(Clone, Debug, Serialize, Deserialize)]
267
pub struct WireEvent {
268
    pub kind: String,
269
    pub actor: String,
270
    pub subject: Option<String>,
271
    pub project: Option<String>,
272
    pub payload: serde_json::Value,
273
    /// Hex of the auth key and the encoded proof; absent for a project's channel.
274
    pub key: Option<String>,
275
    pub proof: Option<String>,
276
}
277
278
impl WireEvent {
27927×
    pub fn from_new(n: &NewEvent) -> WireEvent {
28027×
        WireEvent {
28127×
            kind: n.kind.clone(),
28227×
            actor: n.actor.clone(),
28327×
            subject: n.subject.clone(),
28427×
            project: n.project.clone(),
28527×
            payload: n.payload.clone(),
28627×
            key: n.auth.key.map(hex::encode),
28727×
            proof: n.auth.proof.as_ref().map(hex::encode),
28827×
        }
28927×
    }
290
29125×
    fn into_new_event(self) -> Result<NewEvent> {
29225×
        let auth = match (&self.key, &self.proof) {
29323×
            (Some(k), Some(p)) => Auth {
29423×
                key: Some(hex::decode(k).ok().and_then(|b| b.try_into().ok()).context("a key is 32 bytes of hex")?),
29523×
                method: Method::Signature,
296
                assurance: 2,
29723×
                proof: Some(hex::decode(p).context("a proof is hex")?),
298
            },
2992×
            _ => Auth::system(),
300
        };
30125×
        let mut n = NewEvent::system(&self.kind, self.payload).by(self.actor, auth);
30225×
        n.subject = self.subject;
30325×
        n.project = self.project;
30425×
        Ok(n)
30525×
    }
306
}
307
308
/// The event `n` would be, for checking before it is appended.
30921×
fn probe_event(n: &NewEvent) -> Event {
31021×
    let now = crate::ids::now_ms();
31121×
    Event {
31221×
        seq: 0,
31321×
        id: String::new(),
31421×
        at_ms: now,
31521×
        hlc: crate::hlc::Hlc { wall_ms: now, logical: 0 },
31621×
        prev: [0; 32],
31721×
        hash: [0; 32],
31821×
        auth: n.auth.clone(),
31921×
        kind: n.kind.clone(),
32021×
        actor: n.actor.clone(),
32121×
        session: n.session.clone(),
32221×
        project: n.project.clone(),
32321×
        subject: n.subject.clone(),
32421×
        payload: Some(crate::event::canonical_json(&n.payload)),
32521×
        trace: n.trace.clone(),
32621×
    }
32721×
}