macp_modes/mode/
passthrough.rs1use crate::mode::{Mode, ModeResponse};
2use macp_core::error::MacpError;
3use macp_core::session::Session;
4use macp_pb::pb::Envelope;
5
6pub 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 use macp_core::session::SessionState;
67 use macp_pb::pb::CommitmentPayload;
68 use prost::Message;
69
70 fn make_session() -> Session {
71 Session {
72 session_id: "s1".into(),
73 state: SessionState::Open,
74 ttl_expiry: i64::MAX,
75 ttl_ms: 60_000,
76 started_at_unix_ms: 1000,
77 resolution: None,
78 mode: "ext.test.v1".into(),
79 mode_state: vec![],
80 participants: vec!["alice".into(), "bob".into()],
81 seen_message_ids: std::collections::HashSet::new(),
82 intent: String::new(),
83 mode_version: "1.0.0".into(),
84 configuration_version: "cfg-1".into(),
85 policy_version: String::new(),
86 context_id: String::new(),
87 extensions: std::collections::HashMap::new(),
88 roots: vec![],
89 initiator_sender: "alice".into(),
90 participant_message_counts: std::collections::HashMap::new(),
91 participant_last_seen: std::collections::HashMap::new(),
92 policy_definition: None,
93 suspended_at_ms: None,
94 accumulated_suspended_ms: 0,
95 }
96 }
97
98 fn make_env(sender: &str, message_type: &str, payload: Vec<u8>) -> Envelope {
99 Envelope {
100 macp_version: "1.0".into(),
101 mode: "ext.test.v1".into(),
102 message_type: message_type.into(),
103 message_id: "m1".into(),
104 session_id: "s1".into(),
105 sender: sender.into(),
106 timestamp_unix_ms: 1000,
107 payload,
108 }
109 }
110
111 #[test]
112 fn accepts_any_message_when_no_filter() {
113 let mode = PassthroughMode {
114 allowed_message_types: vec![],
115 };
116 let session = make_session();
117 let env = make_env("alice", "CustomMessage", b"data".to_vec());
118 let result = mode.on_message(&session, &env).unwrap();
119 assert!(matches!(result, ModeResponse::PersistState(_)));
120 }
121
122 #[test]
123 fn rejects_unlisted_message_type() {
124 let mode = PassthroughMode {
125 allowed_message_types: vec!["Allowed".into()],
126 };
127 let session = make_session();
128 let env = make_env("alice", "NotAllowed", vec![]);
129 assert!(mode.on_message(&session, &env).is_err());
130 }
131
132 #[test]
133 fn commitment_resolves_session() {
134 let mode = PassthroughMode {
135 allowed_message_types: vec!["Commitment".into()],
136 };
137 let session = make_session();
138 let payload = CommitmentPayload {
139 commitment_id: "c1".into(),
140 action: "test.done".into(),
141 authority_scope: "test".into(),
142 reason: "done".into(),
143 mode_version: "1.0.0".into(),
144 policy_version: String::new(),
145 configuration_version: "cfg-1".into(),
146 outcome_positive: true,
147 supersedes: None,
148 }
149 .encode_to_vec();
150 let env = make_env("alice", "Commitment", payload);
151 let result = mode.on_message(&session, &env).unwrap();
152 assert!(matches!(result, ModeResponse::Resolve(_)));
153 }
154
155 #[test]
156 fn non_initiator_commitment_forbidden() {
157 let mode = PassthroughMode {
158 allowed_message_types: vec!["Commitment".into()],
159 };
160 let session = make_session();
161 let env = make_env("bob", "Commitment", vec![]);
162 assert!(mode.authorize_sender(&session, &env).is_err());
163 }
164}