Skip to content
Merged
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
68 changes: 65 additions & 3 deletions schemas/dev/mxc-config.schema.0.6.0-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,21 @@
},
"containment": {
"type": "string",
"enum": ["process", "processcontainer", "windows_sandbox", "lxc", "microvm", "wslc", "seatbelt", "isolation_session", "bubblewrap"],
"enum": [
"process",
"processcontainer",
"vm",
"windows_sandbox",
"lxc",
"microvm",
"hyperlight",
"wslc",
"seatbelt",
"isolation_session",
"bubblewrap"
],
"default": "processcontainer",
"description": "Containment value to use for execution. Accepts both abstract intents ('process', 'microvm') and concrete backends ('processcontainer', 'windows_sandbox', 'lxc', 'microvm', 'wslc', 'seatbelt', 'isolation_session', 'bubblewrap'). The native binary resolves abstract intents to a concrete backend at run time based on host capabilities. Note: 'windows_sandbox', 'wslc', 'seatbelt', 'isolation_session', and 'bubblewrap' are experimental and require the --experimental CLI flag."
"description": "Containment value to use for execution. Accepts both abstract intents ('process', 'vm') and concrete backends ('processcontainer', 'windows_sandbox', 'lxc', 'microvm', 'hyperlight', 'wslc', 'seatbelt', 'isolation_session', 'bubblewrap'). The native binary resolves abstract intents to a concrete backend at run time based on host capabilities (e.g., 'process' resolves to ProcessContainer on Windows, LXC on Linux, Seatbelt on macOS; 'vm' resolves to Windows Sandbox on Windows). Note: 'windows_sandbox', 'wslc', 'seatbelt', 'isolation_session', 'hyperlight', and 'bubblewrap' are experimental and require the --experimental CLI flag. 'hyperlight' and 'bubblewrap' have no per-backend configuration block; they share the common filesystem/network policy fields. The legacy aliases 'appcontainer' and 'macos_sandbox' are still accepted by the parser (with a deprecation log line) but are intentionally omitted from this enum so editors steer authors toward the canonical names."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (non-blocking): I wonder if this description may be too implementation heavy, since as we add more backends it'd continue to grow.

},

"phase": {
Expand Down Expand Up @@ -141,6 +153,11 @@
"items": { "type": "string" },
"description": "Hostnames or IP addresses to block (when defaultPolicy is 'allow'). Enforced by 'lxc' and 'processcontainer' containment backends."
},
"allowLocalNetwork": {
"type": "boolean",
"default": false,
"description": "When true, the sandboxed process may bind() and listen() on local IPs and accept incoming connections. Independent of 'defaultPolicy' (which governs outbound traffic). Honored by the 'seatbelt' backend; other backends ignore this field today."
},
"proxy": {
"type": "object",
"description": "Proxy configuration. Supported with the 'processcontainer' (Windows) and 'bubblewrap' (Linux) backends.",
Expand All @@ -167,6 +184,16 @@
},
"required": ["builtinTestServer"],
"additionalProperties": false
},
{
"properties": {
"url": {
"type": "string",
"description": "Full proxy URL including port (e.g., http://proxy.example.com:8080). Host and port are extracted from the URL."
}
},
"required": ["url"],
"additionalProperties": false
}
]
}
Expand Down Expand Up @@ -279,7 +306,7 @@
"type": "integer",
"minimum": 0,
"default": 300000,
"description": "Idle timeout in milliseconds before the daemon tears down the sandbox VM. 0 = no timeout."
"description": "Idle timeout in milliseconds before the daemon tears down the sandbox VM. 0 = no timeout. The parser also accepts the legacy 'idleTimeout' alias (same units) for back-compat, but the schema intentionally omits it so editors steer authors here."
},
"daemonPipeName": {
"type": "string",
Expand Down Expand Up @@ -326,6 +353,35 @@
"storagePath": {
"type": "string",
"description": "Storage path for the WSLC session image store. Omit to use a temporary directory."
},
"portMappings": {
"type": "array",
"description": "Host-to-container port forwards. Each entry maps a Windows host port to a container port for a given protocol.",
"items": {
"type": "object",
"properties": {
"windowsPort": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "Port on the Windows host to bind."
},
"containerPort": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "Port inside the container to forward to."
},
"protocol": {
"type": "string",
"enum": ["tcp", "udp"],
"default": "tcp",
"description": "Transport protocol for the mapping."
}
},
"required": ["windowsPort", "containerPort"],
"additionalProperties": false
}
}
}
},
Expand Down Expand Up @@ -357,6 +413,12 @@
"type": "boolean",
"default": false,
"description": "Allow the inner process to use the macOS Keychain (e.g. via keytar or Security.framework) end-to-end. Adds Mach lookup for com.apple.SecurityServer, com.apple.securityd, com.apple.trustd, com.apple.ocspd, com.apple.cfprefsd.daemon, com.apple.xpcd and the com.apple.lsd.* family; read access to /private/var/db/mds and /private/var/protected/trustd; and read+write access to ~/Library/Keychains and /private/var/folders (XPC cache). System keychain stores under /Library and /System/Library are already covered by the baseline read-only allows. Defaults to false; opt in only when the inner workload genuinely needs Keychain access."
},
"extraMachLookups": {
"type": "array",
"items": { "type": "string" },
"default": [],
"description": "Caller-provided Mach service global-names the inner process may resolve. Each entry is emitted into the generated TinyScheme profile as `(allow mach-lookup (global-name \"...\"))`. Use this as a targeted escape hatch when a workload needs a Mach service the baseline profile does not already grant, without resorting to 'profileOverride' to replace the whole profile."
}
}
},
Expand Down
99 changes: 84 additions & 15 deletions src/wxc_common/src/config_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,13 @@ struct RawSandbox {
daemon_pipe_name: Option<String>,
}

#[derive(Deserialize, Default)]
#[serde(default)]
#[derive(Deserialize)]
struct RawPortMapping {
#[serde(rename = "windowsPort")]
windows_port: Option<u16>,
windows_port: u16,
#[serde(rename = "containerPort")]
container_port: Option<u16>,
container_port: u16,
#[serde(default)]
protocol: Option<String>,
}

Expand Down Expand Up @@ -1003,7 +1003,7 @@ fn convert_raw_config_inner(
}
config
});
let wslc = raw_exp.wslc.map(|cc| {
let wslc = if let Some(cc) = raw_exp.wslc {
let mut config = WslcConfig::default();
if let Some(os) = cc.target_os {
config.target_os = os;
Expand All @@ -1019,17 +1019,33 @@ fn convert_raw_config_inner(
}
config.storage_path = cc.storage_path;
if let Some(mappings) = cc.port_mappings {
config.port_mappings = mappings
.into_iter()
.map(|m| PortMapping {
windows_port: m.windows_port.unwrap_or(0),
container_port: m.container_port.unwrap_or(0),
protocol: m.protocol.unwrap_or_else(|| "tcp".to_string()),
})
.collect();
let mut converted = Vec::with_capacity(mappings.len());
for m in mappings {
if m.windows_port == 0 || m.container_port == 0 {
return Err(WxcError::ConfigParse(format!(
"experimental.wslc.portMappings: port 0 is not a valid forward (windowsPort={}, containerPort={})",
m.windows_port, m.container_port
)));
}
let protocol = m.protocol.unwrap_or_else(|| "tcp".to_string());
if protocol != "tcp" && protocol != "udp" {
return Err(WxcError::ConfigParse(format!(
"experimental.wslc.portMappings: protocol must be 'tcp' or 'udp', got '{}'",
protocol
)));
}
converted.push(PortMapping {
windows_port: m.windows_port,
container_port: m.container_port,
protocol,
});
}
config.port_mappings = converted;
}
config
});
Some(config)
} else {
None
};
let isolation_session = raw_exp.isolation_session.map(|as_cfg| {
let mut config = IsolationSessionConfig::default();
if let Some(id) = as_cfg.configuration_id {
Expand Down Expand Up @@ -2783,6 +2799,59 @@ mod tests {
);
}

#[test]
fn wslc_port_mappings_parsed() {
let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "wslc", "experimental": {"wslc": {"image": "python:3.12", "portMappings": [{"windowsPort": 8080, "containerPort": 80, "protocol": "tcp"}, {"windowsPort": 5353, "containerPort": 53, "protocol": "udp"}]}}}"#;
let encoded = base64_encode(json.as_bytes());
let mut logger = test_logger();

let req = load_request(&encoded, &mut logger, true).unwrap();
let wslc = req.experimental.wslc.unwrap();
assert_eq!(wslc.port_mappings.len(), 2);
assert_eq!(wslc.port_mappings[0].windows_port, 8080);
assert_eq!(wslc.port_mappings[0].container_port, 80);
assert_eq!(wslc.port_mappings[0].protocol, "tcp");
assert_eq!(wslc.port_mappings[1].protocol, "udp");
}

#[test]
fn wslc_port_mappings_default_protocol_is_tcp() {
let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "wslc", "experimental": {"wslc": {"image": "python:3.12", "portMappings": [{"windowsPort": 8080, "containerPort": 80}]}}}"#;
let encoded = base64_encode(json.as_bytes());
let mut logger = test_logger();

let req = load_request(&encoded, &mut logger, true).unwrap();
let wslc = req.experimental.wslc.unwrap();
assert_eq!(wslc.port_mappings[0].protocol, "tcp");
}

#[test]
fn wslc_port_mappings_missing_windows_port_rejected() {
let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "wslc", "experimental": {"wslc": {"image": "python:3.12", "portMappings": [{"containerPort": 80}]}}}"#;
let encoded = base64_encode(json.as_bytes());
let mut logger = test_logger();

assert!(load_request(&encoded, &mut logger, true).is_err());
}

#[test]
fn wslc_port_mappings_zero_port_rejected() {
let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "wslc", "experimental": {"wslc": {"image": "python:3.12", "portMappings": [{"windowsPort": 0, "containerPort": 80}]}}}"#;
let encoded = base64_encode(json.as_bytes());
let mut logger = test_logger();

assert!(load_request(&encoded, &mut logger, true).is_err());
}

#[test]
fn wslc_port_mappings_invalid_protocol_rejected() {
let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "wslc", "experimental": {"wslc": {"image": "python:3.12", "portMappings": [{"windowsPort": 8080, "containerPort": 80, "protocol": "sctp"}]}}}"#;
let encoded = base64_encode(json.as_bytes());
let mut logger = test_logger();

assert!(load_request(&encoded, &mut logger, true).is_err());
}

// ---------- Experimental feature tests ----------

#[test]
Expand Down
Loading