OXYGENHUB
◆ main spectra / crates / spectra-core / src / daemon.rs
13.4 KiB · Rust · last changed spectra agent allow: let one agent post in another project's channel 1 day ago source blame history raw
571572f5 Andy McCall ↶ 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").
87f601cc Andy McCall ↶ 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
];
571572f5 Andy McCall ↶ 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>,
037c45d8 Andy McCall ↶ 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)>>,
571572f5 Andy McCall ↶ 40
}
41
42
impl Daemon {
43
    /// Open the data directory's ledger for writing and start the timers.
44
    pub async fn start(data: &Path) -> Result<Arc<Daemon>> {
45
        let db = data.join("spectra.db");
46
        if !db.exists() {
47
            bail!("no Spectra in {} (run `spectra init`)", data.display());
48
        }
49
        let instance = KeyPair::instance(data, "spectra")?;
50
        let ledger = Ledger::open(&db, Some(instance)).await?;
51
        let principals = Principals::load(&ledger).await?;
52
        let d = Arc::new(Daemon {
53
            data: data.to_path_buf(),
54
            ledger,
55
            principals: RwLock::new(principals),
56
            authn: Authenticator::default(),
57
            presence: Mutex::new(Presence::default()),
037c45d8 Andy McCall ↶ 58
            idle_waits: Mutex::new(Default::default()),
571572f5 Andy McCall ↶ 59
        });
60
        d.clone().watch_principals();
61
        d.clone().tick_presence();
62
        Ok(d)
63
    }
64
65
    /// Reload who is who whenever an identity or agent event is appended.
66
    fn watch_principals(self: Arc<Self>) {
67
        let mut rx = self.ledger.subscribe();
68
        tokio::spawn(async move {
69
            loop {
70
                match rx.recv().await {
71
                    Ok(e)
72
                        if e.kind.starts_with("identity.")
73
                            || e.kind.starts_with("agent.") && e.kind != "agent.presence" && e.kind != "agent.status" =>
74
                    {
75
                        if let Ok(p) = Principals::load(&self.ledger).await {
76
                            *self.principals.write().await = p;
77
                        }
78
                    }
79
                    Ok(_) => {}
80
                    Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
81
                        if let Ok(p) = Principals::load(&self.ledger).await {
82
                            *self.principals.write().await = p;
83
                        }
84
                    }
85
                    Err(_) => return,
86
                }
87
            }
88
        });
89
    }
90
91
    /// Record presence changes and quiet sessions every 15 s, and write a
92
    /// checkpoint when one is due by time.
93
    fn tick_presence(self: Arc<Self>) {
94
        tokio::spawn(async move {
95
            let mut every = tokio::time::interval(std::time::Duration::from_secs(15));
96
            loop {
97
                every.tick().await;
98
                let events = self.presence.lock().await.tick(crate::ids::now_ms());
99
                if !events.is_empty() {
100
                    let _ = self.ledger.append(events).await;
101
                }
102
                let _ = self.ledger.append(vec![]).await;
103
            }
104
        });
105
    }
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`.
110
    pub async fn authenticate(&self, proof: Option<&Proof>, method: &str, body: &[u8], peer: Option<Peer>) -> Result<Caller, Refusal> {
111
        let now = crate::ids::now_ms();
112
        let r = {
113
            let p = self.principals.read().await;
77df591d Andy McCall ↶ 114
            if p.owner().is_none() {
115
                return Err(Refusal::NotAPrincipal("this Spectra has no owner yet: its owner claims it with `spectra connect`".into()));
116
            }
571572f5 Andy McCall ↶ 117
            self.authn.authenticate(&p, proof, method, body, now, peer.map(|x| x.uid))
118
        };
119
        match &r {
120
            Err(Refusal::WrongUid { agent, bound, from }) => {
121
                let e = NewEvent::system(
122
                    "guard.impersonation",
123
                    serde_json::json!({ "agent": agent, "bound_uid": bound, "from_uid": from, "from_pid": peer.map(|x| x.pid), "method": method }),
124
                )
125
                .subject(agent.clone());
126
                let _ = self.ledger.append_one(e).await;
127
                tracing::warn!(agent, bound, from, "a key used from the wrong Unix user");
128
            }
b576d05c Andy McCall ↶ 129
            Err(Refusal::BoundOverNetwork { agent, bound }) => {
130
                let e = NewEvent::system(
131
                    "guard.impersonation",
132
                    serde_json::json!({ "agent": agent, "bound_uid": bound, "from_uid": null, "over": "tcp", "method": method }),
133
                )
134
                .subject(agent.clone());
135
                let _ = self.ledger.append_one(e).await;
136
                tracing::warn!(agent, bound, "a uid-bound key used over the network");
137
            }
571572f5 Andy McCall ↶ 138
            Ok(Caller { principal: Principal::Agent { name, .. }, learn_uid: Some(uid), .. }) => {
139
                let e = NewEvent::system("agent.uid_bound", serde_json::json!({ "name": name, "uid": uid })).subject(name.clone());
140
                let _ = self.ledger.append_one(e).await;
141
            }
142
            _ => {}
143
        }
144
        r
145
    }
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.
149
    pub async fn touch(&self, who: &Principal, new_session: bool) -> Option<String> {
150
        let Principal::Agent { name, .. } = who else { return None };
151
        let (id, started) = self.presence.lock().await.request(name, crate::ids::now_ms(), new_session);
152
        if let Some(e) = started {
153
            let _ = self.ledger.append_one(e).await;
154
        }
155
        Some(id)
156
    }
157
158
    /// Append events the owner's CLI signed over their payload, after
159
    /// checking each as `spectra admin verify` would.
160
    pub async fn accept_owner_events(&self, events: Vec<WireEvent>) -> Result<Vec<u64>> {
161
        let mut principals = self.principals.read().await.clone();
162
        let projects: Vec<String> = events
163
            .iter()
164
            .filter(|e| e.kind == "project.added")
165
            .filter_map(|e| e.payload.get("name").and_then(|n| n.as_str()).map(String::from))
166
            .collect();
167
        let mut out = Vec::new();
168
        for w in events {
169
            let n = w.into_new_event()?;
170
            if n.kind == "channel.created" {
171
                // A project's channel comes with its project, unsigned, as `project add` makes it.
172
                let p = &n.payload;
173
                let ok = p.get("kind").and_then(|k| k.as_str()) == Some("project")
174
                    && p.get("name").and_then(|x| x.as_str()).is_some_and(|x| projects.iter().any(|q| q == x));
175
                if !ok || n.auth.method != Method::System {
176
                    bail!("a channel is only made with its project");
177
                }
178
                out.push(n);
179
                continue;
180
            }
181
            if !OWNER_KINDS.contains(&n.kind.as_str()) {
182
                bail!("{} cannot be handed to the daemon", n.kind);
183
            }
184
            if n.actor != "human" {
185
                bail!("only the owner hands events to the daemon");
186
            }
b044d909 Andy McCall ↶ 187
            if n.kind == "brief.set" {
188
                crate::rules::check(n.payload.get("text").and_then(|t| t.as_str()).unwrap_or(""))?;
189
            }
571572f5 Andy McCall ↶ 190
            // Check it as verify will, against who is who including the
191
            // batch so far.
192
            let probe = probe_event(&n);
193
            principals.check(&probe).map_err(|why| anyhow::anyhow!("refused {}: {why}", n.kind))?;
194
            if n.kind != "identity.rotated" {
98a0fb9f Andy McCall ↶ 195
                let key = crate::identity::KeyId(n.auth.key.context("unsigned")?);
571572f5 Andy McCall ↶ 196
                if !matches!(principals.principal(&key, crate::ids::now_ms()), Ok(Principal::Owner { .. })) {
197
                    bail!("refused {}: not signed by the owner", n.kind);
198
                }
199
            }
200
            out.push(n);
201
        }
202
        let appended = self.ledger.append(out).await?;
203
        let _ = self.ledger.checkpoint().await;
204
        // Who is who has changed: know it before the next request arrives.
205
        *self.principals.write().await = Principals::load(&self.ledger).await?;
206
        Ok(appended.iter().map(|e| e.seq).collect())
77df591d Andy McCall ↶ 207
    }
208
209
    /// The owner `spectra init --owner` named, while none is recorded.
210
    pub fn named_owner(&self) -> Option<String> {
211
        let text = std::fs::read_to_string(self.data.join("spectra.toml")).ok()?;
212
        let t: toml::Table = text.parse().ok()?;
213
        t.get("owner")?.get("key")?.as_str().map(|k| k.to_ascii_lowercase())
214
    }
215
216
    /// The owner claims a Spectra set up with `spectra init --owner`
eca5af92 Andy McCall ↶ 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.
221
    pub async fn claim(&self, events: Vec<WireEvent>) -> Result<Vec<u64>> {
77df591d Andy McCall ↶ 222
        let mut principals = self.principals.read().await.clone();
223
        if principals.owner().is_some() {
224
            bail!("this Spectra already has an owner");
225
        }
226
        let named = self.named_owner().context("this Spectra names no owner (spectra init --owner)")?;
eca5af92 Andy McCall ↶ 227
        let mut out = Vec::new();
228
        for (i, w) in events.into_iter().enumerate() {
229
            let n = w.into_new_event()?;
230
            let want = if i == 0 { "identity.owner" } else { "identity.rotated" };
231
            if n.kind != want || n.actor != "human" {
232
                bail!("a claim is the owner's identity.owner, then their rotations");
233
            }
234
            if i == 0 {
235
                let key = n.payload.get("key").and_then(|k| k.as_str()).unwrap_or("").to_ascii_lowercase();
236
                if key != named || n.auth.key.map(hex::encode) != Some(named.clone()) {
237
                    bail!("this Spectra was set up for another owner");
238
                }
239
            }
240
            principals.check(&probe_event(&n)).map_err(|why| anyhow::anyhow!("refused the claim: {why}"))?;
241
            out.push(n);
242
        }
243
        if out.is_empty() {
244
            bail!("an empty claim");
77df591d Andy McCall ↶ 245
        }
eca5af92 Andy McCall ↶ 246
        let appended = self.ledger.append(out).await?;
77df591d Andy McCall ↶ 247
        let _ = self.ledger.checkpoint().await;
248
        *self.principals.write().await = Principals::load(&self.ledger).await?;
eca5af92 Andy McCall ↶ 249
        Ok(appended.iter().map(|e| e.seq).collect())
571572f5 Andy McCall ↶ 250
    }
251
252
    /// Write a checkpoint covering everything (at shutdown).
253
    pub async fn shutdown(&self) {
254
        let _ = self.ledger.checkpoint().await;
255
    }
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 {
279
    pub fn from_new(n: &NewEvent) -> WireEvent {
280
        WireEvent {
281
            kind: n.kind.clone(),
282
            actor: n.actor.clone(),
283
            subject: n.subject.clone(),
284
            project: n.project.clone(),
285
            payload: n.payload.clone(),
286
            key: n.auth.key.map(hex::encode),
287
            proof: n.auth.proof.as_ref().map(hex::encode),
288
        }
289
    }
290
291
    fn into_new_event(self) -> Result<NewEvent> {
292
        let auth = match (&self.key, &self.proof) {
293
            (Some(k), Some(p)) => Auth {
294
                key: Some(hex::decode(k).ok().and_then(|b| b.try_into().ok()).context("a key is 32 bytes of hex")?),
295
                method: Method::Signature,
296
                assurance: 2,
297
                proof: Some(hex::decode(p).context("a proof is hex")?),
298
            },
299
            _ => Auth::system(),
300
        };
301
        let mut n = NewEvent::system(&self.kind, self.payload).by(self.actor, auth);
302
        n.subject = self.subject;
303
        n.project = self.project;
304
        Ok(n)
305
    }
306
}
307
308
/// The event `n` would be, for checking before it is appended.
309
fn probe_event(n: &NewEvent) -> Event {
310
    let now = crate::ids::now_ms();
311
    Event {
312
        seq: 0,
313
        id: String::new(),
314
        at_ms: now,
315
        hlc: crate::hlc::Hlc { wall_ms: now, logical: 0 },
316
        prev: [0; 32],
317
        hash: [0; 32],
318
        auth: n.auth.clone(),
319
        kind: n.kind.clone(),
320
        actor: n.actor.clone(),
321
        session: n.session.clone(),
322
        project: n.project.clone(),
323
        subject: n.subject.clone(),
324
        payload: Some(crate::event::canonical_json(&n.payload)),
325
        trace: n.trace.clone(),
326
    }
327
}