OXYGENHUB

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

162 of 175 lines run, 26 of 71 functions · run #8 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] = &["identity.rotated", "agent.added", "agent.disabled", "project.added"];
22
23
pub struct Daemon {
24
    pub data: PathBuf,
25
    pub ledger: Ledger,
26
    pub principals: RwLock<Principals>,
27
    pub authn: Authenticator,
28
    pub presence: Mutex<Presence>,
29
}
30
31
impl Daemon {
32
    /// Open the data directory's ledger for writing and start the timers.
333×
    pub async fn start(data: &Path) -> Result<Arc<Daemon>> {
343×
        let db = data.join("spectra.db");
353×
        if !db.exists() {
360×
            bail!("no Spectra in {} (run `spectra init`)", data.display());
373×
        }
383×
        let instance = KeyPair::instance(data, "spectra")?;
393×
        let ledger = Ledger::open(&db, Some(instance)).await?;
403×
        let principals = Principals::load(&ledger).await?;
413×
        let d = Arc::new(Daemon {
423×
            data: data.to_path_buf(),
433×
            ledger,
443×
            principals: RwLock::new(principals),
453×
            authn: Authenticator::default(),
463×
            presence: Mutex::new(Presence::default()),
473×
        });
483×
        d.clone().watch_principals();
493×
        d.clone().tick_presence();
503×
        Ok(d)
513×
    }
52
53
    /// Reload who is who whenever an identity or agent event is appended.
543×
    fn watch_principals(self: Arc<Self>) {
553×
        let mut rx = self.ledger.subscribe();
563×
        tokio::spawn(async move {
57
            loop {
5815×
                match rx.recv().await {
596×
                    Ok(e)
6013×
                        if e.kind.starts_with("identity.")
6112×
                            || e.kind.starts_with("agent.") && e.kind != "agent.presence" && e.kind != "agent.status" =>
62
                    {
636×
                        if let Ok(p) = Principals::load(&self.ledger).await {
645×
                            *self.principals.write().await = p;
650×
                        }
66
                    }
677×
                    Ok(_) => {}
68
                    Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
690×
                        if let Ok(p) = Principals::load(&self.ledger).await {
700×
                            *self.principals.write().await = p;
710×
                        }
72
                    }
730×
                    Err(_) => return,
74
                }
75
            }
760×
        });
773×
    }
78
79
    /// Record presence changes and quiet sessions every 15 s, and write a
80
    /// checkpoint when one is due by time.
813×
    fn tick_presence(self: Arc<Self>) {
823×
        tokio::spawn(async move {
833×
            let mut every = tokio::time::interval(std::time::Duration::from_secs(15));
84
            loop {
856×
                every.tick().await;
863×
                let events = self.presence.lock().await.tick(crate::ids::now_ms());
873×
                if !events.is_empty() {
880×
                    let _ = self.ledger.append(events).await;
893×
                }
903×
                let _ = self.ledger.append(vec![]).await;
91
            }
92
        });
933×
    }
94
95
    /// Authenticate a request (spec §8.5); an agent used from the wrong Unix
96
    /// user is recorded as `guard.impersonation`, and a first connection
97
    /// that binds an agent's uid as `agent.uid_bound`.
9813×
    pub async fn authenticate(&self, proof: Option<&Proof>, method: &str, body: &[u8], peer: Option<Peer>) -> Result<Caller, Refusal> {
9913×
        let now = crate::ids::now_ms();
10013×
        let r = {
10113×
            let p = self.principals.read().await;
10213×
            self.authn.authenticate(&p, proof, method, body, now, peer.map(|x| x.uid))
103
        };
1048×
        match &r {
1051×
            Err(Refusal::WrongUid { agent, bound, from }) => {
1061×
                let e = NewEvent::system(
1071×
                    "guard.impersonation",
1081×
                    serde_json::json!({ "agent": agent, "bound_uid": bound, "from_uid": from, "from_pid": peer.map(|x| x.pid), "method": method }),
109
                )
1101×
                .subject(agent.clone());
1111×
                let _ = self.ledger.append_one(e).await;
1121×
                tracing::warn!(agent, bound, from, "a key used from the wrong Unix user");
113
            }
1141×
            Ok(Caller { principal: Principal::Agent { name, .. }, learn_uid: Some(uid), .. }) => {
1151×
                let e = NewEvent::system("agent.uid_bound", serde_json::json!({ "name": name, "uid": uid })).subject(name.clone());
1161×
                let _ = self.ledger.append_one(e).await;
117
            }
11811×
            _ => {}
119
        }
12013×
        r
12113×
    }
122
123
    /// Note a request from an agent: its session, and the event that
124
    /// starts one when this is the first in ten minutes.
1258×
    pub async fn touch(&self, who: &Principal, new_session: bool) -> Option<String> {
1268×
        let Principal::Agent { name, .. } = who else { return None };
1278×
        let (id, started) = self.presence.lock().await.request(name, crate::ids::now_ms(), new_session);
1288×
        if let Some(e) = started {
1293×
            let _ = self.ledger.append_one(e).await;
1305×
        }
1318×
        Some(id)
1328×
    }
133
134
    /// Append events the owner's CLI signed over their payload, after
135
    /// checking each as `spectra admin verify` would.
1362×
    pub async fn accept_owner_events(&self, events: Vec<WireEvent>) -> Result<Vec<u64>> {
1372×
        let mut principals = self.principals.read().await.clone();
1382×
        let projects: Vec<String> = events
1392×
            .iter()
1404×
            .filter(|e| e.kind == "project.added")
1412×
            .filter_map(|e| e.payload.get("name").and_then(|n| n.as_str()).map(String::from))
1422×
            .collect();
1432×
        let mut out = Vec::new();
1444×
        for w in events {
1454×
            let n = w.into_new_event()?;
1464×
            if n.kind == "channel.created" {
147
                // A project's channel comes with its project, unsigned, as `project add` makes it.
1481×
                let p = &n.payload;
1491×
                let ok = p.get("kind").and_then(|k| k.as_str()) == Some("project")
1501×
                    && p.get("name").and_then(|x| x.as_str()).is_some_and(|x| projects.iter().any(|q| q == x));
1511×
                if !ok || n.auth.method != Method::System {
1520×
                    bail!("a channel is only made with its project");
1531×
                }
1541×
                out.push(n);
1551×
                continue;
1563×
            }
1573×
            if !OWNER_KINDS.contains(&n.kind.as_str()) {
1580×
                bail!("{} cannot be handed to the daemon", n.kind);
1593×
            }
1603×
            if n.actor != "human" {
1610×
                bail!("only the owner hands events to the daemon");
1623×
            }
163
            // Check it as verify will, against who is who including the
164
            // batch so far.
1653×
            let probe = probe_event(&n);
1663×
            principals.check(&probe).map_err(|why| anyhow::anyhow!("refused {}: {why}", n.kind))?;
1673×
            if n.kind != "identity.rotated" {
1682×
                let key = oxy_core::hash::ObjectRef(n.auth.key.context("unsigned")?);
1692×
                if !matches!(principals.principal(&key, crate::ids::now_ms()), Ok(Principal::Owner { .. })) {
1700×
                    bail!("refused {}: not signed by the owner", n.kind);
1712×
                }
1721×
            }
1733×
            out.push(n);
174
        }
1752×
        let appended = self.ledger.append(out).await?;
1762×
        let _ = self.ledger.checkpoint().await;
177
        // Who is who has changed: know it before the next request arrives.
1782×
        *self.principals.write().await = Principals::load(&self.ledger).await?;
1794×
        Ok(appended.iter().map(|e| e.seq).collect())
1802×
    }
181
182
    /// Write a checkpoint covering everything (at shutdown).
1833×
    pub async fn shutdown(&self) {
1843×
        let _ = self.ledger.checkpoint().await;
1853×
    }
186
}
187
188
/// The Unix peer of a socket connection.
189
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190
pub struct Peer {
191
    pub uid: u32,
192
    pub pid: Option<i32>,
193
}
194
195
/// An event as the CLI hands it to the daemon: signed already.
196
#[derive(Clone, Debug, Serialize, Deserialize)]
197
pub struct WireEvent {
198
    pub kind: String,
199
    pub actor: String,
200
    pub subject: Option<String>,
201
    pub project: Option<String>,
202
    pub payload: serde_json::Value,
203
    /// Hex of the auth key and the encoded proof; absent for a project's channel.
204
    pub key: Option<String>,
205
    pub proof: Option<String>,
206
}
207
208
impl WireEvent {
2094×
    pub fn from_new(n: &NewEvent) -> WireEvent {
2104×
        WireEvent {
2114×
            kind: n.kind.clone(),
2124×
            actor: n.actor.clone(),
2134×
            subject: n.subject.clone(),
2144×
            project: n.project.clone(),
2154×
            payload: n.payload.clone(),
2164×
            key: n.auth.key.map(hex::encode),
2174×
            proof: n.auth.proof.as_ref().map(hex::encode),
2184×
        }
2194×
    }
220
2214×
    fn into_new_event(self) -> Result<NewEvent> {
2224×
        let auth = match (&self.key, &self.proof) {
2233×
            (Some(k), Some(p)) => Auth {
2243×
                key: Some(hex::decode(k).ok().and_then(|b| b.try_into().ok()).context("a key is 32 bytes of hex")?),
2253×
                method: Method::Signature,
226
                assurance: 2,
2273×
                proof: Some(hex::decode(p).context("a proof is hex")?),
228
            },
2291×
            _ => Auth::system(),
230
        };
2314×
        let mut n = NewEvent::system(&self.kind, self.payload).by(self.actor, auth);
2324×
        n.subject = self.subject;
2334×
        n.project = self.project;
2344×
        Ok(n)
2354×
    }
236
}
237
238
/// The event `n` would be, for checking before it is appended.
2393×
fn probe_event(n: &NewEvent) -> Event {
2403×
    let now = crate::ids::now_ms();
2413×
    Event {
2423×
        seq: 0,
2433×
        id: String::new(),
2443×
        at_ms: now,
2453×
        hlc: crate::hlc::Hlc { wall_ms: now, logical: 0 },
2463×
        prev: [0; 32],
2473×
        hash: [0; 32],
2483×
        auth: n.auth.clone(),
2493×
        kind: n.kind.clone(),
2503×
        actor: n.actor.clone(),
2513×
        session: n.session.clone(),
2523×
        project: n.project.clone(),
2533×
        subject: n.subject.clone(),
2543×
        payload: Some(crate::event::canonical_json(&n.payload)),
2553×
        trace: n.trace.clone(),
2563×
    }
2573×
}