OXYGENHUB

Coverage / crates/spectra-core/src/principals.rs 90.5%

266 of 294 lines run, 44 of 106 functions · run #7 on main · run missed

1
//! Who is who (spec §8): the owner, the instance, and agents, each an Oxygen
2
//! identity; an agent is valid while the owner's current identity delegates
3
//! to its key (oxygen spec §14.2). Everything here is derived from the
4
//! ledger, and every claim in it was proven when it was recorded:
5
//!
6
//! - `identity.owner` and `identity.rotated` are signed by the identity they
7
//!   record, so only the holder of its secret key can name it the owner.
8
//!   A successor must keep the key pair and chain back, by `previous`, to
9
//!   the owner already known; a fresh key pair needs `oxy identity rotate`'s
10
//!   attestation, which 0.1 does not accept yet.
11
//! - `identity.instance` is signed by the instance key.
12
//! - `agent.added` and `agent.disabled` are signed by the owner, and an
13
//!   agent is added only while the owner's identity delegates to it.
14
//!
15
//! [`Principals`] replays these in ledger order, so `spectra admin verify`
16
//! checks every signed event against who was who at that moment.
17
18
use crate::event::{Event, Method};
19
use crate::ledger::KeyResolver;
20
use crate::proof::Proof;
21
use oxy_core::hash::KeyId;
22
use oxy_core::identity::{Identity, Scope};
23
use serde_json::Value as Json;
24
use sqlx::SqliteConnection;
25
use std::collections::HashMap;
26
27
/// The scopes a new agent gets (spec §8.4).
28
pub const DEFAULT_SCOPES: &[&str] = &["read", "say", "tasks", "claims", "decisions", "status", "search", "schedule:self"];
29
30
/// The Oxygen delegation scope an agent's key needs from its owner.
31
pub const DELEGATION_SCOPE: Scope = Scope::CHANGES;
32
33
/// Who caused something.
34
#[derive(Clone, Debug, PartialEq, Eq)]
35
pub enum Principal {
36
    Owner { key: KeyId, name: String },
37
    Agent { name: String, key: KeyId, owner_name: String, uid: Option<u32>, learn_uid: bool, project: String, scopes: Vec<String> },
38
    Instance { key: KeyId },
39
}
40
41
impl Principal {
42
    /// The `actor` events carry: `human`, `agent:NAME` or `system`.
4382×
    pub fn actor(&self) -> String {
4482×
        match self {
4516×
            Principal::Owner { .. } => "human".into(),
4666×
            Principal::Agent { name, .. } => format!("agent:{name}"),
470×
            Principal::Instance { .. } => "system".into(),
48
        }
4982×
    }
50
    /// How it is shown to people: `claude-oxygen · Andy McCall's agent`.
514×
    pub fn display(&self) -> String {
524×
        match self {
530×
            Principal::Owner { name, .. } => name.clone(),
544×
            Principal::Agent { name, owner_name, .. } => format!("{name} · {owner_name}'s agent"),
550×
            Principal::Instance { .. } => "Spectra".into(),
56
        }
574×
    }
58
}
59
60
#[derive(Clone, Debug)]
61
struct AgentRec {
62
    name: String,
63
    identity: Identity,
64
    owner_name: String,
65
    uid: Option<u32>,
66
    learn_uid: bool,
67
    project: String,
68
    scopes: Vec<String>,
69
    disabled: bool,
70
}
71
72
/// The principals as of some point in the ledger.
73
#[derive(Clone, Debug, Default)]
74
pub struct Principals {
75
    /// The owner's identities, oldest first; the last is current.
76
    owner: Vec<Identity>,
77
    instance: Option<Identity>,
78
    agents: HashMap<KeyId, AgentRec>,
79
    by_name: HashMap<String, KeyId>,
80
}
81
82
impl Principals {
83
    /// Replay every identity and agent event of the ledger.
8429×
    pub async fn load(ledger: &crate::ledger::Ledger) -> anyhow::Result<Principals> {
8529×
        let mut p = Principals::default();
8629×
        let mut from = 1;
87
        loop {
8858×
            let rows = ledger
8958×
                .events_of_kinds(from, &["identity.owner", "identity.rotated", "identity.instance", "agent.added", "agent.disabled"], 1000)
9058×
                .await?;
9158×
            if rows.is_empty() {
9229×
                break;
9329×
            }
94116×
            for e in &rows {
95116×
                // Recorded facts were checked when written; `verify` checks them again.
96116×
                let _ = p.observe(e);
97116×
            }
9829×
            from = rows.last().map(|e| e.seq + 1).unwrap_or(from);
99
        }
10029×
        Ok(p)
10129×
    }
102
103
    /// The owner's current identity.
104148×
    pub fn owner(&self) -> Option<&Identity> {
105148×
        self.owner.last()
106148×
    }
107
1080×
    pub fn instance(&self) -> Option<&Identity> {
1090×
        self.instance.as_ref()
1100×
    }
111
112
    /// The agent named `name`, whatever its state.
1134×
    pub fn agent_key(&self, name: &str) -> Option<KeyId> {
1144×
        self.by_name.get(name).copied()
1154×
    }
116
117
    /// Who `key` is at `at_ms`, if anyone may act with it then: the owner
118
    /// (any of its identities' KeyIds), the instance, or an agent that is
119
    /// not disabled and that the owner's current identity delegates to.
12064×
    pub fn principal(&self, key: &KeyId, at_ms: u64) -> Result<Principal, String> {
121134×
        if let Some(owner) = self.owner.iter().rev().find(|i| &i.hash() == key) {
12216×
            return Ok(Principal::Owner { key: *key, name: owner.name.clone() });
12348×
        }
12448×
        if self.instance.as_ref().is_some_and(|i| &i.hash() == key) {
1250×
            return Ok(Principal::Instance { key: *key });
12648×
        }
12748×
        let a = self.agents.get(key).ok_or("an unknown key")?;
12846×
        if a.disabled {
1295×
            return Err(format!("agent {} is disabled", a.name));
13041×
        }
13141×
        let owner = self.owner().ok_or("no owner is recorded")?;
13241×
        if !owner.delegates_to(key, DELEGATION_SCOPE, at_ms) {
1331×
            return Err(match owner.delegate(key) {
1341×
                Some(d) if d.expires.is_some_and(|x| at_ms > x) => format!("agent {}'s delegation from {} has expired", a.name, owner.name),
1350×
                Some(_) => format!("{}'s delegation to agent {} does not include `changes`", owner.name, a.name),
1360×
                None => format!("{}'s current identity does not delegate to agent {}", owner.name, a.name),
137
            });
13840×
        }
13940×
        Ok(Principal::Agent {
14040×
            name: a.name.clone(),
14140×
            key: *key,
14240×
            owner_name: a.owner_name.clone(),
14340×
            uid: a.uid,
14440×
            learn_uid: a.learn_uid,
14540×
            project: a.project.clone(),
14640×
            scopes: a.scopes.clone(),
14740×
        })
14864×
    }
149
150
    /// The identity behind `key`, for checking its signatures.
15139×
    pub fn identity(&self, key: &KeyId) -> Option<&Identity> {
15239×
        self.owner
15339×
            .iter()
15479×
            .find(|i| &i.hash() == key)
15539×
            .or_else(|| self.instance.as_ref().filter(|i| &i.hash() == key))
15639×
            .or_else(|| self.agents.get(key).map(|a| &a.identity))
15739×
    }
158
159
    /// Take one event into account, checking what it claims first.
160191×
    pub fn observe(&mut self, e: &Event) -> Result<(), String> {
161191×
        let p = e.json();
162191×
        match e.kind.as_str() {
163191×
            "identity.owner" => {
16447×
                if !self.owner.is_empty() {
1651×
                    return Err("an owner is already recorded".into());
16646×
                }
16746×
                let id = identity_of(&p)?;
16846×
                self_signed(e, &id)?;
16946×
                self.owner.push(id);
170
            }
171144×
            "identity.rotated" => {
17246×
                let new = identity_of(&p)?;
17346×
                let cur = self.owner().ok_or("no owner to succeed")?.clone();
17446×
                accept_successor(&cur, &new, &p)?;
17546×
                self_signed(e, &new)?;
17645×
                self.owner.push(new);
177
            }
17898×
            "identity.instance" => {
17948×
                let id = identity_of(&p)?;
18048×
                self_signed(e, &id)?;
18148×
                self.instance = Some(id);
182
            }
18350×
            "agent.added" => {
18446×
                let id = identity_of(&p)?;
18546×
                let key = id.hash();
18646×
                let owner = self.owner_signed(e)?;
18746×
                if !owner.delegates_to(&key, DELEGATION_SCOPE, e.hlc.wall_ms) {
1881×
                    return Err("added without the owner's delegation to its key".into());
18945×
                }
19045×
                let name = text(&p, "name")?;
19145×
                if self.by_name.get(&name).is_some_and(|k| *k != key) {
1920×
                    return Err(format!("agent {name} already exists with another key"));
19345×
                }
19445×
                self.by_name.insert(name.clone(), key);
19545×
                self.agents.insert(
19645×
                    key,
197
                    AgentRec {
19845×
                        name,
19945×
                        identity: id,
20045×
                        owner_name: owner.name.clone(),
20145×
                        uid: p.get("uid").and_then(Json::as_u64).map(|u| u as u32),
20245×
                        learn_uid: p.get("learn_uid").and_then(Json::as_bool).unwrap_or(false),
20345×
                        project: text(&p, "project")?,
20445×
                        scopes: p
20545×
                            .get("scopes")
20645×
                            .and_then(Json::as_array)
207264×
                            .map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect())
20845×
                            .unwrap_or_default(),
209
                        disabled: false,
210
                    },
211
                );
212
            }
2134×
            "agent.disabled" => {
2144×
                self.owner_signed(e)?;
2154×
                let name = text(&p, "name")?;
2164×
                let key = *self.by_name.get(&name).ok_or("an unknown agent")?;
2174×
                if let Some(a) = self.agents.get_mut(&key) {
2184×
                    a.disabled = true;
2194×
                }
220
            }
2210×
            _ => {}
222
        }
223188×
        Ok(())
224191×
    }
225
226
    /// Check that `e` is signed by the owner's current identity; returns it.
22750×
    fn owner_signed(&self, e: &Event) -> Result<Identity, String> {
22850×
        let owner = self.owner().ok_or("no owner is recorded")?.clone();
22950×
        if e.auth.key != Some(owner.hash().0) {
2300×
            return Err("not signed by the owner's current identity".into());
23150×
        }
23250×
        check_proof(e, &owner)?;
23350×
        Ok(owner)
23450×
    }
235
236
    /// Check a signed event's proof against the principal its key was at
237
    /// the event's time.
23837×
    pub fn check_signed(&self, e: &Event) -> Result<(), String> {
23937×
        let key = oxy_core::hash::ObjectRef(e.auth.key.ok_or("signed, but names no key")?);
24037×
        let who = self.principal(&key, e.hlc.wall_ms)?;
241
        // The actor an event names must be the principal that signed it:
242
        // an agent's key never makes an event in another's name.
24335×
        if who.actor() != e.actor {
2442×
            return Err(format!("names {} as its actor but is signed by {}", e.actor, who.display()));
24533×
        }
24633×
        let identity = self.identity(&key).ok_or("an unknown key")?;
24733×
        check_proof(e, identity)
24837×
    }
249
}
250
251
impl KeyResolver for Principals {
252238×
    fn check(&mut self, e: &Event) -> Result<(), String> {
253238×
        match e.kind.as_str() {
254238×
            "identity.owner" | "identity.rotated" | "identity.instance" | "agent.added" | "agent.disabled" => {
25577×
                if e.auth.method != Method::Signature {
2562×
                    return Err(format!("{} must be signed", e.kind));
25775×
                }
25875×
                self.observe(e)
259
            }
260161×
            _ if matches!(e.auth.method, Method::Signature | Method::Passkey) => self.check_signed(e),
261124×
            _ => Ok(()),
262
        }
263238×
    }
264
}
265
266
/// Apply identity and agent events to the projection tables.
26741961×
pub async fn project(conn: &mut SqliteConnection, e: &Event) -> sqlx::Result<()> {
268
    // Who is who is only ever said by a signature; `verify` checks which.
26941961×
    if e.auth.method != Method::Signature {
27041587×
        return Ok(());
271374×
    }
272374×
    let p = e.json();
273374×
    let id = |p: &Json| identity_of(p).ok();
274374×
    match e.kind.as_str() {
275374×
        "identity.owner" | "identity.rotated" | "identity.instance" => {
27683×
            let Some(identity) = id(&p) else { return Ok(()) };
27783×
            let role = if e.kind == "identity.instance" { "instance" } else { "owner" };
27883×
            sqlx::query("UPDATE identities SET current = 0 WHERE role = ?").bind(role).execute(&mut *conn).await?;
27983×
            sqlx::query(
28083×
                "INSERT OR REPLACE INTO identities (key, role, name, identity, previous, current, seq) VALUES (?, ?, ?, ?, ?, 1, ?)",
281
            )
28283×
            .bind(identity.hash().to_hex())
28383×
            .bind(role)
28483×
            .bind(&identity.name)
28583×
            .bind(identity.encode())
28683×
            .bind(identity.previous.map(|k| k.to_hex()))
28783×
            .bind(e.seq as i64)
28883×
            .execute(&mut *conn)
28983×
            .await?;
290
        }
291291×
        "agent.added" => {
29229×
            let Some(identity) = id(&p) else { return Ok(()) };
29329×
            if p.get("name").and_then(Json::as_str).is_none_or(str::is_empty) {
2940×
                return Ok(());
29529×
            }
296174×
            let s = |k: &str| p.get(k).and_then(Json::as_str).unwrap_or("").to_string();
29729×
            sqlx::query(
29829×
                "INSERT OR REPLACE INTO agents (name, key, owner, owner_name, kind, project, home, uid, learn_uid, scopes, created_ms, disabled_ms, seq) \
29929×
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)",
300
            )
30129×
            .bind(s("name"))
30229×
            .bind(identity.hash().to_hex())
30329×
            .bind(s("owner"))
30429×
            .bind(s("owner_name"))
30529×
            .bind(s("kind"))
30629×
            .bind(s("project"))
30729×
            .bind(s("home"))
30829×
            .bind(p.get("uid").and_then(Json::as_i64))
30929×
            .bind(p.get("learn_uid").and_then(Json::as_bool).unwrap_or(false))
31029×
            .bind(p.get("scopes").map(|v| v.to_string()).unwrap_or_else(|| "[]".into()))
31129×
            .bind(e.at_ms as i64)
31229×
            .bind(e.seq as i64)
31329×
            .execute(&mut *conn)
31429×
            .await?;
31529×
            sqlx::query(
31629×
                "INSERT OR REPLACE INTO identities (key, role, name, identity, previous, current, seq) VALUES (?, 'agent', ?, ?, ?, 1, ?)",
317
            )
31829×
            .bind(identity.hash().to_hex())
31929×
            .bind(&identity.name)
32029×
            .bind(identity.encode())
32129×
            .bind(identity.previous.map(|k| k.to_hex()))
32229×
            .bind(e.seq as i64)
32329×
            .execute(&mut *conn)
32429×
            .await?;
325
        }
326262×
        "agent.disabled" => {
3273×
            sqlx::query("UPDATE agents SET disabled_ms = ? WHERE name = ?")
3283×
                .bind(e.at_ms as i64)
3293×
                .bind(p.get("name").and_then(Json::as_str))
3303×
                .execute(&mut *conn)
3313×
                .await?;
332
        }
333259×
        _ => {}
334
    }
335374×
    Ok(())
33641961×
}
337
338
/// The payload of an identity event for `identity`.
33976×
pub fn identity_payload(identity: &Identity) -> Json {
34076×
    serde_json::json!({ "key": identity.hash().to_hex(), "name": identity.name, "identity": hex::encode(identity.encode()) })
34176×
}
342
343298×
fn identity_of(p: &Json) -> Result<Identity, String> {
344298×
    let b = p.get("identity").and_then(Json::as_str).and_then(|h| hex::decode(h).ok()).ok_or("no identity in the event")?;
345298×
    Identity::decode(&b).map_err(|e| format!("malformed identity: {e}"))
346298×
}
347
34894×
fn text(p: &Json, k: &str) -> Result<String, String> {
34994×
    p.get(k).and_then(Json::as_str).map(String::from).ok_or_else(|| format!("no {k}"))
35094×
}
351
352140×
fn self_signed(e: &Event, id: &Identity) -> Result<(), String> {
353140×
    if e.auth.key != Some(id.hash().0) {
3541×
        return Err("not signed by the identity it records".into());
355139×
    }
356139×
    check_proof(e, id)
357140×
}
358
359
/// The event's proof verifies against `identity`, and a CLI proof covers
360
/// exactly the event's payload.
361222×
fn check_proof(e: &Event, identity: &Identity) -> Result<(), String> {
362222×
    let proof = Proof::decode(e.auth.proof.as_deref().ok_or("no proof")?)?;
363222×
    if proof.key() != identity.hash().0 {
3640×
        return Err("the proof is by another key".into());
365222×
    }
366222×
    let body = (proof.method == "cli").then(|| e.payload.as_deref().unwrap_or(b""));
367222×
    proof.verify(identity, body)
368222×
}
369
370
/// A successor of the owner must keep the key pair and chain back to the
371
/// current identity through `previous` (the intermediate identities, if
372
/// any, are carried in the event as `chain`).
37346×
fn accept_successor(cur: &Identity, new: &Identity, p: &Json) -> Result<(), String> {
37446×
    if new.ed25519_pk != cur.ed25519_pk || new.mldsa65_pk != cur.mldsa65_pk {
3750×
        return Err("a successor with a fresh key pair needs `oxy identity rotate`'s attestation, not accepted in 0.1".into());
37646×
    }
37746×
    let chain: Vec<Identity> = p
37846×
        .get("chain")
37946×
        .and_then(Json::as_array)
38046×
        .map(|a| a.iter().filter_map(|h| h.as_str().and_then(|h| hex::decode(h).ok()).and_then(|b| Identity::decode(&b).ok())).collect())
38146×
        .unwrap_or_default();
38246×
    let mut at = new.clone();
38346×
    for _ in 0..=chain.len() {
38446×
        match at.previous {
38546×
            Some(prev) if prev == cur.hash() => return Ok(()),
3860×
            Some(prev) => match chain.iter().find(|i| i.hash() == prev) {
3870×
                Some(i) if i.ed25519_pk == cur.ed25519_pk => at = i.clone(),
3880×
                _ => break,
389
            },
3900×
            None => break,
391
        }
392
    }
3930×
    Err("does not succeed the recorded owner".into())
39446×
}
395
396
/// An event signed by `pair` from the command line: its proof covers the
397
/// canonical payload, so the event's content is bound to the signature.
398113×
pub fn signed_event(pair: &crate::keys::KeyPair, actor: &str, kind: &str, payload: Json) -> crate::event::NewEvent {
399113×
    let body = crate::event::canonical_json(&payload);
400113×
    let proof = Proof::sign(pair, "cli", &body);
401113×
    crate::event::NewEvent::system(kind, payload)
402113×
        .by(actor, crate::event::Auth { key: Some(pair.key.0), method: Method::Signature, assurance: 2, proof: Some(proof.encode()) })
403113×
}