Skip to main content

yapi_core/extensions/
events.rs

1//! Which `pi.events` messages cross between extensions with different grants.
2
3use std::fmt;
4
5use serde::de::{self, MapAccess, Visitor};
6use serde::ser::SerializeMap as _;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use super::Grants;
10
11/// The `pi.events` channels an extension may send to, and hear from,
12/// extensions whose grants differ from its own. Each pattern is an exact
13/// channel, a prefix ending in `*`, or `*` for every channel.
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15pub struct EventPolicy {
16    /// The channels it may send on.
17    pub send: Vec<String>,
18    /// The channels it may hear.
19    pub receive: Vec<String>,
20}
21
22impl Default for EventPolicy {
23    fn default() -> EventPolicy {
24        EventPolicy::shared()
25    }
26}
27
28impl EventPolicy {
29    /// Every channel both ways, as pi shares its bus: the default.
30    pub fn shared() -> EventPolicy {
31        EventPolicy {
32            send: vec!["*".into()],
33            receive: vec!["*".into()],
34        }
35    }
36
37    /// No channel either way, so only extensions with identical grants
38    /// hear it and are heard.
39    pub fn isolated() -> EventPolicy {
40        EventPolicy {
41            send: Vec::new(),
42            receive: Vec::new(),
43        }
44    }
45
46    /// The policy with patterns `send` and `receive`. `Err` names the first
47    /// pattern that is empty or uses glob syntax other than a final `*`.
48    pub fn new(send: Vec<String>, receive: Vec<String>) -> Result<EventPolicy, String> {
49        for pattern in send.iter().chain(&receive) {
50            let body = pattern.strip_suffix('*').unwrap_or(pattern);
51            if pattern.is_empty() || body.contains(['*', '?', '[', ']', '{', '}']) {
52                return Err(format!(
53                    "invalid event channel pattern \"{pattern}\": use a channel name, a prefix ending in `*`, or `*`"
54                ));
55            }
56        }
57        Ok(EventPolicy { send, receive })
58    }
59
60    /// Whether it may send on `channel` across grants.
61    pub fn sends(&self, channel: &str) -> bool {
62        matches_any(&self.send, channel)
63    }
64
65    /// Whether it may hear `channel` across grants.
66    pub fn receives(&self, channel: &str) -> bool {
67        matches_any(&self.receive, channel)
68    }
69
70    /// The channels both `self` and `other` send on and hear.
71    pub fn intersection(&self, other: &EventPolicy) -> EventPolicy {
72        EventPolicy {
73            send: both(&self.send, &other.send),
74            receive: both(&self.receive, &other.receive),
75        }
76    }
77}
78
79/// The patterns of `left` and `right` that the other side covers whole. Two
80/// patterns that match one channel are nested, so these match exactly the
81/// channels both sides match.
82fn both(left: &[String], right: &[String]) -> Vec<String> {
83    let covers = |patterns: &[String], pattern: &str| match pattern.strip_suffix('*') {
84        Some(prefix) => patterns.iter().any(|other| {
85            other
86                .strip_suffix('*')
87                .is_some_and(|wider| prefix.starts_with(wider))
88        }),
89        None => matches_any(patterns, pattern),
90    };
91    let mut found: Vec<String> = Vec::new();
92    for pattern in left.iter().filter(|pattern| covers(right, pattern)) {
93        found.push(pattern.clone());
94    }
95    for pattern in right.iter().filter(|pattern| covers(left, pattern)) {
96        if !found.contains(pattern) {
97            found.push(pattern.clone());
98        }
99    }
100    found
101}
102
103fn matches_any(patterns: &[String], channel: &str) -> bool {
104    patterns
105        .iter()
106        .any(|pattern| match pattern.strip_suffix('*') {
107            Some(prefix) => channel.starts_with(prefix),
108            None => pattern == channel,
109        })
110}
111
112/// Whether an event on `channel` from an extension with `sender`'s grants
113/// and policy reaches one with `receiver`'s: always between identical
114/// grants, since neither gains anything, and otherwise when the sender may
115/// send the channel and the receiver may hear it.
116pub fn delivers(
117    sender: (&Grants, &EventPolicy),
118    receiver: (&Grants, &EventPolicy),
119    channel: &str,
120) -> bool {
121    // shortcut: identical grants ignore filesystem roots, which only codemode narrows,
122    // compare roots too once file grants are path-scoped.
123    sender.0 == receiver.0 || sender.1.sends(channel) && receiver.1.receives(channel)
124}
125
126/// Written as a manifest writes it: `"shared"`, `"isolated"`, or
127/// `{ send, receive }`.
128impl Serialize for EventPolicy {
129    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
130        if *self == EventPolicy::shared() {
131            return serializer.serialize_str("shared");
132        }
133        if *self == EventPolicy::isolated() {
134            return serializer.serialize_str("isolated");
135        }
136        let mut map = serializer.serialize_map(Some(2))?;
137        map.serialize_entry("send", &self.send)?;
138        map.serialize_entry("receive", &self.receive)?;
139        map.end()
140    }
141}
142
143/// Read from `"shared"`, `"isolated"`, or `{ send, receive }`, where a
144/// missing list is empty.
145impl<'de> Deserialize<'de> for EventPolicy {
146    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<EventPolicy, D::Error> {
147        struct PolicyVisitor;
148
149        impl<'de> Visitor<'de> for PolicyVisitor {
150            type Value = EventPolicy;
151
152            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
153                formatter.write_str("\"shared\", \"isolated\" or { send, receive }")
154            }
155
156            fn visit_str<E: de::Error>(self, value: &str) -> Result<EventPolicy, E> {
157                match value {
158                    "shared" => Ok(EventPolicy::shared()),
159                    "isolated" => Ok(EventPolicy::isolated()),
160                    _ => Err(E::invalid_value(de::Unexpected::Str(value), &self)),
161                }
162            }
163
164            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<EventPolicy, A::Error> {
165                let (mut send, mut receive) = (Vec::new(), Vec::new());
166                while let Some(key) = map.next_key::<String>()? {
167                    match key.as_str() {
168                        "send" => send = map.next_value()?,
169                        "receive" => receive = map.next_value()?,
170                        _ => return Err(de::Error::unknown_field(&key, &["send", "receive"])),
171                    }
172                }
173                EventPolicy::new(send, receive).map_err(de::Error::custom)
174            }
175        }
176
177        deserializer.deserialize_any(PolicyVisitor)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn event_policy_routing() {
187        let policy = |send: &[&str], receive: &[&str]| {
188            let owned = |patterns: &[&str]| patterns.iter().map(|p| (*p).to_owned()).collect();
189            EventPolicy::new(owned(send), owned(receive)).unwrap()
190        };
191        let todo = policy(&["todo:*"], &["subagent:done"]);
192        assert!(todo.sends("todo:changed") && todo.sends("todo:"));
193        assert!(!todo.sends("todos") && !todo.sends("subagent:done"));
194        assert!(todo.receives("subagent:done") && !todo.receives("subagent:done:x"));
195        assert!(EventPolicy::default().sends("any") && EventPolicy::shared().receives(""));
196        assert!(!EventPolicy::isolated().sends("any"));
197        // An intersection matches exactly the channels both match.
198        let wide = policy(&["todo:*", "x"], &["*"]);
199        let both = todo.intersection(&wide);
200        assert_eq!(both, policy(&["todo:*"], &["subagent:done"]));
201        let narrow = policy(&["todo:a", "todo:b*", "y"], &[]);
202        assert_eq!(
203            narrow.intersection(&todo),
204            policy(&["todo:a", "todo:b*"], &[])
205        );
206        assert_eq!(
207            EventPolicy::shared().intersection(&EventPolicy::isolated()),
208            EventPolicy::isolated()
209        );
210
211        let fs = Grants {
212            filesystem: true,
213            ..Grants::none()
214        };
215        let all = Grants::default();
216        let isolated = EventPolicy::isolated();
217        let shared = EventPolicy::shared();
218        let cases = [
219            // Identical grants always share, whatever the policies.
220            ((&fs, &isolated), (&fs, &isolated), "x", true),
221            ((&all, &shared), (&fs, &shared), "x", true),
222            ((&fs, &isolated), (&all, &shared), "x", false),
223            ((&all, &shared), (&fs, &isolated), "x", false),
224            ((&fs, &todo), (&all, &shared), "todo:changed", true),
225            ((&fs, &todo), (&all, &shared), "other", false),
226            ((&all, &shared), (&fs, &todo), "subagent:done", true),
227            ((&all, &shared), (&fs, &todo), "todo:changed", false),
228        ];
229        for (sender, receiver, channel, expected) in cases {
230            assert_eq!(
231                delivers(sender, receiver, channel),
232                expected,
233                "{sender:?} to {receiver:?} on {channel}"
234            );
235        }
236
237        for bad in ["", "a*b", "**", "todo:?", "[ab]", "{a,b}"] {
238            assert!(
239                EventPolicy::new(vec![bad.into()], Vec::new()).is_err(),
240                "{bad}"
241            );
242        }
243        let parse =
244            |json: &str| serde_json::from_str::<EventPolicy>(json).map_err(|e| e.to_string());
245        assert_eq!(parse("\"shared\""), Ok(EventPolicy::shared()));
246        assert_eq!(parse("\"isolated\""), Ok(EventPolicy::isolated()));
247        assert_eq!(
248            parse(r#"{"send": ["todo:*"]}"#),
249            Ok(policy(&["todo:*"], &[]))
250        );
251        for bad in [r#""open""#, r#"{"sent": []}"#, r#"{"send": ["a*b"]}"#, "[]"] {
252            assert!(parse(bad).is_err(), "{bad}");
253        }
254        for policy in [EventPolicy::shared(), EventPolicy::isolated(), todo] {
255            let json = yapi_types::json::to_string(&policy).unwrap();
256            assert_eq!(parse(&json), Ok(policy));
257        }
258    }
259}