diff --git a/crates/openshell-driver-mxc/README.md b/crates/openshell-driver-mxc/README.md
index b8c52f9898..7f595bf19c 100644
--- a/crates/openshell-driver-mxc/README.md
+++ b/crates/openshell-driver-mxc/README.md
@@ -23,7 +23,7 @@ it does not implement the Linux `ConnectSupervisor` protocol.
|---|---|
| Filesystem policy | Read-only/read-write grants come only from `SandboxPolicy`. `process_container` enforces default-deny; `isolation_session` is an explicit grant-only compatibility mode. |
| UI policy | `process_container` advertises complete support and maps portable graphical UI, clipboard-direction, and input-injection controls to MXC; omitted fields inside an explicit section deny. `isolation_session` advertises no support, so the gateway rejects any explicit section before provisioning. |
-| Network policy | With `egress_proxy = true` on `process_container`, split into MXC 0.8 loopback-only egress plus the full policy enforced by a per-sandbox OpenShell host CONNECT proxy. The driver injects proxy environment variables for proxy-aware clients; direct Internet access remains denied by MXC. Otherwise rejected synchronously. `isolation_session` remains fail-closed. |
+| Network policy | With `egress_proxy = true` on `process_container`, an explicit `network_policies` rule activates MXC 0.8 loopback-only egress plus the full policy enforced by a per-sandbox OpenShell host CONNECT proxy. The driver injects proxy environment variables for proxy-aware clients; direct Internet access remains denied by MXC. A policy without network rules does not activate the proxy. Otherwise rejected synchronously. `isolation_session` remains fail-closed. |
| Provider credentials | The child receives revision-scoped placeholders and non-secret provider environment only. The per-sandbox host proxy retains the resolver and substitutes credentials only for their bound endpoints. |
| Process policy | Unsupported; MXC supplies OS isolation only. |
| Dynamic forwarding | Supported through `openshell-supervisor-relay`; interactive exec/connect remain unsupported. |
@@ -60,6 +60,10 @@ pc_relay_target_port = 0
# (SYSTEMROOT/WINDIR/PATH/COMSPEC/LOCALAPPDATA); pc_minimal_env starts from an
# EMPTY env for runtimes that need a fully curated per-sandbox environment.
pc_minimal_env = false
+# processContainer only: compatibility fallback for unrestricted outbound TCP.
+# A sandbox with egress_proxy enabled but no explicit network rules rejects
+# this fallback instead of silently changing governed egress to allow-all.
+pc_network_allow = false
# processContainer only: include "allowLocalNetwork": true in the MXC
# network section. This compatibility setting broadens network access and is
# not required by the BaseContainer qualification profile.
@@ -72,8 +76,14 @@ etw_audit = false
```
When `egress_proxy` is enabled, `egress_proxy_addr` must be a loopback
-`IP:PORT` seed. The driver preserves the configured IP and allocates a unique
-ephemeral port for each sandbox's authenticated host CONNECT proxy.
+`IP:PORT` seed. For policies with explicit network rules, the driver preserves
+the configured IP and allocates a unique ephemeral port for that sandbox's
+authenticated host CONNECT proxy.
+
+`pc_network_allow = true` is an explicit unrestricted-egress compatibility
+fallback. If it is combined with `egress_proxy = true`, a sandbox policy
+without explicit network rules is rejected synchronously rather than falling
+through from governed egress to `defaultPolicy = "allow"`.
Supply workload settings for each sandbox. The public config is keyed by driver name; the gateway forwards only the inner `mxc` object to the driver:
@@ -87,7 +97,7 @@ The `command` array is required and preserves Windows argument boundaries. `cwd`
UI capability (Win32k syscalls, clipboard, input injection) is a `SandboxPolicy` concern, not gateway TOML -- see the Capability Matrix above and `docs/reference/policy-schema.mdx`'s `ui` section. Defaults to disabled (Win32k syscall lockdown) when a policy has no explicit `ui:` section; set `allow_graphical_ui: true` for agents that touch user32/gdi32 at startup even without opening a real window (e.g. Node.js-based targets like OpenClaw's gateway -- see `examples/e2e-policies/openclaw-gateway.yaml`).
-`egress_proxy_addr` must be a `127.0.0.1:PORT` address. The port acts only as a configuration seed: the driver reserves a unique ephemeral loopback port for every sandbox. MXC 0.8 denies direct Internet egress and permits `127.0.0.1/32`; the driver points proxy-aware clients at the per-sandbox listener using environment variables. The current policy permits all loopback ports, so sandboxes can also reach unrelated host services bound to loopback. Control-channel forwarding does not require the legacy reverse-WebSocket connections to fresh host ports; restricting the generated policy is separate hardening work. Do not treat this path as loopback-service isolation. Live policy replacement or merge updates remain unsupported; delete and recreate the sandbox to apply a different policy.
+`egress_proxy_addr` must be a `127.0.0.1:PORT` address. The port acts only as a configuration seed: for a sandbox policy with explicit network rules, the driver reserves a unique ephemeral loopback port. MXC 0.8 denies direct Internet egress and permits `127.0.0.1/32`; the driver points proxy-aware clients at the per-sandbox listener using environment variables. A policy without network rules keeps MXC's default network posture and receives neither a host listener nor proxy environment variables. The current governed-egress policy permits all loopback ports, so governed sandboxes can also reach unrelated host services bound to loopback. Control-channel forwarding does not require the legacy reverse-WebSocket connections to fresh host ports; restricting the generated policy is separate hardening work. Do not treat this path as loopback-service isolation. Live policy replacement or merge updates remain unsupported; delete and recreate the sandbox to apply a different policy.
When `etw_audit` is enabled, each gateway process owns a distinct real-time ETW
session named from the stable `OpenShell-MXC-ETW` prefix, its process ID, and a
@@ -142,8 +152,9 @@ or MXC-specific gateway composition variant. Provider resolver state uses a
separate, create-scoped in-process handoff because it intentionally cannot be
represented in the public compute-driver protobuf.
-When `egress_proxy` is enabled, `EmbeddedPolicyMapper` uses `split_policy`
-instead: MXC receives filesystem grants plus loopback-only egress,
+When `egress_proxy` is enabled and the policy contains explicit network rules,
+`EmbeddedPolicyMapper` uses `split_policy` instead: MXC receives filesystem
+grants plus loopback-only egress,
and the driver starts a host CONNECT proxy from the trimmed
network-only `SandboxPolicy`. Policies containing `network_middlewares` are
rejected synchronously until this host-proxy path can receive the gateway's
diff --git a/crates/openshell-driver-mxc/examples/e2e-policies/ws-agent.yaml b/crates/openshell-driver-mxc/examples/e2e-policies/ws-agent.yaml
index 6160be6f5c..683a8c0c66 100644
--- a/crates/openshell-driver-mxc/examples/e2e-policies/ws-agent.yaml
+++ b/crates/openshell-driver-mxc/examples/e2e-policies/ws-agent.yaml
@@ -10,9 +10,8 @@
# system paths and share_dir).
# - TCP socket binding on port 22000 (governed by pc_capabilities in the
# gateway TOML, not by filesystem policy here).
-# - Outbound TCP through the egress proxy, for openshell-supervisor-relay
-# to dial the driver's on-demand relay — governed by egress_proxy in the
-# TOML.
+# - Private/loopback client access for openshell-supervisor-relay to dial
+# the driver's on-demand relay — granted by pc_capabilities in the TOML.
# - No writes to the host filesystem.
#
# workload directory (passed by run-ws-agent-test.ps1, default C:\work\openshell-mxc-ws)
@@ -24,15 +23,9 @@
# patches this path and the per-sandbox driver config when -AgentDir overrides
# the default below.
#
-# This example intentionally omits network_policies, not because the driver
-# would reject it: with egress_proxy = true (set in mxc-ws-gateway.toml), the
-# driver takes the lossless split path (policy_map::split_policy) and
-# delegates network_policies verbatim to the OpenShell host CONNECT proxy for
-# enforcement -- an "info" loss item, not an error, so it would be accepted.
-# (Only the no-proxy coarse path, or an unsupported rule shape, can turn a
-# network_policies entry into a rejected "error" loss item -- see
-# policy_map/map.rs.) This scenario just doesn't need host-enforced network
-# rules beyond the loopback/pc_capabilities grant above.
+# This example intentionally omits network_policies because it does not need
+# governed Internet egress. The driver therefore does not start a host CONNECT
+# proxy or inject proxy environment variables for this sandbox.
version: 1
filesystem_policy:
diff --git a/crates/openshell-driver-mxc/examples/mxc-ws-agent.rs b/crates/openshell-driver-mxc/examples/mxc-ws-agent.rs
index dbd63f1426..1c09e7956a 100644
--- a/crates/openshell-driver-mxc/examples/mxc-ws-agent.rs
+++ b/crates/openshell-driver-mxc/examples/mxc-ws-agent.rs
@@ -282,7 +282,8 @@ async fn run_relay_proxy(relay_url: String, local_url: String, mut stop_rx: ones
const LOCAL_CONNECT_ATTEMPTS: u32 = 15;
const LOCAL_CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
const LOCAL_CONNECT_BACKOFF: Duration = Duration::from_millis(300);
- // Connect to the gateway relay (outbound via egress_proxy).
+ // Connect directly to the gateway relay over the private network access
+ // granted by the qualification profile's privateNetworkClientServer capability.
let relay_ws = match tokio_tungstenite::connect_async(&relay_url).await {
Ok((ws, _)) => {
eprintln!("[spawner] relay connected: {relay_url}");
diff --git a/crates/openshell-driver-mxc/examples/mxc-ws-gateway.toml b/crates/openshell-driver-mxc/examples/mxc-ws-gateway.toml
index afd04a44cd..8a2aa4e9b8 100644
--- a/crates/openshell-driver-mxc/examples/mxc-ws-gateway.toml
+++ b/crates/openshell-driver-mxc/examples/mxc-ws-gateway.toml
@@ -48,11 +48,10 @@ pc_capabilities = ["privateNetworkClientServer"]
# process_container only: keep standard privilege level (not LPA).
pc_least_privilege = false
-# Egress proxy for outbound TCP connectivity -- required for
-# openshell-supervisor-relay to dial out to the driver's on-demand relay
-# (see mxc-openclaw-gateway.toml, which uses the same pattern).
-egress_proxy = true
-egress_proxy_addr = "127.0.0.1:18080"
+# No governed Internet egress is needed. The relay reaches the driver's
+# on-demand private-interface listener through privateNetworkClientServer above.
+egress_proxy = false
+egress_proxy_addr = ""
# The workload command and cwd are supplied per sandbox by
# run-ws-agent-test.ps1 through `sandbox create --driver-config-json`.
diff --git a/crates/openshell-driver-mxc/examples/ocsf-audit.yaml b/crates/openshell-driver-mxc/examples/ocsf-audit.yaml
index 13e7cd571e..e036cc90ef 100644
--- a/crates/openshell-driver-mxc/examples/ocsf-audit.yaml
+++ b/crates/openshell-driver-mxc/examples/ocsf-audit.yaml
@@ -7,9 +7,10 @@
# else is default-deny. run-ocsf-audit.ps1 copies this policy into the result
# bundle and replaces the default grant with -ShareDir for that run.
#
-# No network_policies block is needed here: the per-sandbox egress proxy is driven
-# by `egress_proxy = true` in mxc-ocsf-audit.toml (that is what makes MXC emit the
-# SandboxProxyConfigured event we map to OCSF), not by a policy rule.
+# The explicit network rule activates the per-sandbox egress proxy configured
+# in mxc-ocsf-audit.toml. That proxy configuration makes MXC emit the
+# SandboxProxyConfigured event mapped to OCSF. run-ocsf-audit.ps1 removes this
+# block for its explicit -NoProxy comparison.
version: 1
filesystem_policy:
@@ -17,3 +18,13 @@ filesystem_policy:
read_only: []
read_write:
- "C:/work/openshell-mxc-demo"
+
+network_policies:
+ audit_probe:
+ name: audit-probe
+ endpoints:
+ - host: example.com
+ port: 443
+ protocol: tcp
+ binaries:
+ - path: "C:/Windows/System32/cmd.exe"
diff --git a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1
index 591c38bf24..00d32e10ef 100644
--- a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1
+++ b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1
@@ -144,6 +144,9 @@ try {
$shareDirPolicy = $ShareDir.Replace('\', '/')
$shareDirJson = ConvertTo-Json $shareDirPolicy -Compress
$policyText = Get-Content $policySrc -Raw
+ if (-not $proxyOn) {
+ $policyText = [regex]::Replace($policyText, '(?ms)^network_policies:\s*.*\z', '')
+ }
$defaultGrant = ' - "C:/work/openshell-mxc-demo"'
if (-not $policyText.Contains($defaultGrant)) {
throw "policy template does not contain the expected default ShareDir grant"
diff --git a/crates/openshell-driver-mxc/examples/run-ws-agent-test.ps1 b/crates/openshell-driver-mxc/examples/run-ws-agent-test.ps1
index 0564d17fc5..0372864928 100644
--- a/crates/openshell-driver-mxc/examples/run-ws-agent-test.ps1
+++ b/crates/openshell-driver-mxc/examples/run-ws-agent-test.ps1
@@ -359,15 +359,12 @@ try {
}
Ok "wxc-exec: $WxcExecPath"
- # A real run exercises process_container with egress_proxy = true
- # (mxc-ws-gateway.toml). MXC schema 0.8.0-alpha's network_json()
- # (mxc.rs) now emits a direct egress.allow rule for 127.0.0.0/8
- # instead of runtimeConfig.networkProxy when a proxy is configured,
- # so the driver no longer calls the elevation-only
- # NetworkIsolationSetAppContainerConfig -- process_container +
- # egress_proxy selects the BaseContainer/PSEC tier and runs
- # non-elevated. Elevation is therefore no longer required here; keep
- # logging the elevation state for diagnostics only.
+ # A real run exercises process_container with egress_proxy disabled
+ # (mxc-ws-gateway.toml). The sandbox connects directly to the driver's
+ # route-selected private-interface relay listener through the
+ # privateNetworkClientServer capability; the governed host CONNECT
+ # proxy is not part of this qualification path. Elevation is not
+ # required here; keep logging the elevation state for diagnostics only.
$wid = [Security.Principal.WindowsIdentity]::GetCurrent()
$wp = New-Object Security.Principal.WindowsPrincipal($wid)
$admin = $wp.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
diff --git a/crates/openshell-driver-mxc/src/driver.rs b/crates/openshell-driver-mxc/src/driver.rs
index 072b1debf1..513a92d858 100644
--- a/crates/openshell-driver-mxc/src/driver.rs
+++ b/crates/openshell-driver-mxc/src/driver.rs
@@ -82,7 +82,10 @@ pub struct MxcComputeConfig {
/// `processContainer` only: inject a network section with
/// `defaultPolicy: "allow"` so the `AppContainer` has unrestricted outbound
/// TCP access. Required when `pc_capabilities` alone is insufficient to
- /// enable network access in the target wxc-exec build.
+ /// enable network access in the target wxc-exec build. When `egress_proxy`
+ /// is also enabled, sandbox policies without explicit network rules are
+ /// rejected instead of falling back from governed egress to unrestricted
+ /// access.
pub pc_network_allow: bool,
/// `processContainer` only: include `"allowLocalNetwork": true` in the
/// MXC network section. Required for node.js (and other runtimes that
@@ -123,9 +126,11 @@ pub struct MxcComputeConfig {
/// MXC `configurationId` for isolation session. Default: `"composable"`.
/// Never use `"small"` (known OS bug).
pub default_configuration_id: String,
- /// Enable Pattern-C governed egress. When true, MXC permits loopback-only
- /// egress, the driver injects proxy environment variables, and the host
- /// CONNECT proxy receives the full network policy.
+ /// Enable Pattern-C governed egress for sandbox policies that contain
+ /// explicit network rules. MXC permits loopback-only egress, the driver
+ /// injects proxy environment variables, and the host CONNECT proxy receives
+ /// the full network policy. Policies without network rules do not start a
+ /// listener or receive proxy environment variables.
pub egress_proxy: bool,
/// Loopback `IP:PORT` seed for MXC `network.proxy` while governed egress is
/// enabled. The driver preserves the loopback IP and allocates a unique
@@ -462,6 +467,26 @@ fn configured_egress_addr(config: &MxcComputeConfig) -> Result