1use macp_core::error::MacpError;
2use std::collections::{HashMap, HashSet, VecDeque};
3use std::fs;
4use std::path::PathBuf;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7use tokio::sync::Mutex;
8use tonic::metadata::MetadataMap;
9
10#[derive(Clone, Debug)]
11pub struct AuthIdentity {
12 pub sender: String,
13 pub allowed_modes: Option<HashSet<String>>,
14 pub can_start_sessions: bool,
15 pub max_open_sessions: Option<usize>,
16 pub can_manage_mode_registry: bool,
17 pub is_observer: bool,
18}
19
20#[derive(Clone, Debug, serde::Deserialize)]
21struct RawIdentity {
22 token: String,
23 sender: String,
24 #[serde(default)]
25 allowed_modes: Vec<String>,
26 #[serde(default = "default_true")]
27 can_start_sessions: bool,
28 max_open_sessions: Option<usize>,
29 #[serde(default)]
30 can_manage_mode_registry: bool,
31 #[serde(default)]
32 is_observer: bool,
33}
34
35#[derive(Clone, Debug, serde::Deserialize)]
36#[serde(untagged)]
37enum RawConfig {
38 List(Vec<RawIdentity>),
39 Wrapped { tokens: Vec<RawIdentity> },
40}
41
42fn default_true() -> bool {
43 true
44}
45
46#[derive(Clone, Debug)]
47pub struct RateLimitConfig {
48 pub limit: usize,
49 pub window: Duration,
50}
51
52pub const DEFAULT_LIST_SESSIONS_PAGE_SIZE: usize = 100;
55
56pub const MAX_LIST_SESSIONS_PAGE_SIZE: usize = 1000;
59
60struct RawPageSizeEnv {
69 default_raw: Option<String>,
70 max_raw: Option<String>,
71}
72
73#[derive(Default)]
74struct RateBucket {
75 start_events: Mutex<HashMap<String, VecDeque<Instant>>>,
76 message_events: Mutex<HashMap<String, VecDeque<Instant>>>,
77 start_sweep_counter: std::sync::atomic::AtomicU64,
82 message_sweep_counter: std::sync::atomic::AtomicU64,
83}
84
85#[derive(Clone)]
86pub struct SecurityLayer {
87 identities: Arc<HashMap<String, AuthIdentity>>,
88 rate_bucket: Arc<RateBucket>,
89 auth_chain: Option<Arc<crate::auth::AuthResolverChain>>,
90 pub max_payload_bytes: usize,
91 pub list_sessions_default_page_size: usize,
93 pub list_sessions_max_page_size: usize,
95 session_start_rate: RateLimitConfig,
96 message_rate: RateLimitConfig,
97}
98
99impl SecurityLayer {
100 pub fn dev_mode() -> Self {
104 Self {
105 identities: Arc::new(HashMap::new()),
106 rate_bucket: Arc::new(RateBucket::default()),
107 auth_chain: None,
108 max_payload_bytes: 1_048_576,
109 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
115 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
116 session_start_rate: RateLimitConfig {
117 limit: usize::MAX,
118 window: Duration::from_secs(60),
119 },
120 message_rate: RateLimitConfig {
121 limit: usize::MAX,
122 window: Duration::from_secs(60),
123 },
124 }
125 }
126
127 fn dev_authenticate(&self, metadata: &MetadataMap) -> Result<AuthIdentity, MacpError> {
137 if let Some(token) = Self::bearer_token(metadata) {
138 return Ok(AuthIdentity {
139 sender: token,
140 allowed_modes: None,
141 can_start_sessions: true,
142 max_open_sessions: None,
143 can_manage_mode_registry: true,
144 is_observer: false,
145 });
146 }
147 Err(MacpError::Unauthenticated)
148 }
149
150 fn resolve_list_sessions_page_sizes(raw: RawPageSizeEnv) -> (usize, usize) {
166 let RawPageSizeEnv {
167 default_raw,
168 max_raw,
169 } = raw;
170 let max = max_raw
175 .and_then(|v| v.parse::<usize>().ok())
176 .filter(|&v| v > 0)
177 .unwrap_or(MAX_LIST_SESSIONS_PAGE_SIZE);
178 let default_from_env = default_raw
179 .and_then(|v| v.parse::<usize>().ok())
180 .filter(|&v| v > 0);
181 let mut default = default_from_env.unwrap_or(DEFAULT_LIST_SESSIONS_PAGE_SIZE);
188 if default > max {
189 tracing::warn!(
190 effective_default = default,
191 default_source = Self::page_size_default_source(default_from_env.is_some()),
192 max,
193 "{}",
194 Self::clamp_warning_message(default_from_env.is_some())
195 );
196 default = max;
197 }
198 (default, max)
199 }
200
201 fn page_size_default_source(default_from_env: bool) -> &'static str {
204 if default_from_env {
205 "MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE"
206 } else {
207 "built-in"
208 }
209 }
210
211 fn clamp_warning_message(default_from_env: bool) -> &'static str {
223 if default_from_env {
224 "MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE exceeds MACP_LIST_SESSIONS_MAX_PAGE_SIZE; clamping the default down to the max"
225 } else {
226 "the effective ListSessions default page size is the built-in one and exceeds MACP_LIST_SESSIONS_MAX_PAGE_SIZE; clamping the default down to the max"
227 }
228 }
229
230 pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
231 let max_payload_bytes = std::env::var("MACP_MAX_PAYLOAD_BYTES")
232 .ok()
233 .and_then(|v| v.parse::<usize>().ok())
234 .unwrap_or(1_048_576);
235
236 let (list_sessions_default_page_size, list_sessions_max_page_size) =
246 Self::resolve_list_sessions_page_sizes(RawPageSizeEnv {
247 default_raw: std::env::var("MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE").ok(),
248 max_raw: std::env::var("MACP_LIST_SESSIONS_MAX_PAGE_SIZE").ok(),
249 });
250
251 let session_start_rate = RateLimitConfig {
252 limit: std::env::var("MACP_SESSION_START_LIMIT_PER_MINUTE")
253 .ok()
254 .and_then(|v| v.parse::<usize>().ok())
255 .unwrap_or(60),
256 window: Duration::from_secs(60),
257 };
258 let message_rate = RateLimitConfig {
259 limit: std::env::var("MACP_MESSAGE_LIMIT_PER_MINUTE")
260 .ok()
261 .and_then(|v| v.parse::<usize>().ok())
262 .unwrap_or(600),
263 window: Duration::from_secs(60),
264 };
265
266 let raw = if let Ok(json) = std::env::var("MACP_AUTH_TOKENS_JSON") {
267 Some(json)
268 } else if let Ok(path) = std::env::var("MACP_AUTH_TOKENS_FILE") {
269 Some(fs::read_to_string(PathBuf::from(path))?)
270 } else {
271 None
272 };
273
274 let identities = raw
275 .as_ref()
276 .map(|json| Self::parse_identities(json))
277 .transpose()?
278 .unwrap_or_default();
279
280 let mut resolvers: Vec<Box<dyn crate::auth::AuthResolver>> = Vec::new();
282
283 if let Ok(issuer) = std::env::var("MACP_AUTH_ISSUER") {
285 let audience =
286 std::env::var("MACP_AUTH_AUDIENCE").unwrap_or_else(|_| "macp-runtime".into());
287 let cache_ttl = std::env::var("MACP_AUTH_JWKS_TTL_SECS")
288 .ok()
289 .and_then(|v| v.parse().ok())
290 .unwrap_or(300u64);
291 let algorithms = std::env::var("MACP_AUTH_JWT_ALGS")
296 .ok()
297 .map(|raw| {
298 raw.split(',')
299 .filter_map(|a| match a.trim().to_uppercase().as_str() {
300 "RS256" => Some(jsonwebtoken::Algorithm::RS256),
301 "ES256" => Some(jsonwebtoken::Algorithm::ES256),
302 "HS256" => Some(jsonwebtoken::Algorithm::HS256),
303 other => {
304 tracing::warn!(alg = other, "ignoring unsupported JWT algorithm");
305 None
306 }
307 })
308 .collect::<Vec<_>>()
309 })
310 .filter(|algs| !algs.is_empty())
311 .unwrap_or_else(|| {
312 vec![
313 jsonwebtoken::Algorithm::RS256,
314 jsonwebtoken::Algorithm::ES256,
315 ]
316 });
317 let config = crate::auth::resolvers::jwt_bearer::JwtConfig {
318 issuer,
319 audience,
320 algorithms,
321 };
322 if let Ok(jwks_json) = std::env::var("MACP_AUTH_JWKS_JSON") {
323 match crate::auth::resolvers::JwtBearerResolver::from_inline_json(
324 config, &jwks_json,
325 ) {
326 Ok(resolver) => resolvers.push(Box::new(resolver)),
327 Err(e) => {
328 tracing::error!("failed to create JWT resolver from inline JWKS: {e}")
329 }
330 }
331 } else if let Ok(jwks_url) = std::env::var("MACP_AUTH_JWKS_URL") {
332 resolvers.push(Box::new(
333 crate::auth::resolvers::JwtBearerResolver::from_url(
334 config, jwks_url, cache_ttl,
335 ),
336 ));
337 }
338 }
339
340 if !identities.is_empty() {
342 resolvers.push(Box::new(crate::auth::resolvers::StaticBearerResolver::new(
343 identities.clone(),
344 )));
345 }
346
347 let auth_chain = if resolvers.is_empty() {
348 None
349 } else {
350 Some(Arc::new(crate::auth::AuthResolverChain::new(resolvers)))
351 };
352
353 Ok(Self {
354 identities: Arc::new(identities),
355 rate_bucket: Arc::new(RateBucket::default()),
356 auth_chain,
357 max_payload_bytes,
358 list_sessions_default_page_size,
359 list_sessions_max_page_size,
360 session_start_rate,
361 message_rate,
362 })
363 }
364
365 fn parse_identities(
366 json: &str,
367 ) -> Result<HashMap<String, AuthIdentity>, Box<dyn std::error::Error>> {
368 let parsed: RawConfig = serde_json::from_str(json)?;
369 let items = match parsed {
370 RawConfig::List(items) => items,
371 RawConfig::Wrapped { tokens } => tokens,
372 };
373 let mut identities = HashMap::new();
374 for item in items {
375 identities.insert(
376 item.token,
377 AuthIdentity {
378 sender: item.sender,
379 allowed_modes: if item.allowed_modes.is_empty() {
380 None
381 } else {
382 Some(item.allowed_modes.into_iter().collect())
383 },
384 can_start_sessions: item.can_start_sessions,
385 max_open_sessions: item.max_open_sessions,
386 can_manage_mode_registry: item.can_manage_mode_registry,
387 is_observer: item.is_observer,
388 },
389 );
390 }
391 Ok(identities)
392 }
393
394 fn bearer_token(metadata: &MetadataMap) -> Option<String> {
395 metadata
396 .get("authorization")
397 .and_then(|value| value.to_str().ok())
398 .and_then(|value| value.strip_prefix("Bearer "))
399 .map(str::to_string)
400 .or_else(|| {
401 metadata
402 .get("x-macp-token")
403 .and_then(|value| value.to_str().ok())
404 .map(str::to_string)
405 })
406 }
407
408 pub async fn authenticate_metadata(
409 &self,
410 metadata: &MetadataMap,
411 ) -> Result<AuthIdentity, MacpError> {
412 if let Some(chain) = &self.auth_chain {
416 return chain.authenticate(metadata).await;
417 }
418
419 if !self.identities.is_empty() {
421 if let Some(token) = Self::bearer_token(metadata) {
422 return self
423 .identities
424 .get(&token)
425 .cloned()
426 .ok_or(MacpError::Unauthenticated);
427 }
428 return Err(MacpError::Unauthenticated);
429 }
430
431 self.dev_authenticate(metadata)
433 }
434
435 pub fn authorize_mode(
436 &self,
437 identity: &AuthIdentity,
438 mode: &str,
439 is_session_start: bool,
440 ) -> Result<(), MacpError> {
441 if is_session_start && !identity.can_start_sessions {
442 return Err(MacpError::Forbidden);
443 }
444 if let Some(allowed_modes) = &identity.allowed_modes {
445 if !allowed_modes.contains(mode) {
446 return Err(MacpError::Forbidden);
447 }
448 }
449 Ok(())
450 }
451
452 pub fn authorize_mode_registry(&self, identity: &AuthIdentity) -> Result<(), MacpError> {
453 if identity.can_manage_mode_registry {
454 Ok(())
455 } else {
456 Err(MacpError::Forbidden)
457 }
458 }
459
460 async fn check_bucket(
461 bucket: &Mutex<HashMap<String, VecDeque<Instant>>>,
462 sweep_counter: &std::sync::atomic::AtomicU64,
463 sender: &str,
464 config: &RateLimitConfig,
465 ) -> Result<(), MacpError> {
466 let now = Instant::now();
467 let mut guard = bucket.lock().await;
468
469 const SWEEP_EVERY: u64 = 128;
477 let tick = sweep_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
478 if tick.is_multiple_of(SWEEP_EVERY) {
479 guard.retain(|_, deque| {
480 deque
481 .back()
482 .map(|last| now.duration_since(*last) <= config.window)
483 .unwrap_or(false)
484 });
485 }
486
487 let deque = guard.entry(sender.to_string()).or_default();
488 while deque
489 .front()
490 .map(|instant| now.duration_since(*instant) > config.window)
491 .unwrap_or(false)
492 {
493 deque.pop_front();
494 }
495 if deque.len() >= config.limit {
496 return Err(MacpError::RateLimited);
497 }
498 deque.push_back(now);
499 Ok(())
500 }
501
502 pub fn has_configured_auth(&self) -> bool {
506 self.auth_chain.is_some()
507 }
508
509 pub async fn enforce_rate_limit(
510 &self,
511 sender: &str,
512 is_session_start: bool,
513 ) -> Result<(), MacpError> {
514 if is_session_start {
515 Self::check_bucket(
516 &self.rate_bucket.start_events,
517 &self.rate_bucket.start_sweep_counter,
518 sender,
519 &self.session_start_rate,
520 )
521 .await
522 } else {
523 Self::check_bucket(
524 &self.rate_bucket.message_events,
525 &self.rate_bucket.message_sweep_counter,
526 sender,
527 &self.message_rate,
528 )
529 .await
530 }
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 use std::io::Write;
538 use tempfile::NamedTempFile;
539 use tonic::metadata::MetadataMap;
540
541 fn layer_with_tokens(json: &str) -> SecurityLayer {
544 let identities = SecurityLayer::parse_identities(json).expect("valid JSON");
545 SecurityLayer {
546 identities: Arc::new(identities),
547 rate_bucket: Arc::new(RateBucket::default()),
548 auth_chain: None,
549 max_payload_bytes: 1_048_576,
550 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
551 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
552 session_start_rate: RateLimitConfig {
553 limit: usize::MAX,
554 window: Duration::from_secs(60),
555 },
556 message_rate: RateLimitConfig {
557 limit: usize::MAX,
558 window: Duration::from_secs(60),
559 },
560 }
561 }
562
563 fn insecure_layer() -> SecurityLayer {
565 SecurityLayer {
566 identities: Arc::new(HashMap::new()),
567 rate_bucket: Arc::new(RateBucket::default()),
568 auth_chain: None,
569 max_payload_bytes: 1_048_576,
570 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
571 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
572 session_start_rate: RateLimitConfig {
573 limit: usize::MAX,
574 window: Duration::from_secs(60),
575 },
576 message_rate: RateLimitConfig {
577 limit: usize::MAX,
578 window: Duration::from_secs(60),
579 },
580 }
581 }
582
583 #[tokio::test]
588 async fn dev_mode_requires_dev_header() {
589 let layer = SecurityLayer::dev_mode();
590 let meta = MetadataMap::new();
591 let err = layer.authenticate_metadata(&meta).await.unwrap_err();
592 assert!(matches!(err, MacpError::Unauthenticated));
593 }
594
595 #[tokio::test]
596 async fn dev_mode_rejects_dev_sender_header() {
597 let layer = SecurityLayer::dev_mode();
598 let mut meta = MetadataMap::new();
599 meta.insert("x-macp-agent-id", "agent://dev-bot".parse().unwrap());
600 let err = layer.authenticate_metadata(&meta).await.unwrap_err();
601 assert!(matches!(err, MacpError::Unauthenticated));
602 }
603
604 #[test]
605 fn dev_mode_has_unlimited_rate_limits() {
606 let layer = SecurityLayer::dev_mode();
607 assert_eq!(layer.session_start_rate.limit, usize::MAX);
608 assert_eq!(layer.message_rate.limit, usize::MAX);
609 }
610
611 #[test]
616 fn from_env_defaults_without_env_vars() {
617 let layer = insecure_layer();
619 assert_eq!(layer.max_payload_bytes, 1_048_576);
620 }
621
622 fn raw_page_sizes(default_raw: Option<&str>, max_raw: Option<&str>) -> RawPageSizeEnv {
637 RawPageSizeEnv {
638 default_raw: default_raw.map(str::to_owned),
639 max_raw: max_raw.map(str::to_owned),
640 }
641 }
642
643 #[test]
644 fn from_env_page_size_defaults_without_env_vars() {
645 assert_eq!(
647 SecurityLayer::resolve_list_sessions_page_sizes(RawPageSizeEnv {
648 default_raw: None,
649 max_raw: None,
650 }),
651 (DEFAULT_LIST_SESSIONS_PAGE_SIZE, MAX_LIST_SESSIONS_PAGE_SIZE)
652 );
653 assert_eq!(DEFAULT_LIST_SESSIONS_PAGE_SIZE, 100);
654 assert_eq!(MAX_LIST_SESSIONS_PAGE_SIZE, 1000);
655
656 if [
663 "MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE",
664 "MACP_LIST_SESSIONS_MAX_PAGE_SIZE",
665 "MACP_AUTH_TOKENS_FILE",
666 "MACP_AUTH_TOKENS_JSON",
667 "MACP_AUTH_ISSUER",
668 ]
669 .iter()
670 .all(|v| std::env::var_os(v).is_none())
671 {
672 let layer = SecurityLayer::from_env().expect("from_env with no auth configured");
673 assert_eq!(layer.list_sessions_default_page_size, 100);
674 assert_eq!(layer.list_sessions_max_page_size, 1000);
675 }
676 }
677
678 #[test]
679 fn dev_mode_uses_production_page_size_defaults() {
680 let layer = SecurityLayer::dev_mode();
681 assert_eq!(layer.list_sessions_default_page_size, 100);
682 assert_eq!(layer.list_sessions_max_page_size, 1000);
683 assert_ne!(layer.list_sessions_default_page_size, usize::MAX);
692 assert_ne!(layer.list_sessions_max_page_size, usize::MAX);
693 }
694
695 #[test]
696 fn from_env_clamps_default_page_size_to_max() {
697 assert_eq!(
712 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(Some("5000"), None)),
713 (1000, 1000)
714 );
715 assert_eq!(
717 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(
718 Some("2000"),
719 Some("50")
720 )),
721 (50, 50)
722 );
723 assert_eq!(
725 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(
726 Some("50"),
727 Some("200")
728 )),
729 (50, 200)
730 );
731 assert_eq!(
732 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(
733 Some("200"),
734 Some("200")
735 )),
736 (200, 200)
737 );
738 }
739
740 #[test]
749 fn clamps_builtin_default_against_a_smaller_explicit_max() {
750 assert_eq!(
751 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(None, Some("50"))),
752 (50, 50),
753 "the built-in default must clamp to a smaller explicit max, not stay above it"
754 );
755
756 let builtin = SecurityLayer::clamp_warning_message(false);
761 let explicit = SecurityLayer::clamp_warning_message(true);
762 assert!(
763 !builtin.contains("MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE"),
764 "the built-in-default warning must not name an unset variable: {builtin}"
765 );
766 assert!(
767 builtin.contains("built-in") && builtin.contains("MACP_LIST_SESSIONS_MAX_PAGE_SIZE"),
768 "the built-in-default warning must say where the default came from and name the max: {builtin}"
769 );
770 assert!(
771 explicit.contains("MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE"),
772 "an explicitly configured default must still be named: {explicit}"
773 );
774 assert_eq!(SecurityLayer::page_size_default_source(false), "built-in");
775 assert_eq!(
776 SecurityLayer::page_size_default_source(true),
777 "MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE"
778 );
779 }
780
781 #[test]
782 fn page_size_resolver_treats_zero_like_garbage() {
783 assert_eq!(
788 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(Some("abc"), Some(""))),
789 (100, 1000)
790 );
791 assert_eq!(
792 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(
793 Some("-1"),
794 Some("1e3")
795 )),
796 (100, 1000)
797 );
798
799 assert_eq!(
804 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(Some("0"), None)),
805 (100, 1000),
806 "a default of 0 falls back to the compiled-in default, not to 1"
807 );
808 assert_eq!(
809 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(None, Some("0"))),
810 (100, 1000),
811 "a max of 0 falls back to the compiled-in max and leaves the default alone"
812 );
813 assert_eq!(
814 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(Some("0"), Some("0"))),
815 (100, 1000)
816 );
817 assert_eq!(
819 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(Some("0"), Some("25"))),
820 (25, 25),
821 "the good max is honored, and the fallback default clamps to it"
822 );
823 assert_eq!(
824 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(Some("7"), Some("0"))),
825 (7, 1000),
826 "the good default is honored against the compiled-in max"
827 );
828
829 for (default_raw, max_raw) in [
831 (Some("0"), None),
832 (None, Some("0")),
833 (Some("0"), Some("0")),
834 (Some("abc"), Some("0")),
835 ] {
836 let (default, max) = SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(
837 default_raw,
838 max_raw,
839 ));
840 assert!(
841 default > 0 && max > 0,
842 "{default_raw:?}/{max_raw:?} -> ({default}, {max})"
843 );
844 }
845
846 assert_eq!(
848 SecurityLayer::resolve_list_sessions_page_sizes(raw_page_sizes(None, Some("100000"))),
849 (100, 100_000)
850 );
851 }
852
853 #[tokio::test]
858 async fn bearer_token_authentication_via_authorization_header() {
859 let json = r#"[{"token":"tok-abc","sender":"agent://alice","allowed_modes":[],"can_start_sessions":true}]"#;
860 let layer = layer_with_tokens(json);
861
862 let mut meta = MetadataMap::new();
863 meta.insert("authorization", "Bearer tok-abc".parse().unwrap());
864
865 let id = layer
866 .authenticate_metadata(&meta)
867 .await
868 .expect("should authenticate");
869 assert_eq!(id.sender, "agent://alice");
870 assert!(id.allowed_modes.is_none()); assert!(id.can_start_sessions);
872 }
873
874 #[tokio::test]
875 async fn bearer_token_authentication_via_x_macp_token_header() {
876 let json = r#"[{"token":"tok-xyz","sender":"agent://bob"}]"#;
877 let layer = layer_with_tokens(json);
878
879 let mut meta = MetadataMap::new();
880 meta.insert("x-macp-token", "tok-xyz".parse().unwrap());
881
882 let id = layer
883 .authenticate_metadata(&meta)
884 .await
885 .expect("should authenticate");
886 assert_eq!(id.sender, "agent://bob");
887 }
888
889 #[tokio::test]
890 async fn invalid_bearer_token_returns_unauthenticated() {
891 let json = r#"[{"token":"tok-real","sender":"agent://alice"}]"#;
892 let layer = layer_with_tokens(json);
893
894 let mut meta = MetadataMap::new();
895 meta.insert("authorization", "Bearer tok-fake".parse().unwrap());
896
897 let err = layer.authenticate_metadata(&meta).await.unwrap_err();
898 assert!(matches!(err, MacpError::Unauthenticated));
899 }
900
901 #[tokio::test]
902 async fn no_token_when_auth_required_returns_unauthenticated() {
903 let json = r#"[{"token":"tok-only","sender":"agent://sole"}]"#;
904 let layer = layer_with_tokens(json);
905
906 let meta = MetadataMap::new(); let err = layer.authenticate_metadata(&meta).await.unwrap_err();
908 assert!(matches!(err, MacpError::Unauthenticated));
909 }
910
911 #[tokio::test]
912 async fn parse_identities_wrapped_format() {
913 let json = r#"{"tokens":[{"token":"t1","sender":"agent://wrapped"}]}"#;
914 let layer = layer_with_tokens(json);
915
916 let mut meta = MetadataMap::new();
917 meta.insert("authorization", "Bearer t1".parse().unwrap());
918 let id = layer
919 .authenticate_metadata(&meta)
920 .await
921 .expect("should authenticate");
922 assert_eq!(id.sender, "agent://wrapped");
923 }
924
925 #[tokio::test]
926 async fn parse_identities_with_allowed_modes() {
927 let json = r#"[{"token":"t-modes","sender":"agent://limited","allowed_modes":["macp.mode.decision.v1","macp.mode.task.v1"],"can_start_sessions":false,"max_open_sessions":5}]"#;
928 let layer = layer_with_tokens(json);
929
930 let mut meta = MetadataMap::new();
931 meta.insert("authorization", "Bearer t-modes".parse().unwrap());
932 let id = layer
933 .authenticate_metadata(&meta)
934 .await
935 .expect("should authenticate");
936
937 assert_eq!(id.sender, "agent://limited");
938 assert!(!id.can_start_sessions);
939 assert_eq!(id.max_open_sessions, Some(5));
940 let modes = id
941 .allowed_modes
942 .as_ref()
943 .expect("should have allowed_modes");
944 assert!(modes.contains("macp.mode.decision.v1"));
945 assert!(modes.contains("macp.mode.task.v1"));
946 assert!(!modes.contains("macp.mode.proposal.v1"));
947 }
948
949 #[tokio::test]
950 async fn authorization_header_takes_priority_over_x_macp_token() {
951 let json = r#"[
952 {"token":"bearer-tok","sender":"agent://bearer-user"},
953 {"token":"header-tok","sender":"agent://header-user"}
954 ]"#;
955 let layer = layer_with_tokens(json);
956
957 let mut meta = MetadataMap::new();
958 meta.insert("authorization", "Bearer bearer-tok".parse().unwrap());
959 meta.insert("x-macp-token", "header-tok".parse().unwrap());
960
961 let id = layer
962 .authenticate_metadata(&meta)
963 .await
964 .expect("should authenticate");
965 assert_eq!(id.sender, "agent://bearer-user");
967 }
968
969 #[tokio::test]
974 async fn dev_sender_header_rejected_without_chain() {
975 let layer = SecurityLayer {
976 identities: Arc::new(HashMap::new()),
977 rate_bucket: Arc::new(RateBucket::default()),
978 auth_chain: None,
979 max_payload_bytes: 1_048_576,
980 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
981 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
982 session_start_rate: RateLimitConfig {
983 limit: usize::MAX,
984 window: Duration::from_secs(60),
985 },
986 message_rate: RateLimitConfig {
987 limit: usize::MAX,
988 window: Duration::from_secs(60),
989 },
990 };
991
992 let mut meta = MetadataMap::new();
993 meta.insert("x-macp-agent-id", "agent://dev-agent".parse().unwrap());
994
995 let err = layer.authenticate_metadata(&meta).await.unwrap_err();
996 assert!(matches!(err, MacpError::Unauthenticated));
997 }
998
999 #[tokio::test]
1000 async fn dev_sender_header_ignored_when_not_allowed() {
1001 let layer = SecurityLayer {
1003 identities: Arc::new(HashMap::new()),
1004 rate_bucket: Arc::new(RateBucket::default()),
1005 auth_chain: None,
1006 max_payload_bytes: 1_048_576,
1007 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
1008 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
1009 session_start_rate: RateLimitConfig {
1010 limit: usize::MAX,
1011 window: Duration::from_secs(60),
1012 },
1013 message_rate: RateLimitConfig {
1014 limit: usize::MAX,
1015 window: Duration::from_secs(60),
1016 },
1017 };
1018
1019 let mut meta = MetadataMap::new();
1020 meta.insert("x-macp-agent-id", "agent://sneaky".parse().unwrap());
1021
1022 let err = layer.authenticate_metadata(&meta).await.unwrap_err();
1023 assert!(matches!(err, MacpError::Unauthenticated));
1024 }
1025
1026 #[tokio::test]
1027 async fn bearer_token_takes_priority_over_dev_header() {
1028 let json = r#"[{"token":"real-tok","sender":"agent://real"}]"#;
1029 let identities = SecurityLayer::parse_identities(json).unwrap();
1030
1031 let layer = SecurityLayer {
1032 identities: Arc::new(identities),
1033 rate_bucket: Arc::new(RateBucket::default()),
1034 auth_chain: None,
1035 max_payload_bytes: 1_048_576,
1036 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
1037 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
1038 session_start_rate: RateLimitConfig {
1039 limit: usize::MAX,
1040 window: Duration::from_secs(60),
1041 },
1042 message_rate: RateLimitConfig {
1043 limit: usize::MAX,
1044 window: Duration::from_secs(60),
1045 },
1046 };
1047
1048 let mut meta = MetadataMap::new();
1049 meta.insert("authorization", "Bearer real-tok".parse().unwrap());
1050 meta.insert("x-macp-agent-id", "agent://dev-override".parse().unwrap());
1051
1052 let id = layer
1053 .authenticate_metadata(&meta)
1054 .await
1055 .expect("should authenticate via bearer");
1056 assert_eq!(id.sender, "agent://real");
1057 }
1058
1059 #[test]
1064 fn authorize_mode_allows_any_mode_when_no_restriction() {
1065 let layer = SecurityLayer::dev_mode();
1066 let id = AuthIdentity {
1067 sender: "agent://any".into(),
1068 allowed_modes: None,
1069 can_start_sessions: true,
1070 max_open_sessions: None,
1071 can_manage_mode_registry: false,
1072 is_observer: false,
1073 };
1074 assert!(layer
1075 .authorize_mode(&id, "macp.mode.decision.v1", false)
1076 .is_ok());
1077 assert!(layer.authorize_mode(&id, "macp.mode.task.v1", true).is_ok());
1078 assert!(layer.authorize_mode(&id, "arbitrary.mode", false).is_ok());
1079 }
1080
1081 #[test]
1082 fn authorize_mode_rejects_unlisted_mode() {
1083 let layer = SecurityLayer::dev_mode();
1084 let mut allowed = HashSet::new();
1085 allowed.insert("macp.mode.decision.v1".to_string());
1086
1087 let id = AuthIdentity {
1088 sender: "agent://restricted".into(),
1089 allowed_modes: Some(allowed),
1090 can_start_sessions: true,
1091 max_open_sessions: None,
1092 can_manage_mode_registry: false,
1093 is_observer: false,
1094 };
1095 assert!(layer
1096 .authorize_mode(&id, "macp.mode.decision.v1", false)
1097 .is_ok());
1098 let err = layer
1099 .authorize_mode(&id, "macp.mode.task.v1", false)
1100 .unwrap_err();
1101 assert!(matches!(err, MacpError::Forbidden));
1102 }
1103
1104 #[test]
1105 fn authorize_mode_rejects_session_start_when_not_allowed() {
1106 let layer = SecurityLayer::dev_mode();
1107 let id = AuthIdentity {
1108 sender: "agent://no-start".into(),
1109 allowed_modes: None,
1110 can_start_sessions: false,
1111 max_open_sessions: None,
1112 can_manage_mode_registry: false,
1113 is_observer: false,
1114 };
1115 let err = layer
1116 .authorize_mode(&id, "macp.mode.decision.v1", true)
1117 .unwrap_err();
1118 assert!(matches!(err, MacpError::Forbidden));
1119 }
1120
1121 #[test]
1122 fn authorize_mode_allows_non_session_start_even_when_start_forbidden() {
1123 let layer = SecurityLayer::dev_mode();
1124 let id = AuthIdentity {
1125 sender: "agent://no-start".into(),
1126 allowed_modes: None,
1127 can_start_sessions: false,
1128 max_open_sessions: None,
1129 can_manage_mode_registry: false,
1130 is_observer: false,
1131 };
1132 assert!(layer
1134 .authorize_mode(&id, "macp.mode.decision.v1", false)
1135 .is_ok());
1136 }
1137
1138 #[test]
1139 fn authorize_mode_checks_both_can_start_and_allowed_modes() {
1140 let layer = SecurityLayer::dev_mode();
1141 let mut allowed = HashSet::new();
1142 allowed.insert("macp.mode.decision.v1".to_string());
1143
1144 let id = AuthIdentity {
1145 sender: "agent://double-check".into(),
1146 allowed_modes: Some(allowed),
1147 can_start_sessions: false,
1148 max_open_sessions: None,
1149 can_manage_mode_registry: false,
1150 is_observer: false,
1151 };
1152
1153 let err = layer
1155 .authorize_mode(&id, "macp.mode.decision.v1", true)
1156 .unwrap_err();
1157 assert!(matches!(err, MacpError::Forbidden));
1158
1159 let err = layer
1161 .authorize_mode(&id, "macp.mode.task.v1", false)
1162 .unwrap_err();
1163 assert!(matches!(err, MacpError::Forbidden));
1164
1165 assert!(layer
1167 .authorize_mode(&id, "macp.mode.decision.v1", false)
1168 .is_ok());
1169 }
1170
1171 #[test]
1172 fn authorize_mode_registry_requires_explicit_privilege() {
1173 let layer = SecurityLayer::dev_mode();
1174 let id = AuthIdentity {
1175 sender: "agent://no-admin".into(),
1176 allowed_modes: None,
1177 can_start_sessions: true,
1178 max_open_sessions: None,
1179 is_observer: false,
1180 can_manage_mode_registry: false,
1181 };
1182 let err = layer.authorize_mode_registry(&id).unwrap_err();
1183 assert!(matches!(err, MacpError::Forbidden));
1184 }
1185
1186 #[tokio::test]
1187 async fn bearer_token_can_manage_mode_registry() {
1188 let json =
1189 r#"[{"token":"admin-tok","sender":"agent://admin","can_manage_mode_registry":true}]"#;
1190 let layer = layer_with_tokens(json);
1191 let mut meta = MetadataMap::new();
1192 meta.insert("authorization", "Bearer admin-tok".parse().unwrap());
1193 let id = layer.authenticate_metadata(&meta).await.unwrap();
1194 assert!(layer.authorize_mode_registry(&id).is_ok());
1195 }
1196
1197 #[tokio::test]
1202 async fn rate_limit_session_start_enforced() {
1203 let layer = SecurityLayer {
1204 identities: Arc::new(HashMap::new()),
1205 rate_bucket: Arc::new(RateBucket::default()),
1206 auth_chain: None,
1207 max_payload_bytes: 1_048_576,
1208 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
1209 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
1210 session_start_rate: RateLimitConfig {
1211 limit: 3,
1212 window: Duration::from_secs(60),
1213 },
1214 message_rate: RateLimitConfig {
1215 limit: usize::MAX,
1216 window: Duration::from_secs(60),
1217 },
1218 };
1219
1220 let sender = "agent://rate-test";
1221 for _ in 0..3 {
1223 assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
1224 }
1225 let err = layer.enforce_rate_limit(sender, true).await.unwrap_err();
1227 assert!(matches!(err, MacpError::RateLimited));
1228
1229 assert!(layer.enforce_rate_limit(sender, false).await.is_ok());
1231 }
1232
1233 #[tokio::test]
1234 async fn rate_limit_message_enforced() {
1235 let layer = SecurityLayer {
1236 identities: Arc::new(HashMap::new()),
1237 rate_bucket: Arc::new(RateBucket::default()),
1238 auth_chain: None,
1239 max_payload_bytes: 1_048_576,
1240 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
1241 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
1242 session_start_rate: RateLimitConfig {
1243 limit: usize::MAX,
1244 window: Duration::from_secs(60),
1245 },
1246 message_rate: RateLimitConfig {
1247 limit: 2,
1248 window: Duration::from_secs(60),
1249 },
1250 };
1251
1252 let sender = "agent://msg-test";
1253 assert!(layer.enforce_rate_limit(sender, false).await.is_ok());
1254 assert!(layer.enforce_rate_limit(sender, false).await.is_ok());
1255 let err = layer.enforce_rate_limit(sender, false).await.unwrap_err();
1256 assert!(matches!(err, MacpError::RateLimited));
1257
1258 assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
1260 }
1261
1262 #[tokio::test]
1263 async fn rate_limit_per_sender_isolation() {
1264 let layer = SecurityLayer {
1265 identities: Arc::new(HashMap::new()),
1266 rate_bucket: Arc::new(RateBucket::default()),
1267 auth_chain: None,
1268 max_payload_bytes: 1_048_576,
1269 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
1270 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
1271 session_start_rate: RateLimitConfig {
1272 limit: 1,
1273 window: Duration::from_secs(60),
1274 },
1275 message_rate: RateLimitConfig {
1276 limit: usize::MAX,
1277 window: Duration::from_secs(60),
1278 },
1279 };
1280
1281 assert!(layer.enforce_rate_limit("agent://a", true).await.is_ok());
1283 assert!(layer.enforce_rate_limit("agent://a", true).await.is_err());
1284
1285 assert!(layer.enforce_rate_limit("agent://b", true).await.is_ok());
1287 }
1288
1289 #[tokio::test]
1290 async fn rate_limit_window_expiry() {
1291 let layer = SecurityLayer {
1292 identities: Arc::new(HashMap::new()),
1293 rate_bucket: Arc::new(RateBucket::default()),
1294 auth_chain: None,
1295 max_payload_bytes: 1_048_576,
1296 list_sessions_default_page_size: DEFAULT_LIST_SESSIONS_PAGE_SIZE,
1297 list_sessions_max_page_size: MAX_LIST_SESSIONS_PAGE_SIZE,
1298 session_start_rate: RateLimitConfig {
1299 limit: 1,
1300 window: Duration::from_millis(1), },
1302 message_rate: RateLimitConfig {
1303 limit: usize::MAX,
1304 window: Duration::from_secs(60),
1305 },
1306 };
1307
1308 let sender = "agent://expiry-test";
1309 assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
1310
1311 tokio::time::sleep(Duration::from_millis(5)).await;
1313
1314 assert!(layer.enforce_rate_limit(sender, true).await.is_ok());
1316 }
1317
1318 #[tokio::test]
1323 async fn no_anonymous_fallback_even_when_auth_not_required() {
1324 let layer = insecure_layer();
1325 let meta = MetadataMap::new();
1326 let err = layer.authenticate_metadata(&meta).await.unwrap_err();
1327 assert!(matches!(err, MacpError::Unauthenticated));
1328 }
1329
1330 #[tokio::test]
1331 async fn no_anonymous_fallback_when_auth_required() {
1332 let json = r#"[{"token":"t","sender":"agent://real"}]"#;
1333 let layer = layer_with_tokens(json);
1334
1335 let meta = MetadataMap::new();
1336 let err = layer.authenticate_metadata(&meta).await.unwrap_err();
1337 assert!(matches!(err, MacpError::Unauthenticated));
1338 }
1339
1340 #[tokio::test]
1341 async fn dev_mode_no_fallback_with_empty_metadata() {
1342 let layer = SecurityLayer::dev_mode();
1345 let meta = MetadataMap::new();
1346 let err = layer.authenticate_metadata(&meta).await.unwrap_err();
1347 assert!(matches!(err, MacpError::Unauthenticated));
1348 }
1349
1350 #[test]
1355 fn token_file_loading_via_parse_identities() {
1356 let json = r#"[
1359 {"token":"file-tok-1","sender":"agent://file-alice","allowed_modes":["macp.mode.decision.v1"]},
1360 {"token":"file-tok-2","sender":"agent://file-bob","can_start_sessions":false}
1361 ]"#;
1362 let mut tmp = NamedTempFile::new().expect("create temp file");
1363 write!(tmp, "{}", json).expect("write temp file");
1364
1365 let contents = fs::read_to_string(tmp.path()).expect("read temp file");
1366 let identities = SecurityLayer::parse_identities(&contents).expect("parse identities");
1367
1368 assert_eq!(identities.len(), 2);
1369
1370 let alice = identities.get("file-tok-1").expect("alice entry");
1371 assert_eq!(alice.sender, "agent://file-alice");
1372 let alice_modes = alice.allowed_modes.as_ref().expect("should have modes");
1373 assert!(alice_modes.contains("macp.mode.decision.v1"));
1374 assert!(alice.can_start_sessions); let bob = identities.get("file-tok-2").expect("bob entry");
1377 assert_eq!(bob.sender, "agent://file-bob");
1378 assert!(!bob.can_start_sessions);
1379 assert!(bob.allowed_modes.is_none()); }
1381
1382 #[tokio::test]
1383 async fn token_file_end_to_end_via_layer() {
1384 let json = r#"[{"token":"e2e-tok","sender":"agent://e2e-agent"}]"#;
1386 let mut tmp = NamedTempFile::new().expect("create temp file");
1387 write!(tmp, "{}", json).expect("write temp file");
1388
1389 let contents = fs::read_to_string(tmp.path()).expect("read temp file");
1390 let layer = layer_with_tokens(&contents);
1391
1392 let mut meta = MetadataMap::new();
1393 meta.insert("authorization", "Bearer e2e-tok".parse().unwrap());
1394 let id = layer
1395 .authenticate_metadata(&meta)
1396 .await
1397 .expect("should authenticate");
1398 assert_eq!(id.sender, "agent://e2e-agent");
1399 }
1400
1401 #[test]
1402 fn parse_identities_invalid_json_returns_error() {
1403 let result = SecurityLayer::parse_identities("not valid json");
1404 assert!(result.is_err());
1405 }
1406
1407 #[test]
1408 fn parse_identities_empty_list() {
1409 let identities = SecurityLayer::parse_identities("[]").expect("valid empty list");
1410 assert!(identities.is_empty());
1411 }
1412
1413 #[test]
1414 fn parse_identities_wrapped_empty() {
1415 let identities =
1416 SecurityLayer::parse_identities(r#"{"tokens":[]}"#).expect("valid wrapped empty");
1417 assert!(identities.is_empty());
1418 }
1419
1420 #[tokio::test]
1424 async fn rate_bucket_sweep_removes_stale_senders() {
1425 let bucket = RateBucket::default();
1426 let config = RateLimitConfig {
1427 limit: 10,
1428 window: Duration::from_millis(1),
1429 };
1430
1431 for i in 0..50 {
1433 SecurityLayer::check_bucket(
1434 &bucket.start_events,
1435 &bucket.start_sweep_counter,
1436 &format!("agent://stale-{i}"),
1437 &config,
1438 )
1439 .await
1440 .unwrap();
1441 }
1442 assert!(bucket.start_events.lock().await.len() >= 49);
1443
1444 tokio::time::sleep(Duration::from_millis(5)).await;
1446
1447 for _ in 0..80 {
1450 SecurityLayer::check_bucket(
1451 &bucket.start_events,
1452 &bucket.start_sweep_counter,
1453 "agent://fresh",
1454 &config,
1455 )
1456 .await
1457 .ok(); }
1459 let map = bucket.start_events.lock().await;
1460 assert!(
1461 map.len() <= 2,
1462 "stale senders must be swept; map still has {} entries",
1463 map.len()
1464 );
1465 assert!(map.contains_key("agent://fresh"));
1466 }
1467}