1pub mod admin;
3mod install;
7
8use std::collections::{HashMap, VecDeque};
9use std::sync::Arc;
10use std::time::Instant;
11
12use astrid_audit::{AuditAction, AuditOutcome, AuthorizationProof};
13use astrid_capabilities::{CapabilityCheck, PermissionError};
14use astrid_core::principal::PrincipalId;
15use astrid_events::ipc::{IpcMessage, IpcPayload};
16use astrid_events::kernel_api::{KernelRequest, KernelResponse};
17use serde::Serialize;
18use tracing::{debug, info, warn};
19
20#[cfg(test)]
21mod capability_catalog_tests;
22#[cfg(test)]
23mod connection_tracker_tests;
24
25#[must_use]
38pub(crate) fn spawn_kernel_router(kernel: Arc<crate::Kernel>) -> tokio::task::JoinHandle<()> {
39 drop(spawn_connection_tracker(Arc::clone(&kernel)));
41 drop(admin::spawn_admin_router(Arc::clone(&kernel)));
43
44 let mut receiver = kernel
50 .event_bus
51 .subscribe_topic_as("astrid.v1.request.*", "kernel_router");
52
53 tokio::spawn(async move {
54 let mut rate_limiter = ManagementRateLimiter::new();
55
56 while let Some(event) = receiver.recv().await {
57 let astrid_events::AstridEvent::Ipc { message, .. } = &*event else {
58 continue;
59 };
60
61 let IpcPayload::RawJson(val) = &message.payload else {
63 continue;
64 };
65
66 match serde_json::from_value::<KernelRequest>(val.clone()) {
67 Ok(req) => {
68 let (method, limit) = rate_limit_for_request(&req);
69 if let Some(max) = limit
70 && !rate_limiter.check(method, max)
71 {
72 warn!(
73 security_event = true,
74 method = method,
75 "Rate limited kernel management request"
76 );
77 let response_topic =
78 message.topic.replace("kernel.request.", "kernel.response.");
79 publish_response(
80 &kernel,
81 response_topic,
82 KernelResponse::Error(format!(
83 "Rate limited: max {max} {method} requests per minute"
84 )),
85 );
86 continue;
87 }
88 let caller = resolve_caller(message);
89 handle_request(&kernel, message.topic.clone(), caller, req).await;
90 },
91 Err(e) => {
92 warn!(error = %e, topic = %message.topic, "Failed to parse KernelRequest from IPC");
93 },
94 }
95 }
96 })
97}
98
99#[derive(Debug, PartialEq, Eq)]
101enum ConnectionSignal {
102 Opened,
103 Closed {
107 reason: Option<String>,
108 },
109}
110
111fn connection_signal(topic: &str, payload: &IpcPayload) -> Option<ConnectionSignal> {
124 match payload {
125 IpcPayload::Disconnect { reason } => Some(ConnectionSignal::Closed {
126 reason: reason.clone(),
127 }),
128 IpcPayload::Connect => Some(ConnectionSignal::Opened),
129 IpcPayload::RawJson(val) if topic == "client.v1.disconnect" => {
132 let reason = val.get("reason").and_then(|r| r.as_str().map(String::from));
133 Some(ConnectionSignal::Closed { reason })
134 },
135 _ if topic == "client.v1.disconnect" => Some(ConnectionSignal::Closed { reason: None }),
136 _ if topic == "client.v1.connect" => Some(ConnectionSignal::Opened),
137 _ => None,
138 }
139}
140
141fn spawn_connection_tracker(kernel: Arc<crate::Kernel>) -> tokio::task::JoinHandle<()> {
146 let mut receiver = kernel
149 .event_bus
150 .subscribe_topic_as("client.v1.*", "connection_tracker");
151
152 tokio::spawn(async move {
153 while let Some(event) = receiver.recv().await {
154 let astrid_events::AstridEvent::Ipc { message, .. } = &*event else {
155 continue;
156 };
157 let principal = message
163 .principal
164 .as_deref()
165 .and_then(|p| astrid_core::principal::PrincipalId::new(p).ok())
166 .unwrap_or_default();
167 match connection_signal(&message.topic, &message.payload) {
168 Some(ConnectionSignal::Closed { reason }) => {
169 kernel.connection_closed(&principal);
170 debug!(%principal, topic = %message.topic, ?reason, "Client disconnected");
171 },
172 Some(ConnectionSignal::Opened) => {
173 kernel.connection_opened(&principal);
174 debug!(%principal, topic = %message.topic, "New client connection accepted");
175 },
176 None => {},
177 }
178 }
179 })
180}
181
182#[expect(clippy::too_many_lines)]
183async fn handle_request(
184 kernel: &Arc<crate::Kernel>,
185 topic: String,
186 caller: PrincipalId,
187 req: KernelRequest,
188) {
189 let response_topic = if let Some(suffix) = topic.strip_prefix("astrid.v1.request.") {
190 format!("astrid.v1.response.{suffix}")
191 } else {
192 topic.clone()
193 };
194
195 let method = kernel_request_method(&req);
200 let scope = resolve_scope(&req, &caller);
201 let required_cap = required_capability(&req, scope);
202 match authorize_request(kernel, &caller, required_cap) {
203 Ok(()) => {
204 record_admin_audit(
205 kernel,
206 AdminAuditEntry {
207 caller: &caller,
208 method,
209 required_cap,
210 target_principal: None,
211 params: None,
212 authorization: AuthorizationProof::System {
213 reason: format!("policy allow: {caller} holds {required_cap}"),
214 },
215 outcome: AuditOutcome::success(),
216 },
217 );
218 },
219 Err(e) => {
220 warn!(
221 security_event = true,
222 method = method,
223 principal = %caller,
224 required = required_cap,
225 "Permission check denied admin request"
226 );
227 record_admin_audit(
228 kernel,
229 AdminAuditEntry {
230 caller: &caller,
231 method,
232 required_cap,
233 target_principal: None,
234 params: None,
235 authorization: AuthorizationProof::Denied {
236 reason: e.to_string(),
237 },
238 outcome: AuditOutcome::failure(e.to_string()),
239 },
240 );
241 publish_response(kernel, response_topic, KernelResponse::Error(e.to_string()));
242 return;
243 },
244 }
245
246 let res = match req {
247 KernelRequest::InstallCapsule { source, workspace } => {
248 info!(source = %source, workspace, "Kernel received install request");
249 install::handle_install_capsule(kernel, &source, workspace).await
250 },
251 KernelRequest::ApproveCapability {
252 request_id,
253 signature: _,
254 } => {
255 info!(request_id = %request_id, "Kernel received capability approval");
256 KernelResponse::Error("Approval logic not yet implemented in kernel router".to_string())
257 },
258 KernelRequest::ListCapsules => {
259 let reg = kernel.capsules.read().await;
260 let mut list = Vec::new();
261 for c in reg.list() {
262 list.push(c.to_string());
263 }
264 KernelResponse::Success(serde_json::json!(list))
265 },
266 KernelRequest::GetCommands => {
267 let reg = kernel.capsules.read().await;
268 let mut commands = Vec::new();
269 for c in reg.values() {
270 for cmd in &c.manifest().commands {
271 commands.push(astrid_events::kernel_api::CommandInfo {
272 name: cmd.name.clone(),
273 description: cmd
274 .description
275 .clone()
276 .unwrap_or_else(|| "No description".to_string()),
277 provider_capsule: c.id().to_string(),
278 });
279 }
280 }
281 info!(
282 count = commands.len(),
283 capsules = reg.len(),
284 "GetCommands: returning {} commands from {} capsules",
285 commands.len(),
286 reg.len()
287 );
288 KernelResponse::Commands(commands)
289 },
290 KernelRequest::ReloadCapsules => {
291 {
294 let reg = kernel.capsules.read().await;
295 let failed_ids: Vec<_> = reg
296 .list()
297 .into_iter()
298 .filter(|id| {
299 reg.get(id).is_some_and(|c| {
300 matches!(c.state(), astrid_capsule::capsule::CapsuleState::Failed(_))
301 })
302 })
303 .cloned()
304 .collect();
305 drop(reg);
306
307 let mut reg = kernel.capsules.write().await;
308 for id in failed_ids {
309 let _ = reg.unregister(&id);
310 }
311 }
312
313 kernel.load_all_capsules().await;
314 KernelResponse::Success(serde_json::json!({"status": "reloaded"}))
315 },
316 KernelRequest::Shutdown { reason } => {
317 info!(
318 reason = reason.as_deref().unwrap_or("none"),
319 "Kernel received shutdown request via management API"
320 );
321 publish_response(
323 kernel,
324 response_topic.clone(),
325 KernelResponse::Success(serde_json::json!({"status": "shutting_down"})),
326 );
327 let _ = kernel.shutdown_tx.send(true);
329 return;
331 },
332 KernelRequest::GetStatus => {
333 let uptime = kernel.boot_time.elapsed().as_secs();
334 let reg = kernel.capsules.read().await;
335 let loaded: Vec<String> = reg.list().iter().map(ToString::to_string).collect();
336 let by_principal = kernel
337 .connections_by_principal()
338 .into_iter()
339 .map(
340 |(p, c)| astrid_events::kernel_api::PrincipalConnectionCount {
341 principal: p.to_string(),
342 count: u32::try_from(c).unwrap_or(u32::MAX),
343 },
344 )
345 .collect();
346 let status = astrid_events::kernel_api::DaemonStatus {
347 pid: std::process::id(),
348 uptime_secs: uptime,
349 version: env!("CARGO_PKG_VERSION").to_string(),
350 ephemeral: false, connected_clients: u32::try_from(kernel.total_connection_count())
352 .unwrap_or(u32::MAX),
353 connections_by_principal: by_principal,
354 loaded_capsules: loaded,
355 };
356 KernelResponse::Status(status)
357 },
358 KernelRequest::GetCapsuleMetadata => {
359 let reg = kernel.capsules.read().await;
360 let mut entries = Vec::new();
361 for capsule in reg.values() {
362 let manifest = capsule.manifest();
363 entries.push(astrid_events::kernel_api::CapsuleMetadataEntry {
364 name: manifest.package.name.clone(),
365 interceptor_events: manifest
366 .subscribes
367 .iter()
368 .filter(|(_, def)| def.handler.is_some())
369 .map(|(topic, _)| topic.clone())
370 .collect(),
371 });
372 }
373 KernelResponse::CapsuleMetadata(entries)
374 },
375 };
376
377 publish_response(kernel, response_topic, res);
378}
379
380fn publish_response<R: Serialize>(kernel: &Arc<crate::Kernel>, response_topic: String, res: R) {
381 if let Ok(val) = serde_json::to_value(res) {
382 let msg = IpcMessage::new(
383 response_topic,
384 IpcPayload::RawJson(val),
385 kernel.session_id.0,
386 );
387 let _ = kernel.event_bus.publish(astrid_events::AstridEvent::Ipc {
388 metadata: astrid_events::EventMetadata::new("kernel_router"),
389 message: msg,
390 });
391 }
392}
393
394struct ManagementRateLimiter {
403 buckets: HashMap<&'static str, VecDeque<Instant>>,
404}
405
406impl ManagementRateLimiter {
407 fn new() -> Self {
408 Self {
409 buckets: HashMap::new(),
410 }
411 }
412
413 fn check(&mut self, method: &'static str, max_per_minute: u32) -> bool {
416 let now = Instant::now();
417 let window = std::time::Duration::from_mins(1);
418 let timestamps = self.buckets.entry(method).or_default();
419
420 while let Some(&oldest) = timestamps.front() {
422 if now.saturating_duration_since(oldest) >= window {
423 timestamps.pop_front();
424 } else {
425 break;
426 }
427 }
428
429 if timestamps.len() >= max_per_minute as usize {
430 return false;
431 }
432 timestamps.push_back(now);
433 true
434 }
435}
436
437fn rate_limit_for_request(req: &KernelRequest) -> (&'static str, Option<u32>) {
440 (kernel_request_method(req), rate_limit_max(req))
441}
442
443fn rate_limit_max(req: &KernelRequest) -> Option<u32> {
445 match req {
446 KernelRequest::ReloadCapsules => Some(5),
447 KernelRequest::InstallCapsule { .. } | KernelRequest::ApproveCapability { .. } => Some(10),
448 KernelRequest::Shutdown { .. } => Some(1),
449 KernelRequest::ListCapsules
450 | KernelRequest::GetCommands
451 | KernelRequest::GetCapsuleMetadata
452 | KernelRequest::GetStatus => None,
453 }
454}
455
456#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466pub enum AuthorityScope {
467 Self_,
469 Global,
471}
472
473#[must_use]
478pub fn resolve_scope(_req: &KernelRequest, _caller: &PrincipalId) -> AuthorityScope {
479 AuthorityScope::Self_
480}
481
482#[must_use]
489pub fn required_capability(req: &KernelRequest, scope: AuthorityScope) -> &'static str {
490 match (req, scope) {
491 (KernelRequest::Shutdown { .. }, _) => "system:shutdown",
492 (KernelRequest::GetStatus, _) => "system:status",
493 (KernelRequest::ReloadCapsules, AuthorityScope::Self_) => "self:capsule:reload",
494 (KernelRequest::ReloadCapsules, _) => "capsule:reload",
495 (KernelRequest::InstallCapsule { .. }, AuthorityScope::Self_) => "self:capsule:install",
496 (KernelRequest::InstallCapsule { .. }, _) => "capsule:install",
497 (
498 KernelRequest::ListCapsules
499 | KernelRequest::GetCommands
500 | KernelRequest::GetCapsuleMetadata,
501 AuthorityScope::Self_,
502 ) => "self:capsule:list",
503 (
504 KernelRequest::ListCapsules
505 | KernelRequest::GetCommands
506 | KernelRequest::GetCapsuleMetadata,
507 _,
508 ) => "capsule:list",
509 (KernelRequest::ApproveCapability { .. }, _) => "self:approval:respond",
510 }
511}
512
513#[must_use]
516pub fn kernel_request_method(req: &KernelRequest) -> &'static str {
517 match req {
518 KernelRequest::ReloadCapsules => "ReloadCapsules",
519 KernelRequest::InstallCapsule { .. } => "InstallCapsule",
520 KernelRequest::ApproveCapability { .. } => "ApproveCapability",
521 KernelRequest::ListCapsules => "ListCapsules",
522 KernelRequest::GetCommands => "GetCommands",
523 KernelRequest::GetCapsuleMetadata => "GetCapsuleMetadata",
524 KernelRequest::Shutdown { .. } => "Shutdown",
525 KernelRequest::GetStatus => "GetStatus",
526 }
527}
528
529fn resolve_caller(message: &IpcMessage) -> PrincipalId {
536 message
537 .principal
538 .as_deref()
539 .and_then(|p| PrincipalId::new(p).ok())
540 .unwrap_or_default()
541}
542
543fn authorize_request(
551 kernel: &crate::Kernel,
552 caller: &PrincipalId,
553 required_cap: &str,
554) -> Result<(), PermissionError> {
555 let profile = match kernel.profile_cache.resolve(caller) {
556 Ok(p) => p,
557 Err(e) => {
558 warn!(
559 security_event = true,
560 principal = %caller,
561 error = %e,
562 "Profile resolution failed — fail-closed deny"
563 );
564 return Err(PermissionError::MissingCapability {
565 principal: caller.clone(),
566 required: required_cap.to_string(),
567 });
568 },
569 };
570 if !profile.enabled {
577 warn!(
578 security_event = true,
579 principal = %caller,
580 required = required_cap,
581 "Disabled principal denied — fail-closed enforcement"
582 );
583 return Err(PermissionError::PrincipalDisabled {
584 principal: caller.clone(),
585 });
586 }
587 let groups = kernel.groups.load_full();
588 let check = CapabilityCheck::new(profile.as_ref(), groups.as_ref(), caller.clone());
589 check.require(required_cap)
590}
591
592pub(crate) struct AdminAuditEntry<'a> {
595 pub caller: &'a PrincipalId,
597 pub method: &'a str,
599 pub required_cap: &'a str,
601 pub target_principal: Option<PrincipalId>,
605 pub params: Option<serde_json::Value>,
609 pub authorization: AuthorizationProof,
611 pub outcome: AuditOutcome,
613}
614
615pub const AUDIT_TOPIC: &str = "astrid.v1.audit.entry";
625
626fn record_admin_audit(kernel: &crate::Kernel, entry: AdminAuditEntry<'_>) {
632 let AdminAuditEntry {
633 caller,
634 method,
635 required_cap,
636 target_principal,
637 params,
638 authorization,
639 outcome,
640 } = entry;
641 let action = AuditAction::AdminRequest {
642 method: method.to_string(),
643 required_capability: required_cap.to_string(),
644 target_principal: target_principal.clone(),
645 params: params.clone(),
646 };
647 if let Err(e) = kernel.audit_log.append_with_principal(
648 kernel.session_id.clone(),
649 caller.clone(),
650 action,
651 authorization.clone(),
652 outcome.clone(),
653 ) {
654 warn!(
655 security_event = true,
656 principal = %caller,
657 method = method,
658 error = %e,
659 "Failed to persist admin-request audit entry — continuing"
660 );
661 }
662
663 let event = serde_json::json!({
668 "ts_epoch": std::time::SystemTime::now()
669 .duration_since(std::time::UNIX_EPOCH)
670 .map_or(0, |d| d.as_secs()),
671 "method": method,
672 "required_capability": required_cap,
673 "principal": caller.to_string(),
674 "target_principal": target_principal.as_ref().map(ToString::to_string),
675 "params": params,
676 "outcome": match &outcome {
677 AuditOutcome::Success { .. } => "success",
678 AuditOutcome::Failure { .. } => "failure",
679 },
680 });
681 let msg = IpcMessage::new(AUDIT_TOPIC, IpcPayload::RawJson(event), uuid::Uuid::nil())
682 .with_principal(caller.to_string());
683 let _ = kernel.event_bus.publish(astrid_events::AstridEvent::Ipc {
684 metadata: astrid_events::EventMetadata::new("kernel_router::audit"),
685 message: msg,
686 });
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn rate_limiter_allows_within_limit() {
695 let mut limiter = ManagementRateLimiter::new();
696 for _ in 0..5 {
697 assert!(limiter.check("ReloadCapsules", 5));
698 }
699 assert!(!limiter.check("ReloadCapsules", 5));
701 }
702
703 #[test]
704 fn rate_limiter_independent_buckets() {
705 let mut limiter = ManagementRateLimiter::new();
706 for _ in 0..5 {
708 assert!(limiter.check("ReloadCapsules", 5));
709 }
710 assert!(!limiter.check("ReloadCapsules", 5));
711
712 assert!(limiter.check("InstallCapsule", 10));
714 }
715
716 #[test]
717 fn rate_limiter_sliding_window_eviction() {
718 let mut limiter = ManagementRateLimiter::new();
719 for _ in 0..5 {
721 assert!(limiter.check("ReloadCapsules", 5));
722 }
723 assert!(!limiter.check("ReloadCapsules", 5));
724
725 if let Some(timestamps) = limiter.buckets.get_mut("ReloadCapsules") {
727 let past = Instant::now() - std::time::Duration::from_secs(61);
728 for ts in timestamps.iter_mut() {
729 *ts = past;
730 }
731 }
732
733 assert!(limiter.check("ReloadCapsules", 5));
735 }
736
737 #[test]
738 fn rate_limiter_sliding_window_prevents_boundary_burst() {
739 let mut limiter = ManagementRateLimiter::new();
740 for _ in 0..5 {
742 assert!(limiter.check("ReloadCapsules", 5));
743 }
744
745 if let Some(timestamps) = limiter.buckets.get_mut("ReloadCapsules") {
748 let past = Instant::now() - std::time::Duration::from_secs(61);
749 for ts in timestamps.iter_mut().take(3) {
750 *ts = past;
751 }
752 }
753
754 for _ in 0..3 {
756 assert!(limiter.check("ReloadCapsules", 5));
757 }
758 assert!(!limiter.check("ReloadCapsules", 5));
759 }
760
761 #[test]
762 fn rate_limit_for_request_returns_correct_limits() {
763 let (name, limit) = rate_limit_for_request(&KernelRequest::ReloadCapsules);
764 assert_eq!(name, "ReloadCapsules");
765 assert_eq!(limit, Some(5));
766
767 let (name, limit) = rate_limit_for_request(&KernelRequest::ListCapsules);
768 assert_eq!(name, "ListCapsules");
769 assert_eq!(limit, None);
770 }
771
772 fn all_request_variants() -> Vec<KernelRequest> {
775 vec![
776 KernelRequest::Shutdown { reason: None },
777 KernelRequest::GetStatus,
778 KernelRequest::ReloadCapsules,
779 KernelRequest::InstallCapsule {
780 source: "x".to_string(),
781 workspace: false,
782 },
783 KernelRequest::ListCapsules,
784 KernelRequest::GetCommands,
785 KernelRequest::GetCapsuleMetadata,
786 KernelRequest::ApproveCapability {
787 request_id: "r".to_string(),
788 signature: "s".to_string(),
789 },
790 ]
791 }
792
793 #[test]
794 fn required_capability_every_variant_has_non_empty_mapping() {
795 for req in all_request_variants() {
796 let cap = required_capability(&req, AuthorityScope::Self_);
797 assert!(
798 !cap.is_empty(),
799 "required_capability returned empty for {req:?}"
800 );
801 }
802 }
803
804 #[test]
805 fn required_capability_mapping_per_variant_self_scope() {
806 assert_eq!(
807 required_capability(
808 &KernelRequest::Shutdown { reason: None },
809 AuthorityScope::Self_
810 ),
811 "system:shutdown"
812 );
813 assert_eq!(
814 required_capability(&KernelRequest::GetStatus, AuthorityScope::Self_),
815 "system:status"
816 );
817 assert_eq!(
818 required_capability(&KernelRequest::ReloadCapsules, AuthorityScope::Self_),
819 "self:capsule:reload"
820 );
821 assert_eq!(
822 required_capability(
823 &KernelRequest::InstallCapsule {
824 source: String::new(),
825 workspace: false
826 },
827 AuthorityScope::Self_
828 ),
829 "self:capsule:install"
830 );
831 assert_eq!(
832 required_capability(&KernelRequest::ListCapsules, AuthorityScope::Self_),
833 "self:capsule:list"
834 );
835 assert_eq!(
836 required_capability(&KernelRequest::GetCommands, AuthorityScope::Self_),
837 "self:capsule:list"
838 );
839 assert_eq!(
840 required_capability(&KernelRequest::GetCapsuleMetadata, AuthorityScope::Self_),
841 "self:capsule:list"
842 );
843 assert_eq!(
844 required_capability(
845 &KernelRequest::ApproveCapability {
846 request_id: String::new(),
847 signature: String::new(),
848 },
849 AuthorityScope::Self_
850 ),
851 "self:approval:respond"
852 );
853 }
854
855 #[test]
856 fn required_capability_mapping_global_scope() {
857 assert_eq!(
860 required_capability(&KernelRequest::ReloadCapsules, AuthorityScope::Global),
861 "capsule:reload"
862 );
863 assert_eq!(
864 required_capability(
865 &KernelRequest::InstallCapsule {
866 source: String::new(),
867 workspace: false
868 },
869 AuthorityScope::Global
870 ),
871 "capsule:install"
872 );
873 assert_eq!(
874 required_capability(&KernelRequest::ListCapsules, AuthorityScope::Global),
875 "capsule:list"
876 );
877 assert_eq!(
879 required_capability(
880 &KernelRequest::Shutdown { reason: None },
881 AuthorityScope::Global
882 ),
883 "system:shutdown"
884 );
885 }
886
887 #[test]
888 fn resolve_scope_defaults_to_self() {
889 let caller = PrincipalId::new("alice").unwrap();
890 for req in all_request_variants() {
891 assert_eq!(
892 resolve_scope(&req, &caller),
893 AuthorityScope::Self_,
894 "scope should default to Self_ for today's variants"
895 );
896 }
897 }
898
899 #[test]
902 fn resolve_caller_uses_ipc_principal_when_present() {
903 let mut msg = IpcMessage::new(
904 "astrid.v1.request.system",
905 IpcPayload::RawJson(serde_json::json!({})),
906 uuid::Uuid::nil(),
907 );
908 msg.principal = Some("alice".to_string());
909 let caller = resolve_caller(&msg);
910 assert_eq!(caller.as_str(), "alice");
911 }
912
913 #[test]
914 fn resolve_caller_falls_back_to_default_when_missing() {
915 let msg = IpcMessage::new(
916 "astrid.v1.request.system",
917 IpcPayload::RawJson(serde_json::json!({})),
918 uuid::Uuid::nil(),
919 );
920 let caller = resolve_caller(&msg);
921 assert_eq!(caller, PrincipalId::default());
922 }
923
924 #[test]
925 fn resolve_caller_falls_back_to_default_on_invalid_principal() {
926 let mut msg = IpcMessage::new(
927 "astrid.v1.request.system",
928 IpcPayload::RawJson(serde_json::json!({})),
929 uuid::Uuid::nil(),
930 );
931 msg.principal = Some("alice@evil.example".to_string());
933 let caller = resolve_caller(&msg);
934 assert_eq!(caller, PrincipalId::default());
935 }
936}