Skip to main content

khive_runtime/
mailbox_view.rs

1use std::sync::Arc;
2
3use khive_gate::{check_with_mailbox_policy, mailbox_read_owner};
4use serde_json::Value;
5use sha2::{Digest, Sha256};
6
7use crate::{
8    engine_config::ActorConfig, ActorRef, Gate, GateDecision, GateError, GateRef, GateRequest,
9    KhiveRuntime, MailboxPolicyError, MailboxReadGate, NamespaceToken, RuntimeError, RuntimeResult,
10};
11
12/// An authorized mailbox selection. This never replaces the real caller token.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct MailboxView {
15    pub actor_id: String,
16    pub delegated: bool,
17}
18
19/// The same strict selector check is used before ordinary/intercepted dispatch
20/// and before direct pack-handler reads. JSON null is not an omitted selector.
21pub(crate) fn validate_mailbox_request(req: &GateRequest) -> RuntimeResult<()> {
22    mailbox_read_owner(req)
23        .map(|_| ())
24        .map_err(|error| RuntimeError::InvalidInput(error.to_string()))
25}
26
27impl KhiveRuntime {
28    /// Authorize a read-only mailbox view without changing token identity.
29    ///
30    /// The original arguments are retained as gate input. Direct handler calls
31    /// consult both the existing policy and the separate mailbox capability;
32    /// a permissive or unconfigured gate never grants cross-actor reads.
33    pub fn authorize_mailbox_view(
34        &self,
35        token: &NamespaceToken,
36        verb: &str,
37        selector: Option<&str>,
38        args: &Value,
39    ) -> RuntimeResult<MailboxView> {
40        let selector_field = match verb {
41            "comm.inbox" | "comm.thread" => "mailbox_actor",
42            "comm.probe" => "actor",
43            _ => {
44                return Err(RuntimeError::InvalidInput(
45                    "mailbox views are supported only by comm.inbox, comm.thread and comm.probe"
46                        .into(),
47                ));
48            }
49        };
50        let req = GateRequest::new(
51            token.actor().clone(),
52            token.gate_namespace().clone(),
53            verb,
54            args.clone(),
55        );
56        validate_mailbox_request(&req)?;
57        if args.get(selector_field).and_then(Value::as_str) != selector {
58            return Err(RuntimeError::InvalidInput(format!(
59                "mailbox selector must match the original {selector_field} argument"
60            )));
61        }
62        match check_with_mailbox_policy(self.config().gate.as_ref(), &req) {
63            Ok(GateDecision::Allow { .. }) => {
64                let owner = mailbox_read_owner(&req)
65                    .map_err(|error| RuntimeError::InvalidInput(error.to_string()))?;
66                Ok(MailboxView {
67                    delegated: owner.is_some(),
68                    actor_id: owner.map_or_else(|| token.actor().id.clone(), |owner| owner.id),
69                })
70            }
71            Ok(GateDecision::Deny { reason }) => Err(RuntimeError::permission_denied(verb, reason)),
72            Err(error) => Err(RuntimeError::GateUnavailable {
73                verb: verb.to_string(),
74                reason: error.wire_reason().to_string(),
75            }),
76        }
77    }
78}
79
80impl ActorConfig {
81    pub(crate) fn mailbox_gate(
82        &self,
83        inner: GateRef,
84    ) -> Result<Option<MailboxReadGate>, MailboxPolicyError> {
85        if self.mailbox_readers.is_empty() {
86            return Ok(None);
87        }
88        if self.mailbox_readers.len() > 256 {
89            return Err(MailboxPolicyError::TooManyReaders);
90        }
91        if self
92            .mailbox_readers
93            .iter()
94            .any(|id| !crate::is_valid_mailbox_actor_label(id))
95        {
96            return Err(MailboxPolicyError::InvalidReader);
97        }
98        // Only the explicit serving-file owner can grant access; never infer
99        // this from the effective runtime actor (which may come from env/CLI).
100        let owner = self.id.as_deref().ok_or(MailboxPolicyError::InvalidOwner)?;
101        crate::Namespace::parse(owner).map_err(|_| MailboxPolicyError::InvalidOwner)?;
102        let readers = self
103            .mailbox_readers
104            .iter()
105            .map(|id| ActorRef::new("actor", id.clone()))
106            .collect();
107        MailboxReadGate::new(inner, ActorRef::new("actor", owner), readers).map(Some)
108    }
109}
110
111/// Boot conversion is intentionally infallible for existing embedding callers.
112/// Invalid programmatic mailbox configuration therefore installs an unavailable
113/// gate rather than falling back to the base policy. File loading rejects it.
114pub(crate) fn configured_mailbox_gate(actor: &ActorConfig, inner: GateRef) -> GateRef {
115    match actor.mailbox_gate(inner.clone()) {
116        Ok(Some(gate)) => Arc::new(gate),
117        Ok(None) => inner,
118        Err(_) => {
119            let mut hash = Sha256::new();
120            hash.update(b"khive.invalid-mailbox-read-policy.v1\0");
121            for field in [inner.configuration_fingerprint(), actor.id.as_deref()] {
122                match field {
123                    Some(value) => {
124                        hash.update([1]);
125                        hash.update((value.len() as u64).to_be_bytes());
126                        hash.update(value.as_bytes());
127                    }
128                    None => hash.update([0]),
129                }
130            }
131            hash.update((actor.mailbox_readers.len() as u64).to_be_bytes());
132            for reader in &actor.mailbox_readers {
133                hash.update((reader.len() as u64).to_be_bytes());
134                hash.update(reader.as_bytes());
135            }
136            Arc::new(InvalidMailboxConfigGate {
137                fingerprint: format!("sha256:{:x}", hash.finalize()),
138            })
139        }
140    }
141}
142
143#[derive(Debug)]
144struct InvalidMailboxConfigGate {
145    fingerprint: String,
146}
147
148impl Gate for InvalidMailboxConfigGate {
149    fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
150        Err(GateError::Policy(
151            "invalid mailbox reader configuration".into(),
152        ))
153    }
154
155    fn configuration_fingerprint(&self) -> Option<&str> {
156        Some(&self.fingerprint)
157    }
158}
159
160#[cfg(test)]
161mod tests;