khive_runtime/
mailbox_view.rs1use std::sync::Arc;
2
3use khive_gate::{check_with_mailbox_policy, mailbox_read_owner};
4use khive_storage::note::{Note, NoteMailboxScope};
5use serde_json::Value;
6use sha2::{Digest, Sha256};
7
8use crate::{
9 engine_config::ActorConfig, ActorRef, Gate, GateDecision, GateError, GateRef, GateRequest,
10 KhiveRuntime, MailboxPolicyError, MailboxReadGate, NamespaceToken, RuntimeError, RuntimeResult,
11};
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct MailboxView {
16 pub actor_id: String,
17 pub delegated: bool,
18}
19
20impl MailboxView {
21 fn legacy_local(&self, token: &NamespaceToken) -> bool {
22 !self.delegated && token.actor().is_anonymous() && token.actor().id == "local"
23 }
24
25 pub fn note_scope(&self, token: &NamespaceToken) -> NoteMailboxScope {
29 NoteMailboxScope {
30 actor_id: self.actor_id.clone(),
31 legacy_local: self.legacy_local(token),
32 }
33 }
34
35 pub fn permits_message_note(&self, token: &NamespaceToken, note: &Note) -> bool {
39 permits_message_note(&self.actor_id, self.legacy_local(token), note)
40 }
41
42 pub fn scope_permits_message_note(scope: &NoteMailboxScope, note: &Note) -> bool {
46 permits_message_note(&scope.actor_id, scope.legacy_local, note)
47 }
48}
49
50fn permits_message_note(actor_id: &str, legacy_local: bool, note: &Note) -> bool {
51 if note.kind != "message" {
52 return true;
53 }
54 let properties = note.properties.as_ref();
55 let text = |key: &str| -> Option<&str> {
56 properties
57 .and_then(|value| value.get(key))
58 .and_then(Value::as_str)
59 };
60 match text("direction") {
61 Some("inbound") => {
62 text("to_actor") == Some(actor_id)
63 || (legacy_local
64 && properties
65 .and_then(|p| p.get("to_actor"))
66 .is_none_or(Value::is_null))
67 }
68 Some("outbound") => {
69 text("from_actor") == Some(actor_id)
70 || (legacy_local
71 && properties
72 .and_then(|p| p.get("from_actor"))
73 .is_none_or(Value::is_null))
74 }
75 None => {
78 legacy_local
79 && properties
80 .and_then(|p| p.get("direction"))
81 .is_none_or(Value::is_null)
82 && text("from_actor").is_none()
83 && text("to_actor").is_none()
84 }
85 _ => false,
86 }
87}
88
89pub(crate) fn validate_mailbox_request(req: &GateRequest) -> RuntimeResult<()> {
92 mailbox_read_owner(req)
93 .map(|_| ())
94 .map_err(|error| RuntimeError::InvalidInput(error.to_string()))
95}
96
97impl KhiveRuntime {
98 pub fn authorize_mailbox_view(
104 &self,
105 token: &NamespaceToken,
106 verb: &str,
107 selector: Option<&str>,
108 args: &Value,
109 ) -> RuntimeResult<MailboxView> {
110 let selector_field = match verb {
111 "comm.inbox" | "comm.thread" => Some("mailbox_actor"),
112 "comm.probe" => Some("actor"),
113 "list" | "search" | "get" | "context" | "neighbors" => None,
117 _ => {
118 return Err(RuntimeError::InvalidInput(
119 "mailbox views are supported only by comm.inbox, comm.thread, comm.probe, list, search, get, context and neighbors"
120 .into(),
121 ));
122 }
123 };
124 let req = GateRequest::new(
125 token.actor().clone(),
126 token.gate_namespace().clone(),
127 verb,
128 args.clone(),
129 );
130 validate_mailbox_request(&req)?;
131 if let Some(selector_field) = selector_field {
132 if args.get(selector_field).and_then(Value::as_str) != selector {
133 return Err(RuntimeError::InvalidInput(format!(
134 "mailbox selector must match the original {selector_field} argument"
135 )));
136 }
137 } else if selector.is_some() {
138 return Err(RuntimeError::InvalidInput(format!(
139 "{verb} has no cross-actor mailbox selector"
140 )));
141 }
142 match check_with_mailbox_policy(self.config().gate.as_ref(), &req) {
143 Ok(GateDecision::Allow { .. }) => {
144 let owner = mailbox_read_owner(&req)
145 .map_err(|error| RuntimeError::InvalidInput(error.to_string()))?;
146 Ok(MailboxView {
147 delegated: owner.is_some(),
148 actor_id: owner.map_or_else(|| token.actor().id.clone(), |owner| owner.id),
149 })
150 }
151 Ok(GateDecision::Deny { reason }) => Err(RuntimeError::permission_denied(verb, reason)),
152 Err(error) => Err(RuntimeError::GateUnavailable {
153 verb: verb.to_string(),
154 reason: error.wire_reason().to_string(),
155 }),
156 }
157 }
158}
159
160impl ActorConfig {
161 pub(crate) fn mailbox_gate(
162 &self,
163 inner: GateRef,
164 ) -> Result<Option<MailboxReadGate>, MailboxPolicyError> {
165 if self.mailbox_readers.is_empty() {
166 return Ok(None);
167 }
168 if self.mailbox_readers.len() > 256 {
169 return Err(MailboxPolicyError::TooManyReaders);
170 }
171 if self
172 .mailbox_readers
173 .iter()
174 .any(|id| !crate::is_valid_mailbox_actor_label(id))
175 {
176 return Err(MailboxPolicyError::InvalidReader);
177 }
178 let owner = self.id.as_deref().ok_or(MailboxPolicyError::InvalidOwner)?;
181 crate::Namespace::parse(owner).map_err(|_| MailboxPolicyError::InvalidOwner)?;
182 let readers = self
183 .mailbox_readers
184 .iter()
185 .map(|id| ActorRef::new("actor", id.clone()))
186 .collect();
187 MailboxReadGate::new(inner, ActorRef::new("actor", owner), readers).map(Some)
188 }
189}
190
191pub(crate) fn configured_mailbox_gate(actor: &ActorConfig, inner: GateRef) -> GateRef {
195 match actor.mailbox_gate(inner.clone()) {
196 Ok(Some(gate)) => Arc::new(gate),
197 Ok(None) => inner,
198 Err(_) => {
199 let mut hash = Sha256::new();
200 hash.update(b"khive.invalid-mailbox-read-policy.v1\0");
201 for field in [inner.configuration_fingerprint(), actor.id.as_deref()] {
202 match field {
203 Some(value) => {
204 hash.update([1]);
205 hash.update((value.len() as u64).to_be_bytes());
206 hash.update(value.as_bytes());
207 }
208 None => hash.update([0]),
209 }
210 }
211 hash.update((actor.mailbox_readers.len() as u64).to_be_bytes());
212 for reader in &actor.mailbox_readers {
213 hash.update((reader.len() as u64).to_be_bytes());
214 hash.update(reader.as_bytes());
215 }
216 Arc::new(InvalidMailboxConfigGate {
217 fingerprint: format!("sha256:{:x}", hash.finalize()),
218 })
219 }
220 }
221}
222
223#[derive(Debug)]
224struct InvalidMailboxConfigGate {
225 fingerprint: String,
226}
227
228impl Gate for InvalidMailboxConfigGate {
229 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
230 Err(GateError::Policy(
231 "invalid mailbox reader configuration".into(),
232 ))
233 }
234
235 fn configuration_fingerprint(&self) -> Option<&str> {
236 Some(&self.fingerprint)
237 }
238}
239
240#[cfg(test)]
241mod tests;