Skip to main content

macp_modes/mode/
passthrough.rs

1use crate::mode::{Mode, ModeResponse};
2use macp_core::error::MacpError;
3use macp_core::session::Session;
4use macp_pb::pb::Envelope;
5
6/// Generic extension mode handler for dynamically registered modes.
7///
8/// Accepts any message type listed in the mode descriptor. Commitment messages
9/// from the initiator resolve the session. All other messages are accepted and
10/// the payload is persisted as mode state.
11pub struct PassthroughMode {
12    pub allowed_message_types: Vec<String>,
13}
14
15impl Mode for PassthroughMode {
16    fn on_session_start(
17        &self,
18        _session: &Session,
19        _env: &Envelope,
20    ) -> Result<ModeResponse, MacpError> {
21        Ok(ModeResponse::NoOp)
22    }
23
24    fn on_message(&self, session: &Session, env: &Envelope) -> Result<ModeResponse, MacpError> {
25        if !self.allowed_message_types.is_empty()
26            && !self
27                .allowed_message_types
28                .iter()
29                .any(|t| t == &env.message_type)
30        {
31            return Err(MacpError::InvalidPayload);
32        }
33
34        if env.message_type == "Commitment" {
35            let commitment =
36                crate::mode::util::validate_commitment_payload_for_session(session, &env.payload)?;
37            let resolution = serde_json::json!({
38                "action": commitment.action,
39                "commitment_id": commitment.commitment_id,
40            })
41            .to_string()
42            .into_bytes();
43            return Ok(ModeResponse::Resolve(resolution));
44        }
45
46        Ok(ModeResponse::PersistState(env.payload.clone()))
47    }
48
49    fn authorize_sender(&self, session: &Session, env: &Envelope) -> Result<(), MacpError> {
50        if env.message_type == "Commitment" {
51            if env.sender != session.initiator_sender {
52                return Err(MacpError::Forbidden);
53            }
54            return Ok(());
55        }
56        if !session.participants.is_empty() && !session.participants.contains(&env.sender) {
57            return Err(MacpError::Forbidden);
58        }
59        Ok(())
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    use macp_pb::pb::CommitmentPayload;
68    use prost::Message;
69
70    fn make_session() -> Session {
71        Session::builder("s1", "ext.test.v1", "alice")
72            .ttl_ms(60_000)
73            .started_at_unix_ms(1000)
74            .participants(vec!["alice".into(), "bob".into()])
75            .mode_version("1.0.0")
76            .configuration_version("cfg-1")
77            .build()
78    }
79
80    fn make_env(sender: &str, message_type: &str, payload: Vec<u8>) -> Envelope {
81        Envelope {
82            macp_version: "1.0".into(),
83            mode: "ext.test.v1".into(),
84            message_type: message_type.into(),
85            message_id: "m1".into(),
86            session_id: "s1".into(),
87            sender: sender.into(),
88            timestamp_unix_ms: 1000,
89            payload,
90        }
91    }
92
93    #[test]
94    fn accepts_any_message_when_no_filter() {
95        let mode = PassthroughMode {
96            allowed_message_types: vec![],
97        };
98        let session = make_session();
99        let env = make_env("alice", "CustomMessage", b"data".to_vec());
100        let result = mode.on_message(&session, &env).unwrap();
101        assert!(matches!(result, ModeResponse::PersistState(_)));
102    }
103
104    #[test]
105    fn rejects_unlisted_message_type() {
106        let mode = PassthroughMode {
107            allowed_message_types: vec!["Allowed".into()],
108        };
109        let session = make_session();
110        let env = make_env("alice", "NotAllowed", vec![]);
111        assert!(mode.on_message(&session, &env).is_err());
112    }
113
114    #[test]
115    fn commitment_resolves_session() {
116        let mode = PassthroughMode {
117            allowed_message_types: vec!["Commitment".into()],
118        };
119        let session = make_session();
120        let payload = CommitmentPayload {
121            commitment_id: "c1".into(),
122            action: "test.done".into(),
123            authority_scope: "test".into(),
124            reason: "done".into(),
125            mode_version: "1.0.0".into(),
126            policy_version: String::new(),
127            configuration_version: "cfg-1".into(),
128            outcome_positive: true,
129            supersedes: None,
130        }
131        .encode_to_vec();
132        let env = make_env("alice", "Commitment", payload);
133        let result = mode.on_message(&session, &env).unwrap();
134        assert!(matches!(result, ModeResponse::Resolve(_)));
135    }
136
137    #[test]
138    fn non_initiator_commitment_forbidden() {
139        let mode = PassthroughMode {
140            allowed_message_types: vec!["Commitment".into()],
141        };
142        let session = make_session();
143        let env = make_env("bob", "Commitment", vec![]);
144        assert!(mode.authorize_sender(&session, &env).is_err());
145    }
146}