How the Seatbelt Backend Generates TinyScheme Profiles in MXC
The Seatbelt backend translates MXC ExecutionRequest policies into TinyScheme sandbox profiles at runtime through the build_profile function, assembling layered allow/deny rules before applying them via Apple's sandbox_init API.
The Seatbelt backend in the Microsoft MXC (Multi-platform eXecution Container) project provides macOS sandboxing by generating TinyScheme profiles that Apple's Seatbelt security framework can enforce. When you specify containment: "seatbelt" in an execution request, the backend dynamically constructs a sandbox profile string that translates your MXC policy into native macOS sandbox rules. This process involves translating filesystem, network, and execution constraints into Scheme expressions that the sandbox_init system call consumes.
Profile Construction in build_profile
The core generation logic resides in src/backends/seatbelt/common/src/profile_builder.rs, specifically within the build_profile function. This pure-Rust builder transforms an ExecutionRequest into a valid TinyScheme string through a structured assembly process.
Profile Override Handling
Before generating rules, the function checks for experimental.seatbelt.profile_override. If present, the builder returns this raw TinyScheme string verbatim, bypassing all automatic rule generation.
Layered Rule Assembly
When no override exists, the builder constructs the profile in strict order:
- Header and Baseline –
(version 1)and(deny default)establish the deny-by-default security posture - Baseline Allow Rules –
BASELINE_ALLOWconstants permitting essential process operations - System Read-Only –
SYSTEM_READ_ALLOWfor necessary system library access - PTY Access –
TTY_ALLOWrules for terminal interaction - Policy-Derived Allows – Dynamic rules for:
- Filesystem access (
write_filesystem_allow) - Network connectivity (
write_network_rules) - Nested PTY and Keychain access
- UI isolation when
guiAccessis enabled
- Filesystem access (
- Policy-Derived Denies – Deny rules for
deniedPathsviawrite_filesystem_deny
The builder uses expand_tilde to resolve home directory paths and quote_scheme to escape strings for Scheme literal safety.
Runner Invocation and Sandbox Application
Once constructed, the profile string passes to SeatbeltScriptRunner in src/backends/seatbelt/common/src/seatbelt_runner.rs. This component handles the actual sandbox instantiation through two distinct launch methods.
Exec Mode
In exec mode (the default), the runner forks a child process and uses Command::pre_exec to invoke sandbox_init with the generated profile before executing /bin/sh -c <script>. This applies the sandbox restrictions immediately before the target script gains control.
Open Mode
When experimental.seatbelt.launch_method is set to "open", the runner instead uses LaunchServices to spawn a helper application (such as Terminal.app) and applies the profile using the sandbox-exec CLI utility, enabling GUI applications to run within the sandbox.
Rule Ordering and Last-Match-Wins Semantics
The generated profile follows Apple's last-match-wins evaluation order. By emitting broad allow rules first and specific deny rules for deniedPaths last, the backend ensures that explicit denials override general allowances. This ordering matches MXC's semantic model across all containment backends, allowing deniedPaths to prevail even when parent directories appear in readwritePaths.
Practical Examples
Generating Profiles Directly in Rust
use wxc_common::models::{ExecutionRequest, Policy, NetworkPolicy};
use seatbelt_common::profile_builder::build_profile;
let request = ExecutionRequest {
policy: Policy {
readonly_paths: vec!["/Users/me/project".into()],
readwrite_paths: vec!["/tmp/output".into()],
denied_paths: vec!["/Users/me/.ssh".into()],
default_network_policy: NetworkPolicy::Block,
allowed_hosts: vec![],
blocked_hosts: vec![],
..Default::default()
},
experimental: Default::default(),
script_code: "echo hello from seatbelt".into(),
..Default::default()
};
let profile = build_profile(&request).expect("profile generation");
println!("{}", profile);
Using the TypeScript SDK
import { exec } from "@microsoft/mxc";
await exec({
containment: "seatbelt",
process: { commandLine: "echo hi from seatbelt", timeout: 30000 },
filesystem: {
readwritePaths: ["/tmp/out"],
readonlyPaths: ["/Users/me/project"],
deniedPaths: ["/Users/me/.ssh"]
},
network: { defaultPolicy: "block" },
experimental: {
seatbelt: {
launchMethod: "exec",
guiAccess: false
}
}
});
Supplying a Raw Profile Override
{
"experimental": {
"seatbelt": {
"profileOverride": "(version 1)\n(deny default)\n(allow file-read-data (literal \"/usr/bin\"))\n(deny file-read* file-write* (subpath \"/secret\"))"
}
}
}
When provided, this TinyScheme string bypasses the automatic rule builder entirely.
Summary
- The
build_profilefunction inprofile_builder.rsconstructs TinyScheme profiles by layering allow rules atop a deny-by-default baseline, finishing with specific deny rules fordeniedPaths - Helper functions
expand_tildeandquote_schemeensure path literals are correctly escaped for Scheme syntax SeatbeltScriptRunnerapplies profiles viasandbox_initinexecmode or throughsandbox-execinopenmode- Rule ordering follows Apple's last-match-wins semantics, allowing explicit denials to override broader allowances
- Users can bypass automatic generation entirely using
experimental.seatbelt.profileOverridein the ExecutionRequest
Frequently Asked Questions
What is the difference between exec and open launch methods in the Seatbelt backend?
The exec method forks a new process, applies the TinyScheme profile via the sandbox_init C API using Command::pre_exec, and then executes the target script through /bin/sh. The open method instead uses macOS LaunchServices to spawn a helper application like Terminal.app, applying the sandbox through the sandbox-exec CLI tool. Use exec for command-line tools and open for GUI applications requiring WindowServer access.
How does the Seatbelt backend handle path escaping in TinyScheme profiles?
The builder uses the quote_scheme helper function to escape user-provided paths into valid Scheme string literals. This prevents injection attacks and syntax errors when paths contain spaces, quotes, or other special characters. Additionally, expand_tilde resolves ~ characters to absolute home directory paths before quoting occurs.
Can I use custom TinyScheme profiles with the MXC Seatbelt backend?
Yes. Set experimental.seatbelt.profileOverride to a raw TinyScheme string in your ExecutionRequest. When present, the build_profile function returns this string verbatim without generating baseline, filesystem, or network rules, giving you complete control over the sandbox policy while still using MXC's execution infrastructure.
Why are deny rules emitted last in the generated TinyScheme profile?
Apple's Seatbelt evaluator uses last-match-wins semantics, meaning the final matching rule in the profile determines access. By emitting deniedPaths rules after broader allow rules, the backend ensures that explicit denials take precedence over general filesystem allowances, maintaining consistency with MXC's security model across all platforms.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →