From 0e653ff06f0dc9214beac395ed6953fde149744c Mon Sep 17 00:00:00 2001 From: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:50:44 -0700 Subject: [PATCH] Add Seatbelt configuration to Rust SDK builder Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea8d4b87-97bc-49cb-b319-552e89e3e21e --- src/core/mxc-sdk/README.md | 3 + src/core/mxc-sdk/src/lib.rs | 3 +- src/core/mxc_engine/src/lib.rs | 2 +- src/core/mxc_engine/src/policy.rs | 138 +++++++++++++++++++++++++++++- 4 files changed, 142 insertions(+), 4 deletions(-) diff --git a/src/core/mxc-sdk/README.md b/src/core/mxc-sdk/README.md index 619fec847..d974b3d94 100644 --- a/src/core/mxc-sdk/README.md +++ b/src/core/mxc-sdk/README.md @@ -54,6 +54,9 @@ the TypeScript SDK's `alpine`/`3.23` values. The request can be used with the engine's run-to-completion surface; `mxc-sdk::run` and `spawn_sandbox` do not yet support LXC because they require a streaming backend. +`Containment::Seatbelt` exposes the backend-specific macOS profile, GUI, +launch, nested-PTY, Keychain, and Mach-service settings. + Filesystem-policy discovery helpers (ports of the SDK's `policy.ts`) are also available to feed a policy: [`available_tools_policy`] (PATH + tool/SDK env dirs), [`user_profile_policy`], and [`temporary_files_policy`]. diff --git a/src/core/mxc-sdk/src/lib.rs b/src/core/mxc-sdk/src/lib.rs index 7cd2f2ca6..1c0834bcb 100644 --- a/src/core/mxc-sdk/src/lib.rs +++ b/src/core/mxc-sdk/src/lib.rs @@ -52,6 +52,7 @@ //! | ProcessContainer (AppContainer / BaseContainer) | Windows | [`Containment::Process`] | //! | Explicit ProcessContainer settings | Windows | [`Containment::ProcessContainer`] | //! | LXC request construction | Linux | [`Containment::Lxc`] | +//! | Explicit Seatbelt settings | macOS | [`Containment::Seatbelt`] | //! | WSLC (WSL Container) | Windows | [`Containment::Wslc`] | //! //! WSLC is **experimental**: build with the crate's `wslc` feature, and call @@ -120,7 +121,7 @@ pub use mxc_engine::{ available_backends, available_tools_policy, build_request, build_request_with_containment, platform_support, temporary_files_policy, user_profile_policy, AvailableBackend, BackendCapability, Containment, Error, ErrorCode, FilesystemPolicyResult, Lxc, PlatformSupport, - ProcessContainer, SandboxPolicy, SandboxRequest, WslcSection, + ProcessContainer, SandboxPolicy, SandboxRequest, Seatbelt, SeatbeltLaunchMethod, WslcSection, }; pub use sandbox::{ diff --git a/src/core/mxc_engine/src/lib.rs b/src/core/mxc_engine/src/lib.rs index e97f461af..84800fb1f 100644 --- a/src/core/mxc_engine/src/lib.rs +++ b/src/core/mxc_engine/src/lib.rs @@ -45,7 +45,7 @@ pub use platform::{platform_support, PlatformSupport}; pub use policy::{ available_tools_policy, build_request, build_request_with_containment, temporary_files_policy, user_profile_policy, Containment, FilesystemPolicyResult, Lxc, ProcessContainer, SandboxPolicy, - SandboxRequest, WslcSection, + SandboxRequest, Seatbelt, SeatbeltLaunchMethod, WslcSection, }; pub use probe::{available_backends, AvailableBackend, BackendCapability}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] diff --git a/src/core/mxc_engine/src/policy.rs b/src/core/mxc_engine/src/policy.rs index 4b24e50e1..9906ee07b 100644 --- a/src/core/mxc_engine/src/policy.rs +++ b/src/core/mxc_engine/src/policy.rs @@ -571,6 +571,8 @@ pub enum Containment { ProcessContainer(ProcessContainer), /// Linux LXC container settings. Lxc(Lxc), + /// macOS Seatbelt settings. + Seatbelt(Seatbelt), /// WSL Container backend: a Linux container on a Windows host, via the WSLC /// SDK, configured by the carried [`WslcSection`] /// (`WslcSection::default()` matches the SDK's defaults). @@ -608,6 +610,55 @@ impl Default for Lxc { } } +/// macOS Seatbelt settings carried by [`Containment::Seatbelt`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Seatbelt { + /// Replace the generated sandbox profile. + pub profile_override: Option, + /// Allow GUI application access. + pub gui_access: bool, + /// How to launch the contained process. + pub launch_method: SeatbeltLaunchMethod, + /// Allow the contained process to allocate nested pseudo-terminals. + pub nested_pty: bool, + /// Allow macOS Keychain access. + pub keychain_access: bool, + /// Additional Mach service global names the process may resolve. + pub extra_mach_lookups: Vec, +} + +impl Default for Seatbelt { + fn default() -> Self { + Self { + profile_override: None, + gui_access: false, + launch_method: SeatbeltLaunchMethod::Exec, + nested_pty: true, + keychain_access: false, + extra_mach_lookups: Vec::new(), + } + } +} + +/// Seatbelt inner-process launch method. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum SeatbeltLaunchMethod { + /// Apply Seatbelt, then execute the process directly. + #[default] + Exec, + /// Launch through macOS LaunchServices. + Open, +} + +impl SeatbeltLaunchMethod { + fn wire(self) -> &'static str { + match self { + Self::Exec => "exec", + Self::Open => "open", + } + } +} + /// WSL Container settings, mirroring the SDK's `experimental.wslc` config /// block; carried by [`Containment::Wslc`]. /// @@ -946,6 +997,7 @@ fn build_wire_config( Containment::Process => cfg!(any(target_os = "linux", target_os = "macos")), Containment::ProcessContainer(_) => false, Containment::Lxc(_) => true, + Containment::Seatbelt(_) => true, Containment::Wslc(_) => true, }; @@ -979,6 +1031,7 @@ fn build_wire_config( apply_process_container_backend(&mut config, policy, process_container) } Containment::Lxc(lxc) => apply_lxc_backend(&mut config, lxc), + Containment::Seatbelt(seatbelt) => apply_seatbelt_backend(&mut config, seatbelt), Containment::Wslc(wslc) => apply_wslc_backend(&mut config, wslc), } Ok(config) @@ -1100,6 +1153,20 @@ fn apply_lxc_backend(config: &mut serde_json::Value, lxc: &Lxc) { apply_linux_network_policy(config); } +fn apply_seatbelt_backend(config: &mut serde_json::Value, seatbelt: &Seatbelt) { + use serde_json::json; + + config["containment"] = json!("seatbelt"); + config["seatbelt"] = json!({ + "profileOverride": seatbelt.profile_override, + "guiAccess": seatbelt.gui_access, + "launchMethod": seatbelt.launch_method.wire(), + "nestedPty": seatbelt.nested_pty, + "keychainAccess": seatbelt.keychain_access, + "extraMachLookups": seatbelt.extra_mach_lookups, + }); +} + /// True when the network section carries any host allow/deny rules, deciding /// whether host-level enforcement is engaged. fn has_host_rules(network: &serde_json::Value) -> bool { @@ -1677,9 +1744,11 @@ mod tests { use super::{ build_request_with_containment, build_wire_config, Containment, Lxc, ProcessContainer, - WslcSection, + Seatbelt, SeatbeltLaunchMethod, WslcSection, + }; + use wxc_common::models::{ + ContainmentBackend, LaunchMethod as DomainLaunchMethod, NetworkEnforcementMode, }; - use wxc_common::models::{ContainmentBackend, NetworkEnforcementMode}; fn minimal_policy() -> SandboxPolicy { SandboxPolicy { @@ -1881,6 +1950,71 @@ mod tests { assert!(config.get("processContainer").is_none()); } + #[test] + fn seatbelt_containment_maps_config_to_the_request() { + let seatbelt = Seatbelt { + profile_override: Some("(version 1)(deny default)".to_string()), + gui_access: true, + launch_method: SeatbeltLaunchMethod::Open, + nested_pty: false, + keychain_access: true, + extra_mach_lookups: vec!["com.example.service".to_string()], + }; + + let request = build_request_with_containment( + &minimal_policy(), + &Containment::Seatbelt(seatbelt), + None, + ) + .expect("Seatbelt settings should satisfy the wire contract"); + + assert_eq!(request.inner.containment, ContainmentBackend::Seatbelt); + let config = request.inner.seatbelt.as_ref().expect("Seatbelt config"); + assert_eq!( + config.profile_override.as_deref(), + Some("(version 1)(deny default)") + ); + assert!(config.gui_access); + assert_eq!(config.launch_method, DomainLaunchMethod::Open); + assert!(!config.nested_pty); + assert!(config.keychain_access); + assert_eq!(config.extra_mach_lookups, ["com.example.service"]); + } + + #[test] + fn seatbelt_defaults_match_the_backend() { + let request = build_request_with_containment( + &minimal_policy(), + &Containment::Seatbelt(Seatbelt::default()), + None, + ) + .expect("default Seatbelt settings should build"); + + let config = request.inner.seatbelt.as_ref().expect("Seatbelt config"); + assert!(config.profile_override.is_none()); + assert!(!config.gui_access); + assert_eq!(config.launch_method, DomainLaunchMethod::Exec); + assert!(config.nested_pty); + assert!(!config.keychain_access); + assert!(config.extra_mach_lookups.is_empty()); + } + + #[test] + fn seatbelt_wire_shape_stays_compatible_with_the_versioned_contract() { + let config = build_wire_config( + &minimal_policy(), + &Containment::Seatbelt(Seatbelt::default()), + None, + ) + .expect("build Seatbelt wire config"); + + assert_eq!(config["containment"], "seatbelt"); + assert_eq!(config["seatbelt"]["launchMethod"], "exec"); + assert_eq!(config["seatbelt"]["nestedPty"], true); + assert!(config.get("lxc").is_none()); + assert!(config.get("processContainer").is_none()); + } + #[test] fn wslc_containment_maps_config_to_the_request() { // Mirrors `createConfigFromPolicy(policy, 'wslc')` plus a tweaked