1use crate::error::MacpError;
2use crate::pb::macp_runtime_service_server::MacpRuntimeService;
3use crate::pb::{
4 session_lifecycle_event, Ack, CancelSessionRequest, CancelSessionResponse,
5 CancellationCapability, Capabilities, Envelope, GetManifestRequest, GetManifestResponse,
6 GetPolicyRequest, GetPolicyResponse, GetSessionRequest, GetSessionResponse, InitializeRequest,
7 InitializeResponse, ListExtModesRequest, ListExtModesResponse, ListModesRequest,
8 ListModesResponse, ListPoliciesRequest, ListPoliciesResponse, ListRootsRequest,
9 ListRootsResponse, ListSessionsRequest, ListSessionsResponse, MacpError as PbMacpError,
10 ManifestCapability, ModeRegistryCapability, ParticipantActivity, PolicyDescriptor,
11 PolicyRegistryCapability, ProgressCapability, PromoteModeRequest, PromoteModeResponse,
12 RegisterExtModeRequest, RegisterExtModeResponse, RegisterPolicyRequest, RegisterPolicyResponse,
13 ResumeSessionRequest, ResumeSessionResponse, RootsCapability, RuntimeInfo, SendRequest,
14 SendResponse, SessionLifecycleEvent, SessionMetadata, SessionState as PbSessionState,
15 SessionsCapability, StreamSessionRequest, StreamSessionResponse, SuspendSessionRequest,
16 SuspendSessionResponse, UnregisterExtModeRequest, UnregisterExtModeResponse,
17 UnregisterPolicyRequest, UnregisterPolicyResponse, WatchModeRegistryRequest,
18 WatchModeRegistryResponse, WatchPoliciesRequest, WatchPoliciesResponse, WatchRootsRequest,
19 WatchRootsResponse, WatchSessionsRequest, WatchSessionsResponse, WatchSignalsRequest,
20 WatchSignalsResponse,
21};
22use crate::runtime::Runtime;
23use crate::security::{AuthIdentity, SecurityLayer};
24use crate::session::SessionState;
25use std::collections::HashMap;
26use std::sync::Arc;
27use tonic::{Request, Response, Status};
28
29type SessionResponseStream = std::pin::Pin<
30 Box<dyn futures_core::Stream<Item = Result<StreamSessionResponse, Status>> + Send>,
31>;
32
33#[derive(Clone)]
34pub struct MacpServer {
35 runtime: Arc<Runtime>,
36 security: SecurityLayer,
37 policies_read_only: bool,
42 policy_engine: Option<Arc<dyn crate::policy_engine::PolicyEngine>>,
46}
47
48impl MacpServer {
49 pub fn new(runtime: Arc<Runtime>, security: SecurityLayer) -> Self {
50 Self {
51 runtime,
52 security,
53 policies_read_only: false,
54 policy_engine: None,
55 }
56 }
57
58 pub fn with_read_only_policies(mut self) -> Self {
59 self.policies_read_only = true;
60 self
61 }
62
63 pub fn with_policy_engine(
67 mut self,
68 engine: Arc<dyn crate::policy_engine::PolicyEngine>,
69 ) -> Self {
70 self.policy_engine = Some(engine);
71 self
72 }
73
74 async fn enforce_ingress_policy(
78 &self,
79 identity: &crate::security::AuthIdentity,
80 env: &Envelope,
81 ) -> Result<(), MacpError> {
82 let Some(engine) = &self.policy_engine else {
83 return Ok(());
84 };
85 let decision = if env.message_type == "SessionStart" {
86 engine
87 .evaluate_session_start(identity, &env.mode, env)
88 .await
89 } else if !env.session_id.is_empty() {
90 match self.runtime.get_session_checked(&env.session_id).await {
94 Some(session) => engine.evaluate_message(identity, &session, env).await,
95 None => return Ok(()),
96 }
97 } else {
98 return Ok(());
99 };
100 match decision {
101 macp_core::policy::PolicyDecision::Allow { .. } => Ok(()),
102 macp_core::policy::PolicyDecision::Deny { reasons } => {
103 Err(MacpError::PolicyDenied { reasons })
104 }
105 other => {
106 tracing::warn!(decision = ?other, "unrecognized ingress policy decision");
107 Err(MacpError::PolicyDenied {
108 reasons: vec!["unrecognized policy decision".into()],
109 })
110 }
111 }
112 }
113
114 fn validate_envelope_shape(&self, env: &Envelope) -> Result<(), MacpError> {
115 if env.macp_version != macp_core::MACP_VERSION {
116 return Err(MacpError::InvalidMacpVersion);
117 }
118 if env.message_type.is_empty() || env.message_id.is_empty() {
119 return Err(MacpError::InvalidEnvelope);
120 }
121 let is_ambient_type = env.message_type == "Signal" || env.message_type == "Progress";
124 if env.message_type == "Signal" {
125 if !env.session_id.is_empty() {
126 return Err(MacpError::InvalidEnvelope);
127 }
128 if !env.mode.trim().is_empty() {
129 return Err(MacpError::InvalidEnvelope);
130 }
131 }
132 if env.message_type == "Progress" {
133 let session_id_empty = env.session_id.is_empty();
137 let mode_empty = env.mode.trim().is_empty();
138 if session_id_empty != mode_empty {
139 return Err(MacpError::InvalidEnvelope);
140 }
141 }
142 if !is_ambient_type && env.session_id.is_empty() {
143 return Err(MacpError::InvalidEnvelope);
144 }
145 if !is_ambient_type && env.mode.trim().is_empty() {
146 return Err(MacpError::InvalidEnvelope);
147 }
148 if env.payload.len() > self.security.max_payload_bytes {
149 return Err(MacpError::PayloadTooLarge);
150 }
151 Ok(())
152 }
153
154 fn session_state_to_pb(state: &SessionState) -> i32 {
155 match state {
156 SessionState::Open => PbSessionState::Open.into(),
157 SessionState::Suspended => PbSessionState::Suspended.into(),
158 SessionState::Resolved => PbSessionState::Resolved.into(),
159 SessionState::Expired => PbSessionState::Expired.into(),
160 SessionState::Cancelled => PbSessionState::Cancelled.into(),
161 }
162 }
163
164 fn session_to_metadata(session: &crate::session::Session) -> SessionMetadata {
165 let participant_activity = session
166 .participant_message_counts
167 .iter()
168 .map(|(pid, count)| ParticipantActivity {
169 participant_id: pid.clone(),
170 last_message_at_unix_ms: session
171 .participant_last_seen
172 .get(pid)
173 .copied()
174 .unwrap_or(0),
175 message_count: *count,
176 })
177 .collect();
178 SessionMetadata {
179 session_id: session.session_id.clone(),
180 mode: session.mode.clone(),
181 state: Self::session_state_to_pb(&session.state),
182 started_at_unix_ms: session.started_at_unix_ms,
183 expires_at_unix_ms: session.ttl_expiry,
184 mode_version: session.mode_version.clone(),
185 configuration_version: session.configuration_version.clone(),
186 policy_version: session.policy_version.clone(),
187 participants: session.participants.clone(),
188 participant_activity,
189 initiator: session.initiator_sender.clone(),
190 context_id: session.context_id.clone(),
191 extension_keys: session.extensions.keys().cloned().collect(),
192 }
193 }
194
195 fn make_error_ack(e: &MacpError, env: &Envelope) -> Ack {
196 let details = Self::error_details_bytes(e);
197 Ack {
198 ok: false,
199 duplicate: false,
200 message_id: env.message_id.clone(),
201 session_id: env.session_id.clone(),
202 accepted_at_unix_ms: chrono::Utc::now().timestamp_millis(),
203 session_state: PbSessionState::Unspecified.into(),
204 error: Some(PbMacpError {
205 code: e.error_code().into(),
206 message: e.to_string(),
207 session_id: env.session_id.clone(),
208 message_id: env.message_id.clone(),
209 details,
210 }),
211 }
212 }
213
214 fn error_details_bytes(e: &MacpError) -> Vec<u8> {
217 match e {
218 MacpError::PolicyDenied { reasons } => {
219 serde_json::to_vec(&serde_json::json!({ "reasons": reasons })).unwrap_or_default()
220 }
221 _ => vec![],
222 }
223 }
224
225 fn apply_authenticated_sender(
226 identity: &AuthIdentity,
227 mut env: Envelope,
228 ) -> Result<Envelope, MacpError> {
229 if !env.sender.is_empty() && env.sender != identity.sender {
230 return Err(MacpError::Unauthenticated);
231 }
232 env.sender = identity.sender.clone();
233 Ok(env)
234 }
235
236 async fn authenticate_send_request(
237 &self,
238 request: &Request<SendRequest>,
239 env: Envelope,
240 ) -> Result<(Envelope, Option<usize>), MacpError> {
241 let identity = self
242 .security
243 .authenticate_metadata(request.metadata())
244 .await?;
245 let env = Self::apply_authenticated_sender(&identity, env)?;
246 let is_session_start = env.message_type == "SessionStart";
247 self.security
248 .authorize_mode(&identity, &env.mode, is_session_start)?;
249 self.security
250 .enforce_rate_limit(&identity.sender, is_session_start)
251 .await?;
252 self.enforce_ingress_policy(&identity, &env).await?;
255 let max_open = if is_session_start {
256 identity.max_open_sessions
257 } else {
258 None
259 };
260 Ok((env, max_open))
261 }
262
263 async fn authenticate_session_access<T>(
264 &self,
265 request: &Request<T>,
266 session_id: &str,
267 ) -> Result<AuthIdentity, Status> {
268 let identity = self
269 .security
270 .authenticate_metadata(request.metadata())
271 .await
272 .map_err(Self::status_from_error)?;
273 let session = self
274 .runtime
275 .get_session_checked(session_id)
276 .await
277 .ok_or_else(|| Status::not_found(format!("Session '{}' not found", session_id)))?;
278 let allowed = identity.is_observer
279 || session.initiator_sender == identity.sender
280 || session.participants.iter().any(|p| p == &identity.sender);
281 if !allowed {
282 return Err(Status::permission_denied(
283 "FORBIDDEN: session access denied",
284 ));
285 }
286 if let Some(engine) = &self.policy_engine {
289 let decision = engine.evaluate_session_access(&identity, &session).await;
290 crate::policy_engine::require_allow(decision, "session access")?;
291 }
292 Ok(identity)
293 }
294
295 fn should_skip_replayed(
302 replay_dedup: &mut Option<std::collections::HashSet<String>>,
303 envelope: &Envelope,
304 ) -> bool {
305 if let Some(seen) = replay_dedup.as_mut() {
306 if seen.remove(&envelope.message_id) {
307 return true;
308 }
309 *replay_dedup = None;
310 }
311 false
312 }
313
314 fn try_next_stream_event(
315 receiver: &mut Option<tokio::sync::broadcast::Receiver<Envelope>>,
316 ) -> Result<Option<Envelope>, Status> {
317 use tokio::sync::broadcast::error::TryRecvError;
318
319 let rx = match receiver.as_mut() {
320 Some(rx) => rx,
321 None => return Ok(None),
322 };
323
324 match rx.try_recv() {
325 Ok(envelope) => Ok(Some(envelope)),
326 Err(TryRecvError::Empty) => Ok(None),
327 Err(TryRecvError::Closed) => {
328 *receiver = None;
329 Ok(None)
330 }
331 Err(TryRecvError::Lagged(skipped)) => {
332 tracing::warn!(
335 skipped,
336 "StreamSession receiver fell behind; terminating stream"
337 );
338 Err(Status::resource_exhausted(format!(
339 "StreamSession receiver fell behind by {skipped} envelopes"
340 )))
341 }
342 }
343 }
344
345 async fn process_stream_request(
350 &self,
351 identity: &AuthIdentity,
352 req: StreamSessionRequest,
353 bound_session_id: &mut Option<String>,
354 session_events: &mut Option<tokio::sync::broadcast::Receiver<Envelope>>,
355 ) -> Result<Vec<Envelope>, Status> {
356 if !req.subscribe_session_id.is_empty() {
360 if req.envelope.is_some() {
361 return Err(Status::invalid_argument(
362 "StreamSessionRequest must not contain both envelope and subscribe_session_id",
363 ));
364 }
365 return self
366 .process_subscribe_frame(
367 identity,
368 &req.subscribe_session_id,
369 req.after_sequence,
370 bound_session_id,
371 session_events,
372 )
373 .await;
374 }
375
376 let envelope = req.envelope.ok_or_else(|| {
377 Status::invalid_argument(
378 "StreamSessionRequest must contain an envelope or subscribe_session_id",
379 )
380 })?;
381
382 self.validate_envelope_shape(&envelope)
383 .map_err(Self::status_from_error)?;
384 if envelope.session_id.trim().is_empty() {
385 return Err(Status::invalid_argument(
386 "StreamSession requires a non-empty session_id",
387 ));
388 }
389 if envelope.mode.trim().is_empty() {
390 return Err(Status::invalid_argument(
391 "StreamSession requires a non-empty mode",
392 ));
393 }
394 if let Some(bound) = bound_session_id.as_ref() {
395 if bound != &envelope.session_id {
396 return Err(Status::invalid_argument(
397 "StreamSession may only carry envelopes for one session_id",
398 ));
399 }
400 }
401
402 let envelope = Self::apply_authenticated_sender(identity, envelope)
403 .map_err(Self::status_from_error)?;
404 let is_session_start = envelope.message_type == "SessionStart";
405
406 if !is_session_start {
407 if let Some(session) = self.runtime.get_session_checked(&envelope.session_id).await {
408 if envelope.mode != session.mode {
409 return Err(Status::invalid_argument(
410 "INVALID_ENVELOPE: envelope mode does not match the bound session mode",
411 ));
412 }
413 if session.state != SessionState::Open {
414 return Err(Status::invalid_argument("SESSION_NOT_OPEN"));
415 }
416 } else if envelope.message_type == "Signal" {
417 return Err(Status::not_found(format!(
418 "Session '{}' not found",
419 envelope.session_id
420 )));
421 }
422 }
423
424 self.security
425 .authorize_mode(identity, &envelope.mode, is_session_start)
426 .map_err(Self::status_from_error)?;
427 self.enforce_ingress_policy(identity, &envelope)
431 .await
432 .map_err(Self::status_from_error)?;
433 self.security
434 .enforce_rate_limit(&identity.sender, is_session_start)
435 .await
436 .map_err(Self::status_from_error)?;
437
438 if session_events.is_none() {
439 *bound_session_id = Some(envelope.session_id.clone());
440 *session_events = Some(self.runtime.subscribe_session_stream(&envelope.session_id));
441 }
442
443 let max_open = if is_session_start {
444 identity.max_open_sessions
445 } else {
446 None
447 };
448 self.runtime
449 .process(&envelope, max_open)
450 .await
451 .map_err(Self::status_from_error)?;
452 Ok(vec![])
453 }
454
455 async fn process_subscribe_frame(
459 &self,
460 identity: &AuthIdentity,
461 session_id: &str,
462 after_sequence: u64,
463 bound_session_id: &mut Option<String>,
464 session_events: &mut Option<tokio::sync::broadcast::Receiver<Envelope>>,
465 ) -> Result<Vec<Envelope>, Status> {
466 if let Some(bound) = bound_session_id.as_ref() {
468 if bound != session_id {
469 return Err(Status::invalid_argument(
470 "StreamSession may only carry envelopes for one session_id",
471 ));
472 }
473 }
474
475 let session = self
477 .runtime
478 .get_session_checked(session_id)
479 .await
480 .ok_or_else(|| Status::not_found(format!("Session '{}' not found", session_id)))?;
481
482 let allowed = identity.is_observer
484 || session.initiator_sender == identity.sender
485 || session.participants.iter().any(|p| p == &identity.sender);
486 if !allowed {
487 return Err(Status::permission_denied(
488 "FORBIDDEN: caller is not a declared participant or observer for this session",
489 ));
490 }
491 if let Some(engine) = &self.policy_engine {
494 let decision = engine.evaluate_session_access(identity, &session).await;
495 crate::policy_engine::require_allow(decision, "session access")?;
496 }
497
498 if session_events.is_none() {
500 *bound_session_id = Some(session_id.to_string());
501 *session_events = Some(self.runtime.subscribe_session_stream(session_id));
502 }
503
504 tracing::info!(
505 session_id = %session_id,
506 sender = %identity.sender,
507 after_sequence = after_sequence,
508 "passive subscribe: replaying session history"
509 );
510
511 let replay = self
513 .runtime
514 .get_session_envelopes_after(session_id, after_sequence)
515 .await
516 .map_err(|base| {
517 Status::failed_precondition(format!(
518 "session history before ordinal {base} was compacted; \
519 resume with after_sequence >= {base} or re-read state via GetSession"
520 ))
521 })?;
522
523 Ok(replay)
524 }
525
526 fn build_stream_session_stream<S>(
527 &self,
528 identity: AuthIdentity,
529 inbound: S,
530 ) -> SessionResponseStream
531 where
532 S: futures_core::Stream<Item = Result<StreamSessionRequest, Status>> + Send + 'static,
533 {
534 use tokio::sync::broadcast;
535 use tokio_stream::StreamExt;
536
537 enum StreamAction {
541 ProcessRequest(StreamSessionRequest),
542 EmitEnvelope(Envelope),
543 ClientError(Status),
544 ClientDone,
545 EventsClosed,
546 Lagged(u64),
547 }
548
549 let server = self.clone();
550 let output = async_stream::try_stream! {
551 let mut inbound = Box::pin(inbound);
552 let mut bound_session_id: Option<String> = None;
553 let mut session_events: Option<broadcast::Receiver<Envelope>> = None;
554 let mut replay_dedup: Option<std::collections::HashSet<String>> = None;
562
563 loop {
564 if session_events.is_some() {
565 let action = {
566 let events = session_events.as_mut().unwrap();
567 tokio::select! {
568 maybe_req = inbound.next() => {
569 match maybe_req {
570 Some(Ok(req)) => StreamAction::ProcessRequest(req),
571 Some(Err(status)) => StreamAction::ClientError(status),
572 None => StreamAction::ClientDone,
573 }
574 }
575 recv_result = events.recv() => {
576 match recv_result {
577 Ok(envelope) => StreamAction::EmitEnvelope(envelope),
578 Err(broadcast::error::RecvError::Closed) => StreamAction::EventsClosed,
579 Err(broadcast::error::RecvError::Lagged(n)) => StreamAction::Lagged(n),
580 }
581 }
582 }
583 };
584
585 match action {
586 StreamAction::ProcessRequest(req) => {
587 match server
588 .process_stream_request(
589 &identity,
590 req,
591 &mut bound_session_id,
592 &mut session_events,
593 )
594 .await
595 {
596 Ok(replay) => {
597 if !replay.is_empty() {
599 replay_dedup = Some(
600 replay.iter().map(|e| e.message_id.clone()).collect(),
601 );
602 }
603 for env in replay {
604 yield StreamSessionResponse {
605 response: Some(
606 crate::pb::stream_session_response::Response::Envelope(env),
607 ),
608 };
609 }
610 }
611 Err(status) if Self::is_stream_terminal_error(&status) => {
612 Err(status)?;
613 }
614 Err(status) => {
615 yield StreamSessionResponse {
618 response: Some(
619 crate::pb::stream_session_response::Response::Error(
620 PbMacpError {
621 code: status.message().to_string(),
622 message: status.message().to_string(),
623 session_id: bound_session_id.clone().unwrap_or_default(),
624 message_id: String::new(),
625 details: vec![],
626 },
627 ),
628 ),
629 };
630 }
631 }
632 while let Some(envelope) = Self::try_next_stream_event(&mut session_events)? {
633 if Self::should_skip_replayed(&mut replay_dedup, &envelope) {
634 continue;
635 }
636 yield StreamSessionResponse {
637 response: Some(
638 crate::pb::stream_session_response::Response::Envelope(envelope),
639 ),
640 };
641 }
642 }
643 StreamAction::EmitEnvelope(envelope) => {
644 if Self::should_skip_replayed(&mut replay_dedup, &envelope) {
645 continue;
646 }
647 yield StreamSessionResponse {
648 response: Some(
649 crate::pb::stream_session_response::Response::Envelope(envelope),
650 ),
651 };
652 }
653 StreamAction::ClientError(status) => {
654 Err(status)?;
655 }
656 StreamAction::ClientDone => {
657 while let Some(envelope) = Self::try_next_stream_event(&mut session_events)? {
658 if Self::should_skip_replayed(&mut replay_dedup, &envelope) {
659 continue;
660 }
661 yield StreamSessionResponse {
662 response: Some(
663 crate::pb::stream_session_response::Response::Envelope(envelope),
664 ),
665 };
666 }
667 break;
668 }
669 StreamAction::EventsClosed => {
670 session_events = None;
671 }
672 StreamAction::Lagged(skipped) => {
673 Err(Status::resource_exhausted(format!(
674 "StreamSession receiver fell behind by {skipped} envelopes"
675 )))?;
676 }
677 }
678 } else {
679 match inbound.next().await {
680 Some(Ok(req)) => {
681 match server
682 .process_stream_request(
683 &identity,
684 req,
685 &mut bound_session_id,
686 &mut session_events,
687 )
688 .await
689 {
690 Ok(replay) => {
691 if !replay.is_empty() {
693 replay_dedup = Some(
694 replay.iter().map(|e| e.message_id.clone()).collect(),
695 );
696 }
697 for env in replay {
698 yield StreamSessionResponse {
699 response: Some(
700 crate::pb::stream_session_response::Response::Envelope(env),
701 ),
702 };
703 }
704 }
705 Err(status) if Self::is_stream_terminal_error(&status) => {
706 Err(status)?;
707 }
708 Err(status) => {
709 yield StreamSessionResponse {
710 response: Some(
711 crate::pb::stream_session_response::Response::Error(
712 PbMacpError {
713 code: status.message().to_string(),
714 message: status.message().to_string(),
715 session_id: bound_session_id.clone().unwrap_or_default(),
716 message_id: String::new(),
717 details: vec![],
718 },
719 ),
720 ),
721 };
722 }
723 }
724 while let Some(envelope) = Self::try_next_stream_event(&mut session_events)? {
725 if Self::should_skip_replayed(&mut replay_dedup, &envelope) {
726 continue;
727 }
728 yield StreamSessionResponse {
729 response: Some(
730 crate::pb::stream_session_response::Response::Envelope(envelope),
731 ),
732 };
733 }
734 }
735 Some(Err(status)) => Err(status)?,
736 None => break,
737 }
738 }
739 }
740 };
741 Box::pin(output)
742 }
743
744 fn is_stream_terminal_error(status: &Status) -> bool {
748 matches!(
749 status.code(),
750 tonic::Code::Unauthenticated
751 | tonic::Code::Internal
752 | tonic::Code::ResourceExhausted
753 | tonic::Code::InvalidArgument
754 | tonic::Code::NotFound
755 | tonic::Code::AlreadyExists
756 )
757 }
758
759 fn status_from_error(err: MacpError) -> Status {
760 match err {
761 MacpError::Unauthenticated => Status::unauthenticated(err.to_string()),
762 MacpError::Forbidden => Status::permission_denied(err.to_string()),
763 MacpError::PayloadTooLarge => Status::resource_exhausted(err.to_string()),
764 MacpError::RateLimited => Status::resource_exhausted(err.to_string()),
765 MacpError::StorageFailed => Status::internal(err.to_string()),
766 MacpError::InvalidSessionId => Status::invalid_argument(err.to_string()),
767 MacpError::InvalidPolicyDefinition => Status::invalid_argument(err.to_string()),
768 MacpError::SessionAlreadyExists => Status::already_exists(err.to_string()),
769 MacpError::PolicyDenied { ref reasons } => {
770 let details = Self::error_details_bytes(&err);
771 let msg = if reasons.is_empty() {
772 "PolicyDenied".to_string()
773 } else {
774 format!("PolicyDenied: {}", reasons.join("; "))
775 };
776 let mut status = Status::failed_precondition(msg);
777 if !details.is_empty() {
778 let val = tonic::metadata::MetadataValue::from_bytes(&details);
780 status
781 .metadata_mut()
782 .insert_bin("macp-error-details-bin", val);
783 }
784 status
785 }
786 _ => Status::failed_precondition(err.to_string()),
787 }
788 }
789}
790
791#[tonic::async_trait]
792impl MacpRuntimeService for MacpServer {
793 async fn initialize(
794 &self,
795 request: Request<InitializeRequest>,
796 ) -> Result<Response<InitializeResponse>, Status> {
797 let req = request.into_inner();
798 if req.supported_protocol_versions.is_empty() {
799 return Err(Status::invalid_argument(
800 "INVALID_REQUEST: supported_protocol_versions must not be empty",
801 ));
802 }
803 if !req
804 .supported_protocol_versions
805 .iter()
806 .any(|v| v == macp_core::MACP_VERSION)
807 {
808 return Err(Status::failed_precondition(
809 "UNSUPPORTED_PROTOCOL_VERSION: no mutually supported protocol version",
810 ));
811 }
812
813 Ok(Response::new(InitializeResponse {
814 selected_protocol_version: macp_core::MACP_VERSION.into(),
815 runtime_info: Some(RuntimeInfo {
816 name: "macp-runtime".into(),
817 title: "MACP Reference Runtime".into(),
818 version: env!("CARGO_PKG_VERSION").into(),
821 description: "Reference implementation of the Multi-Agent Coordination Protocol"
822 .into(),
823 website_url: String::new(),
824 }),
825 capabilities: Some(Capabilities {
826 sessions: Some(SessionsCapability { stream: true, list_sessions: true, watch_sessions: true }),
827 cancellation: Some(CancellationCapability {
828 cancel_session: true,
829 }),
830 progress: Some(ProgressCapability { progress: true }),
831 manifest: Some(ManifestCapability { get_manifest: true }),
832 mode_registry: Some(ModeRegistryCapability {
833 list_modes: true,
834 list_changed: true,
835 }),
836 roots: Some(RootsCapability {
837 list_roots: true,
843 list_changed: false,
844 }),
845 policy_registry: Some(PolicyRegistryCapability {
846 register_policy: !self.policies_read_only,
847 list_policies: true,
848 list_changed: true,
849 }),
850 experimental: Some(crate::pb::ExperimentalCapabilities {
851 features: HashMap::from([
852 ("ext_mode_lifecycle".into(), "true".into()),
853 ]),
854 }),
855 }),
856 supported_modes: self.runtime.registered_mode_names(),
857 instructions: "Authenticate requests with Authorization: Bearer <token>. Use the unary Send RPC for all session messaging. For local development only (MACP_ALLOW_INSECURE=1 with no auth configured), the bearer token's value is used directly as the sender identity.".into(),
858 }))
859 }
860
861 async fn send(&self, request: Request<SendRequest>) -> Result<Response<SendResponse>, Status> {
862 let env = request
863 .get_ref()
864 .envelope
865 .clone()
866 .ok_or_else(|| Status::invalid_argument("SendRequest must contain an envelope"))?;
867
868 let result = async {
869 self.validate_envelope_shape(&env)?;
870 let (env, max_open) = self.authenticate_send_request(&request, env).await?;
871 self.runtime
872 .process(&env, max_open)
873 .await
874 .map(|process_result| (env, process_result))
875 }
876 .await;
877
878 let ack = match result {
879 Ok((env, process_result)) => Ack {
880 ok: true,
881 duplicate: process_result.duplicate,
882 message_id: env.message_id.clone(),
883 session_id: env.session_id.clone(),
884 accepted_at_unix_ms: chrono::Utc::now().timestamp_millis(),
885 session_state: Self::session_state_to_pb(&process_result.session_state),
886 error: None,
887 },
888 Err(err) => {
889 let env = request.get_ref().envelope.clone().unwrap_or_default();
890 if !env.session_id.is_empty() {
894 self.runtime.metrics().record_message_rejected(&env.mode);
895 if env.message_type == "Commitment" {
896 self.runtime.metrics().record_commitment_rejected(&env.mode);
897 }
898 }
899 Self::make_error_ack(&err, &env)
900 }
901 };
902
903 Ok(Response::new(SendResponse { ack: Some(ack) }))
904 }
905
906 async fn get_session(
907 &self,
908 request: Request<GetSessionRequest>,
909 ) -> Result<Response<GetSessionResponse>, Status> {
910 let session_id = request.get_ref().session_id.clone();
911 let _identity = self
912 .authenticate_session_access(&request, &session_id)
913 .await?;
914 let session = self
915 .runtime
916 .get_session_checked(&session_id)
917 .await
918 .ok_or_else(|| Status::not_found(format!("Session '{}' not found", session_id)))?;
919
920 Ok(Response::new(GetSessionResponse {
921 metadata: Some(Self::session_to_metadata(&session)),
922 }))
923 }
924
925 async fn cancel_session(
926 &self,
927 request: Request<CancelSessionRequest>,
928 ) -> Result<Response<CancelSessionResponse>, Status> {
929 let session_id = request.get_ref().session_id.clone();
930 let identity = self
931 .security
932 .authenticate_metadata(request.metadata())
933 .await
934 .map_err(Self::status_from_error)?;
935 let session = self
936 .runtime
937 .get_session_checked(&session_id)
938 .await
939 .ok_or_else(|| Status::not_found(format!("Session '{}' not found", session_id)))?;
940 if identity.sender != session.initiator_sender
943 && crate::mode::util::check_commitment_authority(&session, &identity.sender).is_err()
944 {
945 return Err(Status::permission_denied(
946 "FORBIDDEN: only the session initiator or policy-delegated roles can cancel",
947 ));
948 }
949 let sender = identity.sender.clone();
950 let req = request.into_inner();
951 match self
952 .runtime
953 .cancel_session(&req.session_id, &req.reason, &sender)
954 .await
955 {
956 Ok(result) => Ok(Response::new(CancelSessionResponse {
957 ack: Some(Ack {
958 ok: true,
959 duplicate: false,
960 message_id: String::new(),
961 session_id: req.session_id,
962 accepted_at_unix_ms: chrono::Utc::now().timestamp_millis(),
963 session_state: Self::session_state_to_pb(&result.session_state),
964 error: None,
965 }),
966 })),
967 Err(err) => Ok(Response::new(CancelSessionResponse {
968 ack: Some(Ack {
969 ok: false,
970 duplicate: false,
971 message_id: String::new(),
972 session_id: req.session_id.clone(),
973 accepted_at_unix_ms: chrono::Utc::now().timestamp_millis(),
974 session_state: PbSessionState::Unspecified.into(),
975 error: Some(PbMacpError {
976 code: err.error_code().into(),
977 message: err.to_string(),
978 session_id: req.session_id,
979 message_id: String::new(),
980 details: vec![],
981 }),
982 }),
983 })),
984 }
985 }
986
987 async fn suspend_session(
988 &self,
989 request: Request<SuspendSessionRequest>,
990 ) -> Result<Response<SuspendSessionResponse>, Status> {
991 let session_id = request.get_ref().session_id.clone();
992 let identity = self
993 .security
994 .authenticate_metadata(request.metadata())
995 .await
996 .map_err(Self::status_from_error)?;
997 let session = self
998 .runtime
999 .get_session_checked(&session_id)
1000 .await
1001 .ok_or_else(|| Status::not_found(format!("Session '{}' not found", session_id)))?;
1002 if identity.sender != session.initiator_sender
1005 && crate::mode::util::check_commitment_authority(&session, &identity.sender).is_err()
1006 {
1007 return Err(Status::permission_denied(
1008 "FORBIDDEN: only the session initiator or policy-delegated roles can suspend",
1009 ));
1010 }
1011 let sender = identity.sender.clone();
1012 let req = request.into_inner();
1013 match self
1014 .runtime
1015 .suspend_session(&req.session_id, &req.reason, &sender)
1016 .await
1017 {
1018 Ok(result) => Ok(Response::new(SuspendSessionResponse {
1019 ack: Some(Ack {
1020 ok: true,
1021 duplicate: false,
1022 message_id: String::new(),
1023 session_id: req.session_id,
1024 accepted_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1025 session_state: Self::session_state_to_pb(&result.session_state),
1026 error: None,
1027 }),
1028 })),
1029 Err(err) => Ok(Response::new(SuspendSessionResponse {
1030 ack: Some(Ack {
1031 ok: false,
1032 duplicate: false,
1033 message_id: String::new(),
1034 session_id: req.session_id.clone(),
1035 accepted_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1036 session_state: PbSessionState::Unspecified.into(),
1037 error: Some(PbMacpError {
1038 code: err.error_code().into(),
1039 message: err.to_string(),
1040 session_id: req.session_id,
1041 message_id: String::new(),
1042 details: vec![],
1043 }),
1044 }),
1045 })),
1046 }
1047 }
1048
1049 async fn resume_session(
1050 &self,
1051 request: Request<ResumeSessionRequest>,
1052 ) -> Result<Response<ResumeSessionResponse>, Status> {
1053 let session_id = request.get_ref().session_id.clone();
1054 let identity = self
1055 .security
1056 .authenticate_metadata(request.metadata())
1057 .await
1058 .map_err(Self::status_from_error)?;
1059 let session = self
1060 .runtime
1061 .get_session_checked(&session_id)
1062 .await
1063 .ok_or_else(|| Status::not_found(format!("Session '{}' not found", session_id)))?;
1064 if identity.sender != session.initiator_sender
1065 && crate::mode::util::check_commitment_authority(&session, &identity.sender).is_err()
1066 {
1067 return Err(Status::permission_denied(
1068 "FORBIDDEN: only the session initiator or policy-delegated roles can resume",
1069 ));
1070 }
1071 let sender = identity.sender.clone();
1072 let req = request.into_inner();
1073 match self
1074 .runtime
1075 .resume_session(&req.session_id, &req.reason, &sender)
1076 .await
1077 {
1078 Ok(result) => Ok(Response::new(ResumeSessionResponse {
1079 ack: Some(Ack {
1080 ok: true,
1081 duplicate: false,
1082 message_id: String::new(),
1083 session_id: req.session_id,
1084 accepted_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1085 session_state: Self::session_state_to_pb(&result.session_state),
1086 error: None,
1087 }),
1088 })),
1089 Err(err) => Ok(Response::new(ResumeSessionResponse {
1090 ack: Some(Ack {
1091 ok: false,
1092 duplicate: false,
1093 message_id: String::new(),
1094 session_id: req.session_id.clone(),
1095 accepted_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1096 session_state: PbSessionState::Unspecified.into(),
1097 error: Some(PbMacpError {
1098 code: err.error_code().into(),
1099 message: err.to_string(),
1100 session_id: req.session_id,
1101 message_id: String::new(),
1102 details: vec![],
1103 }),
1104 }),
1105 })),
1106 }
1107 }
1108
1109 async fn get_manifest(
1110 &self,
1111 request: Request<GetManifestRequest>,
1112 ) -> Result<Response<GetManifestResponse>, Status> {
1113 let req = request.into_inner();
1114 if !req.agent_id.is_empty() && req.agent_id != "macp-runtime" {
1115 return Err(Status::not_found(format!(
1116 "Agent '{}' not found",
1117 req.agent_id
1118 )));
1119 }
1120
1121 Ok(Response::new(GetManifestResponse {
1122 manifest: Some(crate::pb::AgentManifest {
1123 agent_id: "macp-runtime".into(),
1124 title: "MACP Reference Runtime".into(),
1125 description: "Reference implementation of MACP".into(),
1126 supported_modes: self.runtime.registered_mode_names(),
1127 input_content_types: vec!["application/macp-envelope+proto".into()],
1128 output_content_types: vec!["application/macp-envelope+proto".into()],
1129 metadata: HashMap::new(),
1130 transport_endpoints: vec![],
1132 }),
1133 }))
1134 }
1135
1136 async fn list_modes(
1137 &self,
1138 _request: Request<ListModesRequest>,
1139 ) -> Result<Response<ListModesResponse>, Status> {
1140 Ok(Response::new(ListModesResponse {
1141 modes: self.runtime.standard_mode_descriptors(),
1142 }))
1143 }
1144
1145 async fn list_roots(
1146 &self,
1147 _request: Request<ListRootsRequest>,
1148 ) -> Result<Response<ListRootsResponse>, Status> {
1149 Ok(Response::new(ListRootsResponse { roots: vec![] }))
1150 }
1151
1152 type StreamSessionStream = SessionResponseStream;
1153
1154 async fn stream_session(
1155 &self,
1156 request: Request<tonic::Streaming<StreamSessionRequest>>,
1157 ) -> Result<Response<Self::StreamSessionStream>, Status> {
1158 let identity = self
1159 .security
1160 .authenticate_metadata(request.metadata())
1161 .await
1162 .map_err(Self::status_from_error)?;
1163 let inbound = request.into_inner();
1164 Ok(Response::new(
1165 self.build_stream_session_stream(identity, inbound),
1166 ))
1167 }
1168
1169 type WatchModeRegistryStream = std::pin::Pin<
1170 Box<dyn futures_core::Stream<Item = Result<WatchModeRegistryResponse, Status>> + Send>,
1171 >;
1172
1173 async fn watch_mode_registry(
1174 &self,
1175 _request: Request<WatchModeRegistryRequest>,
1176 ) -> Result<Response<Self::WatchModeRegistryStream>, Status> {
1177 let mut rx = self.runtime.subscribe_mode_changes();
1178 let stream = async_stream::try_stream! {
1179 yield WatchModeRegistryResponse {
1181 change: Some(crate::pb::RegistryChanged {
1182 registry: "modes".into(),
1183 observed_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1184 }),
1185 };
1186 while rx.recv().await.is_ok() {
1188 yield WatchModeRegistryResponse {
1189 change: Some(crate::pb::RegistryChanged {
1190 registry: "modes".into(),
1191 observed_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1192 }),
1193 };
1194 }
1195 };
1196 Ok(Response::new(Box::pin(stream)))
1197 }
1198
1199 type WatchRootsStream = std::pin::Pin<
1200 Box<dyn futures_core::Stream<Item = Result<WatchRootsResponse, Status>> + Send>,
1201 >;
1202
1203 async fn watch_roots(
1204 &self,
1205 _request: Request<WatchRootsRequest>,
1206 ) -> Result<Response<Self::WatchRootsStream>, Status> {
1207 let initial = WatchRootsResponse {
1208 change: Some(crate::pb::RootsChanged {
1209 observed_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1210 }),
1211 };
1212 let stream = async_stream::try_stream! {
1213 yield initial;
1214 std::future::pending::<()>().await;
1216 };
1217 Ok(Response::new(Box::pin(stream)))
1218 }
1219
1220 type WatchSignalsStream = std::pin::Pin<
1221 Box<dyn futures_core::Stream<Item = Result<WatchSignalsResponse, Status>> + Send>,
1222 >;
1223
1224 type WatchSessionsStream = std::pin::Pin<
1225 Box<dyn futures_core::Stream<Item = Result<WatchSessionsResponse, Status>> + Send>,
1226 >;
1227
1228 async fn watch_signals(
1229 &self,
1230 request: Request<WatchSignalsRequest>,
1231 ) -> Result<Response<Self::WatchSignalsStream>, Status> {
1232 let _identity = self
1237 .security
1238 .authenticate_metadata(request.metadata())
1239 .await
1240 .map_err(Self::status_from_error)?;
1241 let mut rx = self.runtime.subscribe_signals();
1242 let stream = async_stream::try_stream! {
1243 loop {
1244 match rx.recv().await {
1245 Ok(envelope) => {
1246 yield WatchSignalsResponse {
1247 envelope: Some(envelope),
1248 };
1249 }
1250 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
1254 Err(Status::resource_exhausted(format!(
1255 "WatchSignals receiver fell behind by {skipped} signals"
1256 )))?;
1257 }
1258 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
1259 }
1260 }
1261 };
1262 Ok(Response::new(Box::pin(stream)))
1263 }
1264
1265 async fn list_sessions(
1268 &self,
1269 request: Request<ListSessionsRequest>,
1270 ) -> Result<Response<ListSessionsResponse>, Status> {
1271 let _identity = self
1275 .security
1276 .authenticate_metadata(request.metadata())
1277 .await
1278 .map_err(Self::status_from_error)?;
1279 let req = request.into_inner();
1280
1281 if req.page_size < 0 {
1288 return Err(Status::invalid_argument(
1289 "INVALID_ARGUMENT: page_size must not be negative",
1290 ));
1291 }
1292 let effective = if req.page_size == 0 {
1295 self.security.list_sessions_default_page_size
1296 } else {
1297 (req.page_size as usize).min(self.security.list_sessions_max_page_size)
1298 };
1299 let effective = effective.max(1);
1307
1308 let cursor = if req.page_token.is_empty() {
1309 None
1310 } else {
1311 Some(
1316 crate::pagination::decode_page_token(&req.page_token).map_err(|_| {
1317 Status::invalid_argument(
1318 "INVALID_ARGUMENT: page_token is not a valid continuation token",
1319 )
1320 })?,
1321 )
1322 };
1323
1324 let ids = self
1329 .runtime
1330 .registry
1331 .session_ids_after(cursor.as_deref(), effective.saturating_add(1))
1332 .await;
1333 let has_more = ids.len() > effective;
1334 let page_ids = &ids[..effective.min(ids.len())];
1335
1336 let next_page_token = match (has_more, page_ids.last()) {
1342 (true, Some(last)) => crate::pagination::encode_page_token(last),
1343 _ => String::new(),
1344 };
1345
1346 let mut metadata: Vec<SessionMetadata> = Vec::with_capacity(page_ids.len());
1347 for id in page_ids {
1348 if let Some(session) = self.runtime.registry.get_session(id).await {
1352 debug_assert_eq!(
1353 session.session_id, *id,
1354 "registry map key must equal Session::session_id — paging orders \
1355 by the key but emits the field"
1356 );
1357 metadata.push(Self::session_to_metadata(&session));
1358 }
1359 }
1360
1361 Ok(Response::new(ListSessionsResponse {
1362 sessions: metadata,
1363 next_page_token,
1364 }))
1365 }
1366
1367 async fn watch_sessions(
1368 &self,
1369 request: Request<WatchSessionsRequest>,
1370 ) -> Result<Response<Self::WatchSessionsStream>, Status> {
1371 let _identity = self
1372 .security
1373 .authenticate_metadata(request.metadata())
1374 .await
1375 .map_err(Self::status_from_error)?;
1376 let mut rx = self.runtime.subscribe_session_lifecycle();
1382 let runtime = Arc::clone(&self.runtime);
1383 let stream = async_stream::try_stream! {
1384 let mut sync = crate::watch_sync::InitialSync::begin(&runtime.registry).await;
1393 let mut synced: std::collections::HashSet<String> =
1405 std::collections::HashSet::with_capacity(sync.remaining());
1406 let mut pending: std::collections::VecDeque<crate::runtime::SessionLifecycleEvent> =
1414 std::collections::VecDeque::new();
1415 loop {
1416 if let Err(drain_err) = crate::watch_sync::drain_lifecycle_events(
1420 &mut rx,
1421 &mut pending,
1422 crate::watch_sync::PENDING_EVENT_LIMIT,
1423 ) {
1424 Err(Status::resource_exhausted(drain_err.message()))?;
1425 break;
1426 }
1427 let Some(session) = sync.next_session().await else { break };
1430 synced.insert(session.session_id.clone());
1431 yield WatchSessionsResponse {
1432 event: Some(SessionLifecycleEvent {
1433 event_type: session_lifecycle_event::EventType::Created.into(),
1434 session: Some(Self::session_to_metadata(&session)),
1435 observed_at_unix_ms: session.started_at_unix_ms,
1436 }),
1437 };
1438 }
1439 loop {
1442 let event = match pending.pop_front() {
1443 Some(event) => event,
1444 None => match rx.recv().await {
1445 Ok(event) => event,
1446 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
1447 Err(Status::resource_exhausted(format!(
1448 "WatchSessions receiver fell behind by {skipped} events"
1449 )))?;
1450 break;
1451 }
1452 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
1453 },
1454 };
1455 let (event_type, sid) = match &event {
1456 crate::runtime::SessionLifecycleEvent::Created { session_id } =>
1457 (session_lifecycle_event::EventType::Created, session_id.clone()),
1458 crate::runtime::SessionLifecycleEvent::Resolved { session_id } =>
1459 (session_lifecycle_event::EventType::Resolved, session_id.clone()),
1460 crate::runtime::SessionLifecycleEvent::Expired { session_id } =>
1461 (session_lifecycle_event::EventType::Expired, session_id.clone()),
1462 crate::runtime::SessionLifecycleEvent::Suspended { session_id } =>
1463 (session_lifecycle_event::EventType::Suspended, session_id.clone()),
1464 crate::runtime::SessionLifecycleEvent::Resumed { session_id } =>
1465 (session_lifecycle_event::EventType::Resumed, session_id.clone()),
1466 crate::runtime::SessionLifecycleEvent::Cancelled { session_id } =>
1467 (session_lifecycle_event::EventType::Cancelled, session_id.clone()),
1468 };
1469 if event_type == session_lifecycle_event::EventType::Created
1480 && synced.contains(&sid)
1481 {
1482 continue;
1483 }
1484 let session_meta = runtime.registry.get_session(&sid).await
1485 .map(|s| Self::session_to_metadata(&s));
1486 yield WatchSessionsResponse {
1487 event: Some(SessionLifecycleEvent {
1488 event_type: event_type.into(),
1489 session: session_meta,
1490 observed_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1491 }),
1492 };
1493 }
1494 };
1495 Ok(Response::new(Box::pin(stream)))
1496 }
1497
1498 async fn list_ext_modes(
1501 &self,
1502 _request: Request<ListExtModesRequest>,
1503 ) -> Result<Response<ListExtModesResponse>, Status> {
1504 Ok(Response::new(ListExtModesResponse {
1505 modes: self.runtime.extension_mode_descriptors(),
1506 }))
1507 }
1508
1509 async fn register_ext_mode(
1510 &self,
1511 request: Request<RegisterExtModeRequest>,
1512 ) -> Result<Response<RegisterExtModeResponse>, Status> {
1513 let identity = self
1514 .security
1515 .authenticate_metadata(request.metadata())
1516 .await
1517 .map_err(Self::status_from_error)?;
1518 self.security
1519 .authorize_mode_registry(&identity)
1520 .map_err(Self::status_from_error)?;
1521 let req = request.into_inner();
1522 let descriptor = req
1523 .mode_descriptor
1524 .ok_or_else(|| Status::invalid_argument("descriptor required"))?;
1525 match self.runtime.register_extension(descriptor) {
1526 Ok(()) => Ok(Response::new(RegisterExtModeResponse {
1527 ok: true,
1528 error: String::new(),
1529 })),
1530 Err(e) => Ok(Response::new(RegisterExtModeResponse {
1531 ok: false,
1532 error: e,
1533 })),
1534 }
1535 }
1536
1537 async fn unregister_ext_mode(
1538 &self,
1539 request: Request<UnregisterExtModeRequest>,
1540 ) -> Result<Response<UnregisterExtModeResponse>, Status> {
1541 let identity = self
1542 .security
1543 .authenticate_metadata(request.metadata())
1544 .await
1545 .map_err(Self::status_from_error)?;
1546 self.security
1547 .authorize_mode_registry(&identity)
1548 .map_err(Self::status_from_error)?;
1549 let req = request.into_inner();
1550 match self.runtime.unregister_extension(&req.mode) {
1551 Ok(()) => Ok(Response::new(UnregisterExtModeResponse {
1552 ok: true,
1553 error: String::new(),
1554 })),
1555 Err(e) => Ok(Response::new(UnregisterExtModeResponse {
1556 ok: false,
1557 error: e,
1558 })),
1559 }
1560 }
1561
1562 async fn promote_mode(
1563 &self,
1564 request: Request<PromoteModeRequest>,
1565 ) -> Result<Response<PromoteModeResponse>, Status> {
1566 let identity = self
1567 .security
1568 .authenticate_metadata(request.metadata())
1569 .await
1570 .map_err(Self::status_from_error)?;
1571 self.security
1572 .authorize_mode_registry(&identity)
1573 .map_err(Self::status_from_error)?;
1574 let req = request.into_inner();
1575 let new_name = if req.promoted_mode_name.is_empty() {
1576 None
1577 } else {
1578 Some(req.promoted_mode_name.as_str())
1579 };
1580 match self.runtime.promote_mode(&req.mode, new_name) {
1581 Ok(final_name) => Ok(Response::new(PromoteModeResponse {
1582 ok: true,
1583 error: String::new(),
1584 mode: final_name,
1585 })),
1586 Err(e) => Ok(Response::new(PromoteModeResponse {
1587 ok: false,
1588 error: e,
1589 mode: String::new(),
1590 })),
1591 }
1592 }
1593
1594 async fn register_policy(
1597 &self,
1598 request: Request<RegisterPolicyRequest>,
1599 ) -> Result<Response<RegisterPolicyResponse>, Status> {
1600 if self.policies_read_only {
1601 return Err(Status::failed_precondition(
1602 "policy registry is read-only: policies are file-loaded via MACP_POLICIES_DIR",
1603 ));
1604 }
1605 let identity = self
1606 .security
1607 .authenticate_metadata(request.metadata())
1608 .await
1609 .map_err(Self::status_from_error)?;
1610 self.security
1611 .authorize_mode_registry(&identity)
1612 .map_err(Self::status_from_error)?;
1613 let req = request.into_inner();
1614 let descriptor = req
1615 .policy_descriptor
1616 .ok_or_else(|| Status::invalid_argument("descriptor required"))?;
1617 let definition = Self::policy_descriptor_to_definition(&descriptor);
1618 match self.runtime.register_policy(definition) {
1619 Ok(()) => Ok(Response::new(RegisterPolicyResponse {
1620 ok: true,
1621 error: String::new(),
1622 })),
1623 Err(e) => Ok(Response::new(RegisterPolicyResponse {
1624 ok: false,
1625 error: e,
1626 })),
1627 }
1628 }
1629
1630 async fn unregister_policy(
1631 &self,
1632 request: Request<UnregisterPolicyRequest>,
1633 ) -> Result<Response<UnregisterPolicyResponse>, Status> {
1634 if self.policies_read_only {
1635 return Err(Status::failed_precondition(
1636 "policy registry is read-only: policies are file-loaded via MACP_POLICIES_DIR",
1637 ));
1638 }
1639 let identity = self
1640 .security
1641 .authenticate_metadata(request.metadata())
1642 .await
1643 .map_err(Self::status_from_error)?;
1644 self.security
1645 .authorize_mode_registry(&identity)
1646 .map_err(Self::status_from_error)?;
1647 let req = request.into_inner();
1648 match self.runtime.unregister_policy(&req.policy_id) {
1649 Ok(()) => Ok(Response::new(UnregisterPolicyResponse {
1650 ok: true,
1651 error: String::new(),
1652 })),
1653 Err(e) => Ok(Response::new(UnregisterPolicyResponse {
1654 ok: false,
1655 error: e,
1656 })),
1657 }
1658 }
1659
1660 async fn get_policy(
1661 &self,
1662 request: Request<GetPolicyRequest>,
1663 ) -> Result<Response<GetPolicyResponse>, Status> {
1664 let _identity = self
1665 .security
1666 .authenticate_metadata(request.metadata())
1667 .await
1668 .map_err(Self::status_from_error)?;
1669 let req = request.into_inner();
1670 let policy = self
1671 .runtime
1672 .get_policy(&req.policy_id)
1673 .ok_or_else(|| Status::not_found(format!("Policy '{}' not found", req.policy_id)))?;
1674 Ok(Response::new(GetPolicyResponse {
1675 policy_descriptor: Some(Self::policy_definition_to_descriptor(&policy)),
1676 }))
1677 }
1678
1679 async fn list_policies(
1680 &self,
1681 request: Request<ListPoliciesRequest>,
1682 ) -> Result<Response<ListPoliciesResponse>, Status> {
1683 let _identity = self
1684 .security
1685 .authenticate_metadata(request.metadata())
1686 .await
1687 .map_err(Self::status_from_error)?;
1688 let req = request.into_inner();
1689 let mode_filter = if req.mode.is_empty() {
1690 None
1691 } else {
1692 Some(req.mode.as_str())
1693 };
1694 let policies = self.runtime.list_policies(mode_filter);
1695 let descriptors = policies
1696 .iter()
1697 .map(Self::policy_definition_to_descriptor)
1698 .collect();
1699 Ok(Response::new(ListPoliciesResponse { descriptors }))
1700 }
1701
1702 type WatchPoliciesStream = std::pin::Pin<
1703 Box<dyn futures_core::Stream<Item = Result<WatchPoliciesResponse, Status>> + Send>,
1704 >;
1705
1706 async fn watch_policies(
1707 &self,
1708 _request: Request<WatchPoliciesRequest>,
1709 ) -> Result<Response<Self::WatchPoliciesStream>, Status> {
1710 let mut rx = self.runtime.subscribe_policy_changes();
1711 let runtime = Arc::clone(&self.runtime);
1712 let stream = async_stream::try_stream! {
1713 let policies = runtime.list_policies(None);
1715 let descriptors: Vec<PolicyDescriptor> = policies
1716 .iter()
1717 .map(MacpServer::policy_definition_to_descriptor)
1718 .collect();
1719 yield WatchPoliciesResponse {
1720 descriptors,
1721 observed_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1722 };
1723 while rx.recv().await.is_ok() {
1725 let policies = runtime.list_policies(None);
1726 let descriptors: Vec<PolicyDescriptor> = policies
1727 .iter()
1728 .map(MacpServer::policy_definition_to_descriptor)
1729 .collect();
1730 yield WatchPoliciesResponse {
1731 descriptors,
1732 observed_at_unix_ms: chrono::Utc::now().timestamp_millis(),
1733 };
1734 }
1735 };
1736 Ok(Response::new(Box::pin(stream)))
1737 }
1738}
1739
1740impl MacpServer {
1743 fn policy_descriptor_to_definition(
1744 descriptor: &PolicyDescriptor,
1745 ) -> crate::policy::PolicyDefinition {
1746 let rules: serde_json::Value = if descriptor.rules.is_empty() {
1747 serde_json::json!({})
1748 } else {
1749 serde_json::from_str(&descriptor.rules).unwrap_or_else(|_| serde_json::json!({}))
1750 };
1751 crate::policy::PolicyDefinition {
1752 policy_id: descriptor.policy_id.clone(),
1753 mode: descriptor.mode.clone(),
1754 description: descriptor.description.clone(),
1755 rules,
1756 schema_version: descriptor.schema_version,
1757 }
1758 }
1759
1760 fn policy_definition_to_descriptor(
1761 definition: &crate::policy::PolicyDefinition,
1762 ) -> PolicyDescriptor {
1763 PolicyDescriptor {
1764 policy_id: definition.policy_id.clone(),
1765 mode: definition.mode.clone(),
1766 description: definition.description.clone(),
1767 rules: serde_json::to_string(&definition.rules).unwrap_or_default(),
1768 schema_version: definition.schema_version,
1769 registered_at_unix_ms: 0,
1770 }
1771 }
1772}
1773
1774#[cfg(test)]
1775mod tests {
1776 use super::*;
1777 use crate::log_store::LogStore;
1778 use crate::pb::SessionStartPayload;
1779 use crate::registry::SessionRegistry;
1780 use chrono::Utc;
1781 use prost::Message;
1782
1783 fn new_sid() -> String {
1784 uuid::Uuid::new_v4().as_hyphenated().to_string()
1785 }
1786
1787 fn make_server() -> (MacpServer, Arc<Runtime>) {
1788 make_server_with_security(SecurityLayer::dev_mode())
1789 }
1790
1791 fn make_server_with_security(security: SecurityLayer) -> (MacpServer, Arc<Runtime>) {
1795 let storage: Arc<dyn crate::storage::StorageBackend> =
1796 Arc::new(crate::storage::MemoryBackend);
1797 let registry = Arc::new(SessionRegistry::new());
1798 let log_store = Arc::new(LogStore::new());
1799 let runtime = Arc::new(Runtime::new(storage, registry, log_store));
1800 let server = MacpServer::new(runtime.clone(), security);
1801 (server, runtime)
1802 }
1803
1804 fn send_req(sender: &str, env: Envelope) -> Request<SendRequest> {
1805 let mut req = Request::new(SendRequest {
1806 envelope: Some(env),
1807 });
1808 req.metadata_mut()
1809 .insert("authorization", format!("Bearer {sender}").parse().unwrap());
1810 req
1811 }
1812
1813 async fn do_send(server: &MacpServer, sender: &str, env: Envelope) -> Ack {
1814 let resp = server.send(send_req(sender, env)).await.unwrap();
1815 resp.into_inner().ack.unwrap()
1816 }
1817
1818 fn start_payload() -> Vec<u8> {
1819 SessionStartPayload {
1820 intent: "intent".into(),
1821 participants: vec!["agent://fraud".into()],
1822 mode_version: "1.0.0".into(),
1823 configuration_version: "cfg-1".into(),
1824 policy_version: String::new(),
1825 ttl_ms: 1000,
1826 context_id: String::new(),
1827 extensions: std::collections::HashMap::new(),
1828 roots: vec![],
1829 max_suspend_ms: 0,
1830 }
1831 .encode_to_vec()
1832 }
1833
1834 #[tokio::test]
1835 async fn sender_is_derived_from_authenticated_metadata() {
1836 let (server, runtime) = make_server();
1837 let sid = new_sid();
1838 let ack = do_send(
1839 &server,
1840 "agent://orchestrator",
1841 Envelope {
1842 macp_version: "1.0".into(),
1843 mode: "macp.mode.decision.v1".into(),
1844 message_type: "SessionStart".into(),
1845 message_id: "m1".into(),
1846 session_id: sid.clone(),
1847 sender: String::new(),
1848 timestamp_unix_ms: Utc::now().timestamp_millis(),
1849 payload: start_payload(),
1850 },
1851 )
1852 .await;
1853 assert!(ack.ok);
1854 let session = runtime.get_session_checked(&sid).await.unwrap();
1855 assert_eq!(session.initiator_sender, "agent://orchestrator");
1856 }
1857
1858 #[tokio::test]
1859 async fn spoofed_sender_is_rejected() {
1860 let (server, _) = make_server();
1861 let sid = new_sid();
1862 let ack = do_send(
1863 &server,
1864 "agent://orchestrator",
1865 Envelope {
1866 macp_version: "1.0".into(),
1867 mode: "macp.mode.decision.v1".into(),
1868 message_type: "SessionStart".into(),
1869 message_id: "m1".into(),
1870 session_id: sid,
1871 sender: "agent://spoof".into(),
1872 timestamp_unix_ms: Utc::now().timestamp_millis(),
1873 payload: start_payload(),
1874 },
1875 )
1876 .await;
1877 assert!(!ack.ok);
1878 assert_eq!(ack.error.as_ref().unwrap().code, "UNAUTHENTICATED");
1879 }
1880
1881 #[tokio::test]
1882 async fn get_session_requires_session_membership() {
1883 let (server, _) = make_server();
1884 let sid = new_sid();
1885 let ack = do_send(
1886 &server,
1887 "agent://orchestrator",
1888 Envelope {
1889 macp_version: "1.0".into(),
1890 mode: "macp.mode.decision.v1".into(),
1891 message_type: "SessionStart".into(),
1892 message_id: "m1".into(),
1893 session_id: sid.clone(),
1894 sender: String::new(),
1895 timestamp_unix_ms: Utc::now().timestamp_millis(),
1896 payload: start_payload(),
1897 },
1898 )
1899 .await;
1900 assert!(ack.ok);
1901
1902 let mut req = Request::new(GetSessionRequest { session_id: sid });
1903 req.metadata_mut().insert(
1904 "authorization",
1905 format!("Bearer {}", "agent://outsider").parse().unwrap(),
1906 );
1907 let err = server.get_session(req).await.unwrap_err();
1908 assert_eq!(err.code(), tonic::Code::PermissionDenied);
1909 }
1910
1911 #[tokio::test]
1912 async fn register_ext_mode_requires_authenticated_registry_permission() {
1913 let storage: Arc<dyn crate::storage::StorageBackend> =
1914 Arc::new(crate::storage::MemoryBackend);
1915 let registry = Arc::new(SessionRegistry::new());
1916 let log_store = Arc::new(LogStore::new());
1917 let runtime = Arc::new(Runtime::new(storage, registry, log_store));
1918 let security = SecurityLayer::from_env().unwrap_or_else(|_| SecurityLayer::dev_mode());
1919 let server = MacpServer::new(runtime, security);
1920
1921 let req = Request::new(RegisterExtModeRequest {
1922 mode_descriptor: Some(crate::pb::ModeDescriptor {
1923 mode: "ext.custom.v1".into(),
1924 mode_version: "1.0.0".into(),
1925 message_types: vec!["SessionStart".into(), "Commitment".into()],
1926 ..Default::default()
1927 }),
1928 });
1929 let err = server.register_ext_mode(req).await.unwrap_err();
1930 assert_eq!(err.code(), tonic::Code::Unauthenticated);
1931 }
1932
1933 fn stream_identity(sender: &str) -> AuthIdentity {
1934 AuthIdentity {
1935 sender: sender.into(),
1936 allowed_modes: None,
1937 can_start_sessions: true,
1938 max_open_sessions: None,
1939 can_manage_mode_registry: false,
1940 is_observer: false,
1941 }
1942 }
1943
1944 #[tokio::test]
1945 async fn stream_session_emits_accepted_envelopes_only() {
1946 use tokio_stream::{iter, StreamExt};
1947
1948 let (server, _) = make_server();
1949 let sid = new_sid();
1950 let requests = iter(vec![Ok(StreamSessionRequest {
1951 subscribe_session_id: String::new(),
1952 after_sequence: 0,
1953 envelope: Some(Envelope {
1954 macp_version: "1.0".into(),
1955 mode: "macp.mode.decision.v1".into(),
1956 message_type: "SessionStart".into(),
1957 message_id: "m1".into(),
1958 session_id: sid.clone(),
1959 sender: String::new(),
1960 timestamp_unix_ms: Utc::now().timestamp_millis(),
1961 payload: start_payload(),
1962 }),
1963 })]);
1964
1965 let mut stream =
1966 server.build_stream_session_stream(stream_identity("agent://orchestrator"), requests);
1967
1968 let response = stream.next().await.unwrap().unwrap();
1969 let envelope = match response.response.unwrap() {
1970 crate::pb::stream_session_response::Response::Envelope(e) => e,
1971 _ => panic!("expected envelope"),
1972 };
1973 assert_eq!(envelope.message_type, "SessionStart");
1974 assert_eq!(envelope.message_id, "m1");
1975 assert!(stream.next().await.is_none());
1976 }
1977
1978 #[tokio::test]
1979 async fn stream_session_rejects_mixed_session_ids() {
1980 use tokio_stream::{iter, StreamExt};
1981
1982 let (server, _) = make_server();
1983 let sid1 = new_sid();
1984 let sid2 = new_sid();
1985 let requests = iter(vec![
1986 Ok(StreamSessionRequest {
1987 subscribe_session_id: String::new(),
1988 after_sequence: 0,
1989 envelope: Some(Envelope {
1990 macp_version: "1.0".into(),
1991 mode: "macp.mode.decision.v1".into(),
1992 message_type: "SessionStart".into(),
1993 message_id: "m1".into(),
1994 session_id: sid1.clone(),
1995 sender: String::new(),
1996 timestamp_unix_ms: Utc::now().timestamp_millis(),
1997 payload: start_payload(),
1998 }),
1999 }),
2000 Ok(StreamSessionRequest {
2001 subscribe_session_id: String::new(),
2002 after_sequence: 0,
2003 envelope: Some(Envelope {
2004 macp_version: "1.0".into(),
2005 mode: "macp.mode.decision.v1".into(),
2006 message_type: "SessionStart".into(),
2007 message_id: "m2".into(),
2008 session_id: sid2,
2009 sender: String::new(),
2010 timestamp_unix_ms: Utc::now().timestamp_millis(),
2011 payload: start_payload(),
2012 }),
2013 }),
2014 ]);
2015
2016 let mut stream =
2017 server.build_stream_session_stream(stream_identity("agent://orchestrator"), requests);
2018
2019 let first = stream.next().await.unwrap().unwrap();
2020 let first_env = match first.response.unwrap() {
2021 crate::pb::stream_session_response::Response::Envelope(e) => e,
2022 _ => panic!("expected envelope"),
2023 };
2024 assert_eq!(first_env.session_id, sid1);
2025 let err = stream.next().await.unwrap().unwrap_err();
2026 assert_eq!(err.code(), tonic::Code::InvalidArgument);
2027 }
2028
2029 #[tokio::test]
2030 async fn list_modes_returns_standard_modes() {
2031 let (server, _) = make_server();
2032 let resp = server
2033 .list_modes(Request::new(ListModesRequest {}))
2034 .await
2035 .unwrap();
2036 let names: Vec<String> = resp
2037 .into_inner()
2038 .modes
2039 .iter()
2040 .map(|m| m.mode.clone())
2041 .collect();
2042 assert_eq!(names.len(), 5);
2043 assert!(names.contains(&"macp.mode.decision.v1".to_string()));
2044 assert!(names.contains(&"macp.mode.proposal.v1".to_string()));
2045 assert!(names.contains(&"macp.mode.task.v1".to_string()));
2046 assert!(names.contains(&"macp.mode.handoff.v1".to_string()));
2047 assert!(names.contains(&"macp.mode.quorum.v1".to_string()));
2048 assert!(!names.contains(&"ext.multi_round.v1".to_string()));
2050 }
2051
2052 #[tokio::test]
2053 async fn list_ext_modes_returns_extensions() {
2054 let (server, _) = make_server();
2055 let resp = server
2056 .list_ext_modes(Request::new(ListExtModesRequest {}))
2057 .await
2058 .unwrap();
2059 let names: Vec<String> = resp
2060 .into_inner()
2061 .modes
2062 .iter()
2063 .map(|m| m.mode.clone())
2064 .collect();
2065 assert_eq!(names.len(), 1);
2066 assert!(names.contains(&"ext.multi_round.v1".to_string()));
2067 }
2068
2069 #[tokio::test]
2070 async fn get_manifest_includes_all_modes() {
2071 let (server, _) = make_server();
2072 let resp = server
2073 .get_manifest(Request::new(crate::pb::GetManifestRequest {
2074 agent_id: String::new(),
2075 }))
2076 .await
2077 .unwrap();
2078 let manifest = resp.into_inner().manifest.unwrap();
2079 assert_eq!(manifest.supported_modes.len(), 6);
2080 assert!(manifest
2081 .supported_modes
2082 .contains(&"ext.multi_round.v1".to_string()));
2083 }
2084
2085 #[tokio::test]
2086 async fn get_session_returns_metadata() {
2087 let (server, _) = make_server();
2088 let sid = new_sid();
2089 let ack = do_send(
2090 &server,
2091 "agent://orchestrator",
2092 Envelope {
2093 macp_version: "1.0".into(),
2094 mode: "macp.mode.decision.v1".into(),
2095 message_type: "SessionStart".into(),
2096 message_id: "m1".into(),
2097 session_id: sid.clone(),
2098 sender: String::new(),
2099 timestamp_unix_ms: Utc::now().timestamp_millis(),
2100 payload: start_payload(),
2101 },
2102 )
2103 .await;
2104 assert!(ack.ok);
2105
2106 let mut req = Request::new(GetSessionRequest {
2107 session_id: sid.clone(),
2108 });
2109 req.metadata_mut().insert(
2110 "authorization",
2111 format!("Bearer {}", "agent://orchestrator")
2112 .parse()
2113 .unwrap(),
2114 );
2115 let resp = server.get_session(req).await.unwrap();
2116 let meta = resp.into_inner().metadata.unwrap();
2117 assert_eq!(meta.session_id, sid);
2118 assert_eq!(meta.mode, "macp.mode.decision.v1");
2119 assert_eq!(meta.mode_version, "1.0.0");
2120 assert_eq!(meta.configuration_version, "cfg-1");
2121 }
2122
2123 #[tokio::test]
2124 async fn cancel_session_transitions_to_cancelled() {
2125 let (server, _) = make_server();
2126 let sid = new_sid();
2127 let ack = do_send(
2128 &server,
2129 "agent://orchestrator",
2130 Envelope {
2131 macp_version: "1.0".into(),
2132 mode: "macp.mode.decision.v1".into(),
2133 message_type: "SessionStart".into(),
2134 message_id: "m1".into(),
2135 session_id: sid.clone(),
2136 sender: String::new(),
2137 timestamp_unix_ms: Utc::now().timestamp_millis(),
2138 payload: start_payload(),
2139 },
2140 )
2141 .await;
2142 assert!(ack.ok);
2143
2144 let mut req = Request::new(CancelSessionRequest {
2145 session_id: sid,
2146 reason: "no longer needed".into(),
2147 });
2148 req.metadata_mut().insert(
2149 "authorization",
2150 format!("Bearer {}", "agent://orchestrator")
2151 .parse()
2152 .unwrap(),
2153 );
2154 let resp = server.cancel_session(req).await.unwrap();
2155 let ack = resp.into_inner().ack.unwrap();
2156 assert!(ack.ok);
2157 assert_eq!(ack.session_state, PbSessionState::Cancelled as i32);
2159 }
2160
2161 #[tokio::test]
2162 async fn participant_cannot_cancel_session() {
2163 let (server, _) = make_server();
2164 let sid = new_sid();
2165 let ack = do_send(
2166 &server,
2167 "agent://orchestrator",
2168 Envelope {
2169 macp_version: "1.0".into(),
2170 mode: "macp.mode.decision.v1".into(),
2171 message_type: "SessionStart".into(),
2172 message_id: "m1".into(),
2173 session_id: sid.clone(),
2174 sender: String::new(),
2175 timestamp_unix_ms: Utc::now().timestamp_millis(),
2176 payload: start_payload(),
2177 },
2178 )
2179 .await;
2180 assert!(ack.ok);
2181
2182 let mut req = Request::new(CancelSessionRequest {
2183 session_id: sid,
2184 reason: "I want to cancel".into(),
2185 });
2186 req.metadata_mut().insert(
2187 "authorization",
2188 format!("Bearer {}", "agent://fraud").parse().unwrap(),
2189 );
2190 let err = server.cancel_session(req).await.unwrap_err();
2191 assert_eq!(err.code(), tonic::Code::PermissionDenied);
2192 }
2193
2194 #[tokio::test]
2195 async fn cancel_session_unknown_session_returns_error() {
2196 let (server, _) = make_server();
2197 let mut req = Request::new(CancelSessionRequest {
2198 session_id: "nonexistent".into(),
2199 reason: "test".into(),
2200 });
2201 req.metadata_mut().insert(
2202 "authorization",
2203 format!("Bearer {}", "agent://orchestrator")
2204 .parse()
2205 .unwrap(),
2206 );
2207 let err = server.cancel_session(req).await.unwrap_err();
2208 assert_eq!(err.code(), tonic::Code::NotFound);
2209 }
2210
2211 #[tokio::test]
2212 async fn ambient_signal_accepted() {
2213 let (server, _) = make_server();
2214 let ack = do_send(
2215 &server,
2216 "agent://orchestrator",
2217 Envelope {
2218 macp_version: "1.0".into(),
2219 mode: String::new(),
2220 message_type: "Signal".into(),
2221 message_id: "sig-1".into(),
2222 session_id: String::new(),
2223 sender: String::new(),
2224 timestamp_unix_ms: Utc::now().timestamp_millis(),
2225 payload: vec![],
2226 },
2227 )
2228 .await;
2229 assert!(ack.ok);
2230 }
2231
2232 #[tokio::test]
2233 async fn signal_with_session_id_rejected() {
2234 let (server, _) = make_server();
2235 let ack = do_send(
2236 &server,
2237 "agent://orchestrator",
2238 Envelope {
2239 macp_version: "1.0".into(),
2240 mode: String::new(),
2241 message_type: "Signal".into(),
2242 message_id: "sig-2".into(),
2243 session_id: "some-session".into(),
2244 sender: String::new(),
2245 timestamp_unix_ms: Utc::now().timestamp_millis(),
2246 payload: vec![],
2247 },
2248 )
2249 .await;
2250 assert!(!ack.ok);
2251 assert_eq!(ack.error.as_ref().unwrap().code, "INVALID_ENVELOPE");
2252 }
2253
2254 #[tokio::test]
2255 async fn signal_with_mode_rejected() {
2256 let (server, _) = make_server();
2257 let ack = do_send(
2258 &server,
2259 "agent://orchestrator",
2260 Envelope {
2261 macp_version: "1.0".into(),
2262 mode: "macp.mode.decision.v1".into(),
2263 message_type: "Signal".into(),
2264 message_id: "sig-3".into(),
2265 session_id: String::new(),
2266 sender: String::new(),
2267 timestamp_unix_ms: Utc::now().timestamp_millis(),
2268 payload: vec![],
2269 },
2270 )
2271 .await;
2272 assert!(!ack.ok);
2273 assert_eq!(ack.error.as_ref().unwrap().code, "INVALID_ENVELOPE");
2274 }
2275
2276 #[tokio::test]
2277 async fn ambient_progress_accepted() {
2278 let (server, _) = make_server();
2279 let ack = do_send(
2280 &server,
2281 "agent://orchestrator",
2282 Envelope {
2283 macp_version: "1.0".into(),
2284 mode: String::new(),
2285 message_type: "Progress".into(),
2286 message_id: "prog-1".into(),
2287 session_id: String::new(),
2288 sender: String::new(),
2289 timestamp_unix_ms: Utc::now().timestamp_millis(),
2290 payload: vec![],
2291 },
2292 )
2293 .await;
2294 assert!(ack.ok);
2295 }
2296
2297 #[tokio::test]
2298 async fn ambient_progress_with_mode_rejected() {
2299 let (server, _) = make_server();
2300 let ack = do_send(
2301 &server,
2302 "agent://orchestrator",
2303 Envelope {
2304 macp_version: "1.0".into(),
2305 mode: "macp.mode.decision.v1".into(),
2306 message_type: "Progress".into(),
2307 message_id: "prog-2".into(),
2308 session_id: String::new(),
2309 sender: String::new(),
2310 timestamp_unix_ms: Utc::now().timestamp_millis(),
2311 payload: vec![],
2312 },
2313 )
2314 .await;
2315 assert!(!ack.ok);
2316 assert_eq!(ack.error.as_ref().unwrap().code, "INVALID_ENVELOPE");
2317 }
2318
2319 #[tokio::test]
2320 async fn manifest_advertises_stream_enabled() {
2321 let (server, _) = make_server();
2322 let resp = server
2323 .initialize(Request::new(InitializeRequest {
2324 supported_protocol_versions: vec!["1.0".into()],
2325 client_info: None,
2326 capabilities: None,
2327 }))
2328 .await
2329 .unwrap();
2330 let caps = resp.into_inner().capabilities.unwrap();
2331 assert!(caps.sessions.unwrap().stream);
2332 }
2333
2334 #[tokio::test]
2335 async fn initialize_empty_versions_rejected() {
2336 let (server, _) = make_server();
2337 let err = server
2338 .initialize(Request::new(InitializeRequest {
2339 supported_protocol_versions: vec![],
2340 client_info: None,
2341 capabilities: None,
2342 }))
2343 .await
2344 .unwrap_err();
2345 assert_eq!(err.code(), tonic::Code::InvalidArgument);
2346 }
2347
2348 #[tokio::test]
2349 async fn initialize_unsupported_version_rejected() {
2350 let (server, _) = make_server();
2351 let err = server
2352 .initialize(Request::new(InitializeRequest {
2353 supported_protocol_versions: vec!["2.0".into()],
2354 client_info: None,
2355 capabilities: None,
2356 }))
2357 .await
2358 .unwrap_err();
2359 assert_eq!(err.code(), tonic::Code::FailedPrecondition);
2360 }
2361
2362 #[tokio::test]
2367 async fn initialize_instructions_do_not_advertise_removed_header() {
2368 let (server, _) = make_server();
2369 let resp = server
2370 .initialize(Request::new(InitializeRequest {
2371 supported_protocol_versions: vec!["1.0".into()],
2372 client_info: None,
2373 capabilities: None,
2374 }))
2375 .await
2376 .unwrap();
2377 let instructions = resp.into_inner().instructions;
2378 assert!(
2379 !instructions.contains("x-macp-agent-id"),
2380 "instructions must not advertise the removed x-macp-agent-id header: {instructions:?}"
2381 );
2382 assert!(
2383 instructions.contains("MACP_ALLOW_INSECURE"),
2384 "instructions must describe the real dev-mode auth path: {instructions:?}"
2385 );
2386 }
2387
2388 fn observer_identity(sender: &str) -> AuthIdentity {
2391 AuthIdentity {
2392 sender: sender.into(),
2393 allowed_modes: None,
2394 can_start_sessions: false,
2395 max_open_sessions: None,
2396 can_manage_mode_registry: false,
2397 is_observer: true,
2398 }
2399 }
2400
2401 fn subscribe_frame(session_id: &str, after: u64) -> StreamSessionRequest {
2402 StreamSessionRequest {
2403 subscribe_session_id: session_id.into(),
2404 after_sequence: after,
2405 envelope: None,
2406 }
2407 }
2408
2409 fn start_multi_participant(participants: Vec<String>) -> Vec<u8> {
2410 SessionStartPayload {
2411 intent: "intent".into(),
2412 participants,
2413 mode_version: "1.0.0".into(),
2414 configuration_version: "cfg-1".into(),
2415 policy_version: String::new(),
2416 ttl_ms: 60_000,
2417 context_id: String::new(),
2418 extensions: std::collections::HashMap::new(),
2419 roots: vec![],
2420 max_suspend_ms: 0,
2421 }
2422 .encode_to_vec()
2423 }
2424
2425 async fn start_session(
2426 server: &MacpServer,
2427 initiator: &str,
2428 sid: &str,
2429 participants: Vec<String>,
2430 ) {
2431 let ack = do_send(
2432 server,
2433 initiator,
2434 Envelope {
2435 macp_version: "1.0".into(),
2436 mode: "macp.mode.decision.v1".into(),
2437 message_type: "SessionStart".into(),
2438 message_id: "start".into(),
2439 session_id: sid.into(),
2440 sender: String::new(),
2441 timestamp_unix_ms: Utc::now().timestamp_millis(),
2442 payload: start_multi_participant(participants),
2443 },
2444 )
2445 .await;
2446 assert!(ack.ok, "SessionStart failed: {:?}", ack.error);
2447 }
2448
2449 async fn send_proposal(
2450 server: &MacpServer,
2451 sender: &str,
2452 sid: &str,
2453 message_id: &str,
2454 proposal_id: &str,
2455 ) {
2456 let payload = crate::decision_pb::ProposalPayload {
2457 proposal_id: proposal_id.into(),
2458 option: "opt".into(),
2459 rationale: "r".into(),
2460 supporting_data: vec![],
2461 }
2462 .encode_to_vec();
2463 let ack = do_send(
2464 server,
2465 sender,
2466 Envelope {
2467 macp_version: "1.0".into(),
2468 mode: "macp.mode.decision.v1".into(),
2469 message_type: "Proposal".into(),
2470 message_id: message_id.into(),
2471 session_id: sid.into(),
2472 sender: String::new(),
2473 timestamp_unix_ms: Utc::now().timestamp_millis(),
2474 payload,
2475 },
2476 )
2477 .await;
2478 assert!(ack.ok, "Proposal failed: {:?}", ack.error);
2479 }
2480
2481 #[tokio::test]
2482 async fn subscribe_replays_session_history_from_zero() {
2483 let (server, _) = make_server();
2484 let sid = new_sid();
2485 let initiator = "agent://orchestrator";
2486 let peer = "agent://fraud";
2487 start_session(
2488 &server,
2489 initiator,
2490 &sid,
2491 vec![initiator.into(), peer.into()],
2492 )
2493 .await;
2494 send_proposal(&server, peer, &sid, "m2", "p1").await;
2495
2496 let mut bound = None;
2497 let mut events = None;
2498 let replay = server
2499 .process_stream_request(
2500 &stream_identity(peer),
2501 subscribe_frame(&sid, 0),
2502 &mut bound,
2503 &mut events,
2504 )
2505 .await
2506 .unwrap();
2507
2508 assert_eq!(replay.len(), 2);
2509 assert_eq!(replay[0].message_type, "SessionStart");
2510 assert_eq!(replay[0].message_id, "start");
2511 assert_eq!(replay[1].message_type, "Proposal");
2512 assert_eq!(replay[1].message_id, "m2");
2513 assert_eq!(bound.as_deref(), Some(sid.as_str()));
2514 assert!(events.is_some());
2515 }
2516
2517 #[tokio::test]
2518 async fn subscribe_after_sequence_filters_history() {
2519 let (server, _) = make_server();
2520 let sid = new_sid();
2521 let initiator = "agent://orchestrator";
2522 let peer = "agent://fraud";
2523 start_session(
2524 &server,
2525 initiator,
2526 &sid,
2527 vec![initiator.into(), peer.into()],
2528 )
2529 .await;
2530 send_proposal(&server, peer, &sid, "m2", "p1").await;
2531 send_proposal(&server, peer, &sid, "m3", "p2").await;
2532
2533 let mut bound = None;
2534 let mut events = None;
2535 let replay = server
2536 .process_stream_request(
2537 &stream_identity(peer),
2538 subscribe_frame(&sid, 2),
2539 &mut bound,
2540 &mut events,
2541 )
2542 .await
2543 .unwrap();
2544
2545 assert_eq!(replay.len(), 1);
2546 assert_eq!(replay[0].message_id, "m3");
2547 }
2548
2549 #[tokio::test]
2550 async fn subscribe_unknown_session_returns_not_found() {
2551 let (server, _) = make_server();
2552 let mut bound = None;
2553 let mut events = None;
2554 let status = server
2555 .process_stream_request(
2556 &stream_identity("agent://orchestrator"),
2557 subscribe_frame("missing-session", 0),
2558 &mut bound,
2559 &mut events,
2560 )
2561 .await
2562 .unwrap_err();
2563 assert_eq!(status.code(), tonic::Code::NotFound);
2564 assert!(bound.is_none());
2565 assert!(events.is_none());
2566 }
2567
2568 #[tokio::test]
2569 async fn subscribe_non_participant_is_forbidden() {
2570 let (server, _) = make_server();
2571 let sid = new_sid();
2572 start_session(
2573 &server,
2574 "agent://orchestrator",
2575 &sid,
2576 vec!["agent://orchestrator".into(), "agent://fraud".into()],
2577 )
2578 .await;
2579
2580 let mut bound = None;
2581 let mut events = None;
2582 let status = server
2583 .process_stream_request(
2584 &stream_identity("agent://outsider"),
2585 subscribe_frame(&sid, 0),
2586 &mut bound,
2587 &mut events,
2588 )
2589 .await
2590 .unwrap_err();
2591 assert_eq!(status.code(), tonic::Code::PermissionDenied);
2592 }
2593
2594 #[tokio::test]
2595 async fn subscribe_observer_identity_allowed() {
2596 let (server, _) = make_server();
2597 let sid = new_sid();
2598 start_session(
2599 &server,
2600 "agent://orchestrator",
2601 &sid,
2602 vec!["agent://orchestrator".into(), "agent://fraud".into()],
2603 )
2604 .await;
2605
2606 let mut bound = None;
2607 let mut events = None;
2608 let replay = server
2609 .process_stream_request(
2610 &observer_identity("agent://auditor"),
2611 subscribe_frame(&sid, 0),
2612 &mut bound,
2613 &mut events,
2614 )
2615 .await
2616 .unwrap();
2617 assert_eq!(replay.len(), 1);
2618 assert_eq!(replay[0].message_type, "SessionStart");
2619 }
2620
2621 #[tokio::test]
2622 async fn subscribe_initiator_allowed_even_when_not_listed() {
2623 let (server, _) = make_server();
2626 let sid = new_sid();
2627 start_session(
2628 &server,
2629 "agent://orchestrator",
2630 &sid,
2631 vec!["agent://fraud".into()],
2632 )
2633 .await;
2634
2635 let mut bound = None;
2636 let mut events = None;
2637 let replay = server
2638 .process_stream_request(
2639 &stream_identity("agent://orchestrator"),
2640 subscribe_frame(&sid, 0),
2641 &mut bound,
2642 &mut events,
2643 )
2644 .await
2645 .unwrap();
2646 assert_eq!(replay.len(), 1);
2647 }
2648
2649 #[tokio::test]
2650 async fn stream_request_with_envelope_and_subscribe_is_rejected() {
2651 let (server, _) = make_server();
2652 let sid = new_sid();
2653 let req = StreamSessionRequest {
2654 subscribe_session_id: sid.clone(),
2655 after_sequence: 0,
2656 envelope: Some(Envelope {
2657 macp_version: "1.0".into(),
2658 mode: "macp.mode.decision.v1".into(),
2659 message_type: "SessionStart".into(),
2660 message_id: "m1".into(),
2661 session_id: sid,
2662 sender: String::new(),
2663 timestamp_unix_ms: Utc::now().timestamp_millis(),
2664 payload: start_payload(),
2665 }),
2666 };
2667
2668 let mut bound = None;
2669 let mut events = None;
2670 let status = server
2671 .process_stream_request(
2672 &stream_identity("agent://orchestrator"),
2673 req,
2674 &mut bound,
2675 &mut events,
2676 )
2677 .await
2678 .unwrap_err();
2679 assert_eq!(status.code(), tonic::Code::InvalidArgument);
2680 }
2681
2682 #[tokio::test]
2683 async fn subscribe_to_different_session_on_bound_stream_is_rejected() {
2684 let (server, _) = make_server();
2685 let sid1 = new_sid();
2686 let sid2 = new_sid();
2687 start_session(
2688 &server,
2689 "agent://orchestrator",
2690 &sid1,
2691 vec!["agent://orchestrator".into(), "agent://fraud".into()],
2692 )
2693 .await;
2694 start_session(
2695 &server,
2696 "agent://orchestrator",
2697 &sid2,
2698 vec!["agent://orchestrator".into(), "agent://fraud".into()],
2699 )
2700 .await;
2701
2702 let identity = stream_identity("agent://fraud");
2704 let mut bound = None;
2705 let mut events = None;
2706 server
2707 .process_stream_request(
2708 &identity,
2709 subscribe_frame(&sid1, 0),
2710 &mut bound,
2711 &mut events,
2712 )
2713 .await
2714 .unwrap();
2715 assert_eq!(bound.as_deref(), Some(sid1.as_str()));
2716
2717 let status = server
2719 .process_stream_request(
2720 &identity,
2721 subscribe_frame(&sid2, 0),
2722 &mut bound,
2723 &mut events,
2724 )
2725 .await
2726 .unwrap_err();
2727 assert_eq!(status.code(), tonic::Code::InvalidArgument);
2728 }
2729
2730 struct DenySenderEngine {
2734 denied: String,
2735 }
2736
2737 #[async_trait::async_trait]
2738 impl crate::policy_engine::PolicyEngine for DenySenderEngine {
2739 async fn evaluate_session_start(
2740 &self,
2741 identity: &crate::security::AuthIdentity,
2742 _mode: &str,
2743 _env: &Envelope,
2744 ) -> macp_core::policy::PolicyDecision {
2745 if identity.sender == self.denied {
2746 macp_core::policy::PolicyDecision::Deny {
2747 reasons: vec!["sender embargoed".into()],
2748 }
2749 } else {
2750 macp_core::policy::PolicyDecision::Allow { reasons: vec![] }
2751 }
2752 }
2753
2754 async fn evaluate_message(
2755 &self,
2756 identity: &crate::security::AuthIdentity,
2757 _session: &macp_core::session::Session,
2758 _env: &Envelope,
2759 ) -> macp_core::policy::PolicyDecision {
2760 if identity.sender == self.denied {
2761 macp_core::policy::PolicyDecision::Deny {
2762 reasons: vec!["sender embargoed".into()],
2763 }
2764 } else {
2765 macp_core::policy::PolicyDecision::Allow { reasons: vec![] }
2766 }
2767 }
2768
2769 async fn evaluate_session_access(
2770 &self,
2771 identity: &crate::security::AuthIdentity,
2772 _session: &macp_core::session::Session,
2773 ) -> macp_core::policy::PolicyDecision {
2774 if identity.sender == self.denied {
2775 macp_core::policy::PolicyDecision::Deny {
2776 reasons: vec!["sender embargoed".into()],
2777 }
2778 } else {
2779 macp_core::policy::PolicyDecision::Allow { reasons: vec![] }
2780 }
2781 }
2782 }
2783
2784 #[tokio::test]
2785 async fn policy_engine_gates_all_three_ingress_points() {
2786 let (server, _runtime) = make_server();
2787 let server = server.with_policy_engine(Arc::new(DenySenderEngine {
2788 denied: "agent://embargoed".into(),
2789 }));
2790
2791 let sid = new_sid();
2792 let start_payload = SessionStartPayload {
2793 intent: "e3".into(),
2794 participants: vec!["agent://ok".into(), "agent://embargoed".into()],
2795 mode_version: "1.0.0".into(),
2796 configuration_version: "cfg-1".into(),
2797 policy_version: String::new(),
2798 ttl_ms: 60_000,
2799 context_id: String::new(),
2800 extensions: Default::default(),
2801 roots: vec![],
2802 max_suspend_ms: 0,
2803 }
2804 .encode_to_vec();
2805 let start_env = |sender: &str, sid: &str| Envelope {
2806 macp_version: "1.0".into(),
2807 mode: "macp.mode.decision.v1".into(),
2808 message_type: "SessionStart".into(),
2809 message_id: new_sid(),
2810 session_id: sid.into(),
2811 sender: sender.into(),
2812 timestamp_unix_ms: Utc::now().timestamp_millis(),
2813 payload: start_payload.clone(),
2814 };
2815
2816 let ack = server
2818 .send(send_req(
2819 "agent://embargoed",
2820 start_env("agent://embargoed", &sid),
2821 ))
2822 .await
2823 .unwrap()
2824 .into_inner()
2825 .ack
2826 .unwrap();
2827 assert!(!ack.ok);
2828 assert_eq!(ack.error.unwrap().code, "POLICY_DENIED");
2829
2830 let ack = server
2832 .send(send_req("agent://ok", start_env("agent://ok", &sid)))
2833 .await
2834 .unwrap()
2835 .into_inner()
2836 .ack
2837 .unwrap();
2838 assert!(ack.ok, "allowed sender must start: {:?}", ack.error);
2839
2840 let proposal = crate::decision_pb::ProposalPayload {
2842 proposal_id: "p1".into(),
2843 option: "x".into(),
2844 rationale: "r".into(),
2845 supporting_data: vec![],
2846 }
2847 .encode_to_vec();
2848 let msg_env = Envelope {
2849 macp_version: "1.0".into(),
2850 mode: "macp.mode.decision.v1".into(),
2851 message_type: "Proposal".into(),
2852 message_id: new_sid(),
2853 session_id: sid.clone(),
2854 sender: "agent://embargoed".into(),
2855 timestamp_unix_ms: Utc::now().timestamp_millis(),
2856 payload: proposal,
2857 };
2858 let ack = server
2859 .send(send_req("agent://embargoed", msg_env))
2860 .await
2861 .unwrap()
2862 .into_inner()
2863 .ack
2864 .unwrap();
2865 assert!(!ack.ok);
2866 assert_eq!(ack.error.unwrap().code, "POLICY_DENIED");
2867
2868 let mut req = Request::new(crate::pb::GetSessionRequest {
2870 session_id: sid.clone(),
2871 });
2872 req.metadata_mut()
2873 .insert("authorization", "Bearer agent://embargoed".parse().unwrap());
2874 let err = server
2875 .get_session(req)
2876 .await
2877 .expect_err("embargoed read must be denied");
2878 assert_eq!(err.code(), tonic::Code::PermissionDenied);
2879 }
2880
2881 #[tokio::test]
2885 async fn policy_engine_gates_stream_path() {
2886 let (server, runtime) = make_server();
2887 let server = server.with_policy_engine(Arc::new(DenySenderEngine {
2888 denied: "agent://embargoed".into(),
2889 }));
2890
2891 let sid = new_sid();
2895 let payload = SessionStartPayload {
2896 intent: "e3-stream".into(),
2897 participants: vec!["agent://ok".into(), "agent://embargoed".into()],
2898 mode_version: "1.0.0".into(),
2899 configuration_version: "cfg-1".into(),
2900 policy_version: String::new(),
2901 ttl_ms: 60_000,
2902 context_id: String::new(),
2903 extensions: Default::default(),
2904 roots: vec![],
2905 max_suspend_ms: 0,
2906 }
2907 .encode_to_vec();
2908 runtime
2909 .process(
2910 &Envelope {
2911 macp_version: "1.0".into(),
2912 mode: "macp.mode.decision.v1".into(),
2913 message_type: "SessionStart".into(),
2914 message_id: new_sid(),
2915 session_id: sid.clone(),
2916 sender: "agent://ok".into(),
2917 timestamp_unix_ms: Utc::now().timestamp_millis(),
2918 payload,
2919 },
2920 None,
2921 )
2922 .await
2923 .unwrap();
2924
2925 let embargoed = crate::security::AuthIdentity {
2926 sender: "agent://embargoed".into(),
2927 allowed_modes: None,
2928 can_start_sessions: true,
2929 max_open_sessions: None,
2930 can_manage_mode_registry: false,
2931 is_observer: false,
2932 };
2933 let mut bound = None;
2934 let mut events = None;
2935
2936 let proposal = crate::decision_pb::ProposalPayload {
2938 proposal_id: "p1".into(),
2939 option: "x".into(),
2940 rationale: "r".into(),
2941 supporting_data: vec![],
2942 }
2943 .encode_to_vec();
2944 let req = StreamSessionRequest {
2945 envelope: Some(Envelope {
2946 macp_version: "1.0".into(),
2947 mode: "macp.mode.decision.v1".into(),
2948 message_type: "Proposal".into(),
2949 message_id: new_sid(),
2950 session_id: sid.clone(),
2951 sender: "agent://embargoed".into(),
2952 timestamp_unix_ms: Utc::now().timestamp_millis(),
2953 payload: proposal,
2954 }),
2955 subscribe_session_id: String::new(),
2956 after_sequence: 0,
2957 };
2958 let err = server
2959 .process_stream_request(&embargoed, req, &mut bound, &mut events)
2960 .await
2961 .expect_err("stream envelope from embargoed sender must be denied");
2962 assert_eq!(err.code(), tonic::Code::FailedPrecondition, "{err:?}");
2965 assert!(err.message().contains("PolicyDenied"), "{err:?}");
2966
2967 let req = StreamSessionRequest {
2970 envelope: None,
2971 subscribe_session_id: sid.clone(),
2972 after_sequence: 0,
2973 };
2974 let err = server
2975 .process_stream_request(&embargoed, req, &mut bound, &mut events)
2976 .await
2977 .expect_err("stream subscribe from embargoed sender must be denied");
2978 assert_eq!(err.code(), tonic::Code::PermissionDenied, "{err:?}");
2979 }
2980 fn paged_session(id: &str) -> crate::session::Session {
2983 crate::session::Session::builder(id, "macp.mode.decision.v1", "agent://initiator")
2984 .participants(vec!["agent://a".into()])
2985 .mode_version("1.0.0")
2986 .configuration_version("cfg-1")
2987 .started_at_unix_ms(1)
2988 .build()
2989 }
2990
2991 async fn seed_sessions(runtime: &Arc<Runtime>, ids: &[String]) {
2995 for id in ids {
2996 runtime
2997 .registry
2998 .insert_recovered_session(id.clone(), paged_session(id))
2999 .await;
3000 }
3001 }
3002
3003 fn list_sessions_req(page_size: i32, page_token: &str) -> Request<ListSessionsRequest> {
3004 let mut req = Request::new(ListSessionsRequest {
3005 page_size,
3006 page_token: page_token.to_string(),
3007 });
3008 req.metadata_mut()
3009 .insert("authorization", "Bearer agent://observer".parse().unwrap());
3010 req
3011 }
3012
3013 fn page_size_security(default: usize, max: usize) -> SecurityLayer {
3014 let mut security = SecurityLayer::dev_mode();
3015 security.list_sessions_default_page_size = default;
3016 security.list_sessions_max_page_size = max;
3017 security
3018 }
3019
3020 fn seed_ids(n: usize) -> Vec<String> {
3021 (0..n).map(|i| format!("session-{i:03}")).collect()
3022 }
3023
3024 #[tokio::test]
3025 async fn list_sessions_applies_default_page_size_when_zero() {
3026 let (server, runtime) = make_server_with_security(page_size_security(3, 1000));
3027 seed_sessions(&runtime, &seed_ids(10)).await;
3028
3029 let resp = server
3030 .list_sessions(list_sessions_req(0, ""))
3031 .await
3032 .unwrap()
3033 .into_inner();
3034 assert_eq!(resp.sessions.len(), 3);
3035 assert!(!resp.next_page_token.is_empty());
3036 }
3037
3038 #[tokio::test]
3039 async fn list_sessions_honors_explicit_page_size() {
3040 let (server, runtime) = make_server_with_security(page_size_security(100, 1000));
3041 seed_sessions(&runtime, &seed_ids(10)).await;
3042
3043 let resp = server
3044 .list_sessions(list_sessions_req(4, ""))
3045 .await
3046 .unwrap()
3047 .into_inner();
3048 assert_eq!(resp.sessions.len(), 4);
3049 assert!(!resp.next_page_token.is_empty());
3050 }
3051
3052 #[tokio::test]
3053 async fn list_sessions_clamps_page_size_above_max() {
3054 let (server, runtime) = make_server_with_security(page_size_security(100, 3));
3055 seed_sessions(&runtime, &seed_ids(10)).await;
3056
3057 let resp = server
3058 .list_sessions(list_sessions_req(1000, ""))
3059 .await
3060 .unwrap()
3061 .into_inner();
3062 assert_eq!(resp.sessions.len(), 3);
3063 assert!(!resp.next_page_token.is_empty());
3064 }
3065
3066 #[tokio::test]
3067 async fn list_sessions_rejects_negative_page_size() {
3068 let (server, runtime) = make_server();
3069 seed_sessions(&runtime, &seed_ids(3)).await;
3070
3071 let err = server
3072 .list_sessions(list_sessions_req(-1, ""))
3073 .await
3074 .unwrap_err();
3075 assert_eq!(err.code(), tonic::Code::InvalidArgument, "{err:?}");
3076 assert!(err.message().contains("page_size"), "{err:?}");
3077 }
3078
3079 #[tokio::test]
3080 async fn list_sessions_rejects_garbage_page_token() {
3081 use base64::Engine;
3082 let (server, runtime) = make_server();
3083 seed_sessions(&runtime, &seed_ids(3)).await;
3084
3085 let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
3086 let valid = engine.encode("v1:session-000");
3087 let tokens = vec![
3088 "not-a-token!".to_string(),
3090 engine.encode("v2:session-000"),
3092 engine.encode("v1:"),
3094 engine.encode("v1"),
3100 valid[1..].to_string(),
3104 "A".repeat(2 * 1024 * 1024),
3106 ];
3107 for token in tokens {
3108 let err = server
3109 .list_sessions(list_sessions_req(0, &token))
3110 .await
3111 .unwrap_err();
3112 assert_eq!(err.code(), tonic::Code::InvalidArgument);
3113 assert_eq!(
3115 err.message(),
3116 "INVALID_ARGUMENT: page_token is not a valid continuation token"
3117 );
3118 }
3119 }
3120
3121 #[tokio::test]
3122 async fn list_sessions_full_traversal_visits_every_session_exactly_once() {
3123 let (server, runtime) = make_server_with_security(page_size_security(100, 1000));
3124 let ids = seed_ids(25);
3125 seed_sessions(&runtime, &ids).await;
3126
3127 let mut collected: Vec<String> = Vec::new();
3128 let mut token = String::new();
3129 for _ in 0..100 {
3130 let resp = server
3131 .list_sessions(list_sessions_req(4, &token))
3132 .await
3133 .unwrap()
3134 .into_inner();
3135 collected.extend(resp.sessions.iter().map(|s| s.session_id.clone()));
3136 token = resp.next_page_token;
3137 if token.is_empty() {
3138 break;
3139 }
3140 }
3141 assert!(token.is_empty(), "traversal did not terminate");
3142 let unique: std::collections::HashSet<&String> = collected.iter().collect();
3143 assert_eq!(unique.len(), 25, "sessions were dropped or duplicated");
3146 assert_eq!(collected.len(), 25, "sessions were duplicated");
3147 }
3148
3149 #[tokio::test]
3150 async fn list_sessions_terminal_page_has_empty_next_page_token() {
3151 let (server, runtime) = make_server_with_security(page_size_security(100, 1000));
3152 seed_sessions(&runtime, &seed_ids(10)).await;
3153
3154 let mut tokens: Vec<String> = Vec::new();
3155 let mut token = String::new();
3156 for _ in 0..20 {
3157 let resp = server
3158 .list_sessions(list_sessions_req(5, &token))
3159 .await
3160 .unwrap()
3161 .into_inner();
3162 token = resp.next_page_token;
3163 tokens.push(token.clone());
3164 if token.is_empty() {
3165 break;
3166 }
3167 }
3168 assert_eq!(tokens.len(), 2, "{tokens:?}");
3171 assert!(!tokens[0].is_empty());
3172 assert!(tokens[1].is_empty());
3173 }
3174
3175 #[tokio::test]
3176 async fn list_sessions_orders_by_session_id_ascending() {
3177 let (server, runtime) = make_server_with_security(page_size_security(100, 1000));
3178 let ids: Vec<String> = ["delta", "alpha", "echo", "charlie", "bravo"]
3180 .iter()
3181 .map(|s| s.to_string())
3182 .collect();
3183 seed_sessions(&runtime, &ids).await;
3184
3185 let mut collected: Vec<String> = Vec::new();
3186 let mut token = String::new();
3187 loop {
3188 let resp = server
3189 .list_sessions(list_sessions_req(2, &token))
3190 .await
3191 .unwrap()
3192 .into_inner();
3193 collected.extend(resp.sessions.iter().map(|s| s.session_id.clone()));
3194 token = resp.next_page_token;
3195 if token.is_empty() {
3196 break;
3197 }
3198 }
3199 assert_eq!(
3201 collected,
3202 vec!["alpha", "bravo", "charlie", "delta", "echo"]
3203 );
3204 }
3205
3206 #[tokio::test]
3207 async fn list_sessions_still_requires_authentication() {
3208 let (server, runtime) = make_server();
3209 seed_sessions(&runtime, &seed_ids(3)).await;
3210
3211 let req = Request::new(ListSessionsRequest {
3214 page_size: -1,
3215 page_token: String::new(),
3216 });
3217 let err = server.list_sessions(req).await.unwrap_err();
3218 assert_eq!(err.code(), tonic::Code::Unauthenticated, "{err:?}");
3219 }
3220
3221 #[tokio::test]
3222 async fn list_sessions_tolerates_cursor_for_removed_session() {
3223 let (server, runtime) = make_server_with_security(page_size_security(100, 1000));
3224 let ids = seed_ids(4);
3225 seed_sessions(&runtime, &ids).await;
3226
3227 let first = server
3228 .list_sessions(list_sessions_req(1, ""))
3229 .await
3230 .unwrap()
3231 .into_inner();
3232 assert_eq!(first.sessions[0].session_id, "session-000");
3233 assert!(!first.next_page_token.is_empty());
3234
3235 runtime
3238 .registry
3239 .sessions
3240 .write()
3241 .await
3242 .remove("session-000");
3243
3244 let second = server
3245 .list_sessions(list_sessions_req(1, &first.next_page_token))
3246 .await
3247 .unwrap()
3248 .into_inner();
3249 assert_eq!(second.sessions[0].session_id, "session-001");
3250 }
3251
3252 #[tokio::test]
3253 async fn list_sessions_cursor_comes_from_the_id_list_not_the_returned_sessions() {
3254 let (server, runtime) = make_server_with_security(page_size_security(100, 1000));
3265 seed_sessions(&runtime, &seed_ids(6)).await;
3266
3267 let first = runtime.registry.get_shared("session-000").await.unwrap();
3271 let guard = first.lock().await;
3272
3273 let handler = server.list_sessions(list_sessions_req(3, ""));
3274 let mutator = async {
3275 let mut spins = 0;
3280 while Arc::strong_count(&first) < 3 {
3281 assert!(spins < 10_000, "handler never parked on the session mutex");
3282 spins += 1;
3283 tokio::task::yield_now().await;
3284 }
3285 runtime
3286 .registry
3287 .sessions
3288 .write()
3289 .await
3290 .remove("session-002");
3291 drop(guard);
3292 };
3293 let (resp, ()) = tokio::join!(handler, mutator);
3294 let resp = resp.unwrap().into_inner();
3295
3296 assert_eq!(
3298 resp.sessions.len(),
3299 2,
3300 "expected session-002 to vanish between the scan and the fetch"
3301 );
3302 assert_eq!(resp.sessions[1].session_id, "session-001");
3303 assert_eq!(
3305 crate::pagination::decode_page_token(&resp.next_page_token),
3306 Ok("session-002".to_string()),
3307 "cursor was derived from the returned sessions, not the ID list"
3308 );
3309
3310 runtime
3312 .registry
3313 .insert_recovered_session("session-002".to_string(), paged_session("session-002"))
3314 .await;
3315 let second = server
3316 .list_sessions(list_sessions_req(3, &resp.next_page_token))
3317 .await
3318 .unwrap()
3319 .into_inner();
3320 assert_eq!(
3321 second.sessions[0].session_id, "session-003",
3322 "the cursor moved backwards past an ID the page had already accounted for"
3323 );
3324 }
3325
3326 #[tokio::test]
3327 async fn list_sessions_replaying_a_token_returns_the_identical_page() {
3328 let (server, runtime) = make_server_with_security(page_size_security(100, 1000));
3329 seed_sessions(&runtime, &seed_ids(10)).await;
3330
3331 let first = server
3332 .list_sessions(list_sessions_req(3, ""))
3333 .await
3334 .unwrap()
3335 .into_inner();
3336 let token = first.next_page_token;
3337 assert!(!token.is_empty());
3338
3339 let page_a = server
3340 .list_sessions(list_sessions_req(3, &token))
3341 .await
3342 .unwrap()
3343 .into_inner();
3344 let page_b = server
3345 .list_sessions(list_sessions_req(3, &token))
3346 .await
3347 .unwrap()
3348 .into_inner();
3349
3350 let ids_a: Vec<&str> = page_a.sessions.iter().map(|s| &*s.session_id).collect();
3351 let ids_b: Vec<&str> = page_b.sessions.iter().map(|s| &*s.session_id).collect();
3352 assert_eq!(ids_a, ids_b);
3353 assert_eq!(page_a.next_page_token, page_b.next_page_token);
3354 }
3355
3356 #[tokio::test]
3357 async fn list_sessions_survives_zero_effective_page_size() {
3358 let (server, runtime) = make_server_with_security(page_size_security(0, 0));
3362 seed_sessions(&runtime, &seed_ids(3)).await;
3363
3364 let resp = server
3365 .list_sessions(list_sessions_req(0, ""))
3366 .await
3367 .unwrap()
3368 .into_inner();
3369 assert!(
3370 !resp.sessions.is_empty(),
3371 "empty page with token {:?} — the traversal terminates and ListSessions returns nothing",
3372 resp.next_page_token
3373 );
3374 assert_eq!(resp.sessions.len(), 1);
3375 assert!(!resp.next_page_token.is_empty());
3376
3377 let next = server
3379 .list_sessions(list_sessions_req(0, &resp.next_page_token))
3380 .await
3381 .unwrap()
3382 .into_inner();
3383 assert_eq!(next.sessions.len(), 1);
3384 assert_ne!(next.sessions[0].session_id, resp.sessions[0].session_id);
3385 }
3386
3387 fn watch_sessions_req(sender: &str) -> Request<WatchSessionsRequest> {
3388 let mut req = Request::new(WatchSessionsRequest {});
3389 req.metadata_mut()
3390 .insert("authorization", format!("Bearer {sender}").parse().unwrap());
3391 req
3392 }
3393
3394 async fn next_lifecycle_event(
3396 stream: &mut <MacpServer as MacpRuntimeService>::WatchSessionsStream,
3397 ) -> crate::pb::SessionLifecycleEvent {
3398 use tokio_stream::StreamExt;
3399 let resp = tokio::time::timeout(std::time::Duration::from_secs(5), stream.next())
3400 .await
3401 .expect("WatchSessions produced no event within 5s")
3402 .expect("stream ended")
3403 .expect("stream errored");
3404 resp.event.expect("event present")
3405 }
3406
3407 #[tokio::test]
3411 async fn watch_sessions_initial_sync_emits_each_session_exactly_once() {
3412 let (server, runtime) = make_server();
3413 let ids = seed_ids(24);
3414 seed_sessions(&runtime, &ids).await;
3415
3416 let mut stream = server
3417 .watch_sessions(watch_sessions_req("agent://observer"))
3418 .await
3419 .unwrap()
3420 .into_inner();
3421
3422 let mut counts: HashMap<String, usize> = HashMap::new();
3423 for _ in 0..ids.len() {
3424 let event = next_lifecycle_event(&mut stream).await;
3425 assert_eq!(
3426 event.event_type,
3427 session_lifecycle_event::EventType::Created as i32
3428 );
3429 let session = event.session.expect("initial sync always carries metadata");
3430 *counts.entry(session.session_id).or_default() += 1;
3431 }
3432 assert_eq!(counts.len(), ids.len(), "sync emitted the wrong set");
3433 for id in &ids {
3434 assert_eq!(
3435 counts.get(id).copied(),
3436 Some(1),
3437 "{id} was not emitted exactly once"
3438 );
3439 }
3440 }
3441
3442 #[tokio::test]
3452 async fn watch_sessions_subscribes_before_the_generator_is_polled() {
3453 let (server, _runtime) = make_server();
3454 let initiator = "agent://orchestrator";
3455 let sid = new_sid();
3456 start_session(&server, initiator, &sid, vec![initiator.into()]).await;
3457
3458 let mut stream = server
3459 .watch_sessions(watch_sessions_req("agent://observer"))
3460 .await
3461 .unwrap()
3462 .into_inner();
3463
3464 let mut cancel = Request::new(CancelSessionRequest {
3467 session_id: sid.clone(),
3468 reason: "test".into(),
3469 });
3470 cancel.metadata_mut().insert(
3471 "authorization",
3472 format!("Bearer {initiator}").parse().unwrap(),
3473 );
3474 let ack = server
3475 .cancel_session(cancel)
3476 .await
3477 .unwrap()
3478 .into_inner()
3479 .ack
3480 .unwrap();
3481 assert!(ack.ok);
3482
3483 let first = next_lifecycle_event(&mut stream).await;
3485 assert_eq!(
3486 first.event_type,
3487 session_lifecycle_event::EventType::Created as i32
3488 );
3489 assert_eq!(first.session.unwrap().session_id, sid);
3490
3491 let second = next_lifecycle_event(&mut stream).await;
3493 assert_eq!(
3494 second.event_type,
3495 session_lifecycle_event::EventType::Cancelled as i32,
3496 "the event published before the first poll was lost — the \
3497 subscription must be taken in the unary call"
3498 );
3499 assert_eq!(second.session.unwrap().session_id, sid);
3500 }
3501
3502 #[tokio::test]
3516 async fn watch_sessions_emits_created_once_for_synced_and_live_sessions() {
3517 let (server, _runtime) = make_server();
3518 let initiator = "agent://orchestrator";
3519 let synced_sid = new_sid();
3520
3521 let mut stream = server
3522 .watch_sessions(watch_sessions_req("agent://observer"))
3523 .await
3524 .unwrap()
3525 .into_inner();
3526
3527 start_session(&server, initiator, &synced_sid, vec![initiator.into()]).await;
3530
3531 let from_sync = next_lifecycle_event(&mut stream).await;
3532 assert_eq!(
3533 from_sync.event_type,
3534 session_lifecycle_event::EventType::Created as i32
3535 );
3536 assert_eq!(from_sync.session.unwrap().session_id, synced_sid);
3537
3538 let live_sid = new_sid();
3540 start_session(&server, initiator, &live_sid, vec![initiator.into()]).await;
3541
3542 let live = next_lifecycle_event(&mut stream).await;
3545 assert_eq!(
3546 live.event_type,
3547 session_lifecycle_event::EventType::Created as i32
3548 );
3549 assert_eq!(
3550 live.session.unwrap().session_id,
3551 live_sid,
3552 "the sync entry's buffered Created must be suppressed, and the \
3553 live session's must not be"
3554 );
3555
3556 use tokio_stream::StreamExt;
3558 let extra =
3559 tokio::time::timeout(std::time::Duration::from_millis(300), stream.next()).await;
3560 assert!(
3561 extra.is_err(),
3562 "unexpected extra lifecycle event: {extra:?}"
3563 );
3564 }
3565
3566 #[tokio::test]
3579 async fn watch_sessions_survives_a_slow_consumer_during_a_long_sync() {
3580 use tokio_stream::StreamExt;
3581
3582 let (server, runtime) = make_server();
3583 let seeded = seed_ids(200);
3584 seed_sessions(&runtime, &seeded).await;
3585
3586 let mut stream = server
3587 .watch_sessions(watch_sessions_req("agent://observer"))
3588 .await
3589 .unwrap()
3590 .into_inner();
3591
3592 let mut counts: HashMap<String, usize> = HashMap::new();
3593 async fn read(
3596 stream: &mut <MacpServer as MacpRuntimeService>::WatchSessionsStream,
3597 counts: &mut HashMap<String, usize>,
3598 ) {
3599 let resp = tokio::time::timeout(std::time::Duration::from_secs(10), stream.next())
3600 .await
3601 .expect("WatchSessions stalled")
3602 .expect("stream ended early")
3603 .expect("stream must not be terminated (RESOURCE_EXHAUSTED)");
3604 let event = resp.event.expect("event present");
3605 if event.event_type == session_lifecycle_event::EventType::Created as i32 {
3606 let session = event.session.expect("Created always carries metadata");
3607 *counts.entry(session.session_id).or_default() += 1;
3608 }
3609 }
3610
3611 read(&mut stream, &mut counts).await;
3614
3615 let initiator = "agent://orchestrator";
3619 let mut live = Vec::new();
3620 for _ in 0..70 {
3621 let sid = new_sid();
3622 start_session(&server, initiator, &sid, vec![initiator.into()]).await;
3623 live.push(sid);
3624 read(&mut stream, &mut counts).await;
3625 read(&mut stream, &mut counts).await;
3626 }
3627
3628 let expected = seeded.len() + live.len();
3630 while counts.len() < expected {
3631 read(&mut stream, &mut counts).await;
3632 }
3633
3634 for id in seeded.iter().chain(live.iter()) {
3635 assert_eq!(
3636 counts.get(id).copied(),
3637 Some(1),
3638 "{id} was not emitted exactly once"
3639 );
3640 }
3641 assert_eq!(counts.len(), expected, "unexpected extra sessions emitted");
3642 }
3643}