Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/core/mxc-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down
3 changes: 2 additions & 1 deletion src/core/mxc-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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::{
Expand Down
2 changes: 1 addition & 1 deletion src/core/mxc_engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down
138 changes: 136 additions & 2 deletions src/core/mxc_engine/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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<String>,
/// 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<String>,
}

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`].
///
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down