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
22 changes: 22 additions & 0 deletions docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ production configs and the dev schema when working on experimental features:
"deniedPaths": ["C:\\Windows"] // Blocked paths
},

"fallback": {
"allowDaclMutation": true // Allow Tier 3 DACL fallback (default true)
},

"network": {
"defaultPolicy": "block", // "allow" or "block"
"enforcementMode": "firewall", // "capabilities", "firewall", or "both"
Expand Down Expand Up @@ -77,6 +81,24 @@ production configs and the dev schema when working on experimental features:
}
```

### Filesystem Policy

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.

This document does not define all policies. Adding filesystem is ragged. We should probably add an issue to add documentation on all of the schema sections.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Filed #285 to track full schema documentation coverage. Will keep this PR's scope limited to the new allowDaclFallback field.


The `filesystem` section defines path access policy shared across backends:

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `readwritePaths` | string[] | `[]` | Paths the process can read and write. |
| `readonlyPaths` | string[] | `[]` | Paths the process can read but not write. |
| `deniedPaths` | string[] | `[]` | Paths the process cannot access at all. |

### Fallback Policy

The `fallback` section gates the runner's host-impacting fallbacks. Each flag is an explicit operator consent for a specific mechanism the runner may otherwise pick when the preferred primitive is unavailable. Defaults preserve the pre-fallback-section behavior (all permitted).

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `allowDaclMutation` | boolean | `true` | When the BaseContainer API is absent and `bfscfg.exe` is unavailable, allow MXC to apply DACL ACEs on policy paths (Tier 3 fallback). **⚠️ This modifies host filesystem security descriptors**; original DACLs are restored on exit. Set to `false` to refuse this fallback β€” the run will then fail on machines that require Tier 3 (e.g., pre-GE Windows 11 builds without the BaseContainer API). |

### Containment Backends

The `containment` field accepts both **abstract intent values** (which the
Expand Down
12 changes: 12 additions & 0 deletions schemas/dev/mxc-config.schema.0.6.0-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@
}
},

"fallback": {
"type": "object",
"description": "Operator consent for host-impacting containment fallbacks. Each flag gates a specific fallback mechanism the runner may otherwise pick when the preferred primitive is unavailable. Defaults preserve the pre-fallback-section behavior (all permitted).",
"properties": {
"allowDaclMutation": {
"type": "boolean",
"default": true,
"description": "When the BaseContainer API is absent and bfscfg.exe is unavailable, allow MXC to apply DACL ACEs on policy paths (Tier 3 fallback). MODIFIES HOST FILESYSTEM SECURITY DESCRIPTORS β€” original DACLs are restored on exit. Set to false to refuse this fallback (the run will fail on machines that need Tier 3)."
}
}
},

"network": {
"type": "object",
"description": "Network access policy. Shared across all backends.",
Expand Down
51 changes: 51 additions & 0 deletions src/wxc_common/src/config_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ struct RawFilesystem {
denied_paths: Option<Vec<String>>,
}

#[derive(Deserialize, Default)]
#[serde(default)]
struct RawFallback {
#[serde(rename = "allowDaclMutation")]
allow_dacl_mutation: Option<bool>,
}

#[derive(Deserialize, Default)]
#[serde(default)]
struct RawNetwork {
Expand Down Expand Up @@ -205,6 +212,7 @@ struct RawConfig {
process_container: Option<RawProcessContainer>,
lxc: Option<RawLxc>,
filesystem: Option<RawFilesystem>,
fallback: Option<RawFallback>,
network: Option<RawNetwork>,
ui: Option<RawUi>,
experimental: Option<RawExperimental>,
Expand All @@ -230,6 +238,8 @@ struct RawStateAwareRequest {
#[serde(default)]
filesystem: Option<RawFilesystem>,
#[serde(default)]
fallback: Option<RawFallback>,
#[serde(default)]
network: Option<RawNetwork>,
#[serde(default)]
ui: Option<RawUi>,
Expand Down Expand Up @@ -728,6 +738,13 @@ fn convert_raw_config_inner(
}
validate_filesystem_paths(&policy, logger)?;

// Fallback section
if let Some(fbcfg) = raw.fallback {
if let Some(v) = fbcfg.allow_dacl_mutation {
policy.fallback.allow_dacl_mutation = v;
}
}

// Network section
if let Some(net) = raw.network {
if let Some(proxy_value) = net.proxy {
Expand Down Expand Up @@ -930,6 +947,7 @@ fn convert_raw_state_aware(
process_container: None,
lxc: None,
filesystem: raw.filesystem,
fallback: raw.fallback,
network: raw.network,
ui: raw.ui,
// The state-aware experimental block has a different shape from the
Expand Down Expand Up @@ -1519,6 +1537,39 @@ mod tests {
assert_eq!(req.script_timeout, 0);
}

#[test]
fn allow_dacl_mutation_default_true() {
let json = r#"{"process": {"commandLine": "echo hi"}}"#;
let encoded = base64_encode(json.as_bytes());
let mut logger = test_logger();
let req = load_request(&encoded, &mut logger, true).unwrap();
assert!(req.policy.fallback.allow_dacl_mutation);
}

#[test]
fn allow_dacl_mutation_explicit_false() {
let json = r#"{
"process": {"commandLine": "echo hi"},
"fallback": {"allowDaclMutation": false}
}"#;
let encoded = base64_encode(json.as_bytes());
let mut logger = test_logger();
let req = load_request(&encoded, &mut logger, true).unwrap();
assert!(!req.policy.fallback.allow_dacl_mutation);
}

#[test]
fn allow_dacl_mutation_explicit_true() {
let json = r#"{
"process": {"commandLine": "echo hi"},
"fallback": {"allowDaclMutation": true}
}"#;
let encoded = base64_encode(json.as_bytes());
let mut logger = test_logger();
let req = load_request(&encoded, &mut logger, true).unwrap();
assert!(req.policy.fallback.allow_dacl_mutation);
}

// ====== Containment backend selection tests ======

#[test]
Expand Down
26 changes: 25 additions & 1 deletion src/wxc_common/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,38 @@ impl Default for BaseProcessUiConfig {
}
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
/// Operator consent for host-impacting containment fallbacks. Each flag gates
/// a specific fallback the runner may otherwise pick when the preferred
/// primitive is unavailable. Defaults preserve the pre-fallback-section
/// behavior (all permitted).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FallbackPolicy {
/// When the BaseContainer API is absent and `bfscfg.exe` is unavailable,
/// allow MXC to apply DACL ACEs on policy paths (Tier 3 fallback). This
/// modifies host filesystem security descriptors; original DACLs are
/// restored on exit. Defaults to `true`. Set to `false` to refuse the
/// fallback (the run will fail on machines that require Tier 3).
pub allow_dacl_mutation: bool,
}

impl Default for FallbackPolicy {
fn default() -> Self {
Self {
allow_dacl_mutation: true,
}
}
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ContainerPolicy {
pub least_privilege_mode: bool,
pub capabilities: Vec<String>,
pub readwrite_paths: Vec<String>,
pub readonly_paths: Vec<String>,
pub denied_paths: Vec<String>,
pub fallback: FallbackPolicy,
pub default_network_policy: NetworkPolicy,
pub network_enforcement_mode: NetworkEnforcementMode,
pub allowed_hosts: Vec<String>,
Expand Down
Loading