Relevant area(s)
Windows (WSLc backend)
Brief description of your issue
The one-shot WSLc flow (WSLContainerRunner::start_container) and the state-aware WSLc flow (WslcStateAwareRunner + daemon) diverge on two networking behaviors that should be identical. A full one-shot-vs-state-aware policy comparison found filesystem handling is identical and the cooperative proxy is enforced identically (same apply_cooperative_proxy_env on the workload; only the declaration site differs, which is structural and acceptable). Two real gaps remain:
Gap 1 — Redundant host lists are handled inconsistently (fail-open in one-shot).
Both flows already reject meaningful per-host filtering at parse time (config_parser.rs:1064, needs_host_filtering(), added in #681). But a redundant host list — defaultPolicy: "block" + blockedHosts, or defaultPolicy: "allow" + allowedHosts — passes that gate (needs_host_filtering() → false):
- One-shot
validate_runner (wsl_container_runner.rs:636) also uses needs_host_filtering(), so it accepts and silently ignores the list.
- State-aware
reject_host_filtering (policy.rs:115) rejects on any non-empty list.
This is a fail-open footgun: a user who writes defaultPolicy: "allow" + allowedHosts: ["github.com"] (a classic "I meant an allowlist" typo) gets all egress permitted in one-shot, with no error — the opposite of intent.
Gap 2 — State-aware silently drops experimental.wslc.portMappings.
One-shot forwards ports (wsl_container_runner.rs:1491 → WslcSetContainerSettingsPortMappings). The state-aware provision config WslcProvisionPhase (wire.rs:550) deliberately omits portMappings, and because the experimental block is permissive (no deny_unknown_fields), a state-aware config carrying portMappings is silently ignored — no inbound forwarding, no error (container_steps.rs:803 create_daemon_container passes &[]).
Steps to reproduce
Gap 1:
- Run a one-shot WSLc config with
network: { "defaultPolicy": "allow", "allowedHosts": ["github.com"] }.
- Observe the container starts and all egress is permitted (the
allowedHosts list is ignored).
- Run the same policy through the state-aware provision phase — it is rejected with
policy_validation.
Gap 2:
- Run a one-shot WSLc config with
experimental.wslc.portMappings — the host→container port forward is configured.
- Provide the same
portMappings to a state-aware provision config.
- Observe no port forwarding is set up and no error is raised.
Expected behavior
- Gap 1: Both flows handle host lists identically, failing closed — any non-empty
allowedHosts/blockedHosts on WSLc is rejected (WSLc cannot enforce per-host filtering).
- Gap 2: Both flows support
experimental.wslc.portMappings, or state-aware explicitly rejects it rather than silently ignoring it.
Actual behavior
- Gap 1: One-shot accepts and silently ignores redundant host lists (fail-open); state-aware rejects any non-empty list. Behavior differs.
- Gap 2: One-shot forwards ports; state-aware silently drops
portMappings.
Proposed actions
Gap 1 — make one-shot reject (align both flows to fail-closed):
- Change the shared parser predicate
config_parser.rs:1064 from policy.needs_host_filtering() to reject any non-empty allowed_hosts/blocked_hosts for WSLc; reword the error message (drop the "allowedHosts with defaultPolicy='block'…" framing).
- Mirror the same predicate in one-shot
validate_runner (wsl_container_runner.rs:636) for the parser-bypass path.
- Flip the redundant-list tests (
config_parser.rs WSLc host-filtering tests, wsl_container_runner.rs validate_runner_* tests) to expect rejection; keep the bare-defaults-accepted tests.
- State-aware
reject_host_filtering (policy.rs:115) already matches — no change.
Gap 2 — make state-aware support portMappings (align to one-shot):
- Add
port_mappings to WslcProvisionPhase (wire.rs:550) and regenerate the dev schema + SDK wire types (mxc_schema_gen).
- Add
port_mappings: Vec<PortMapping> to the daemon ProvisionConfig (daemon_protocol.rs:75).
- Thread it:
state_aware.rs::provision → ProvisionConfig → session_manager.rs → create_daemon_container (container_steps.rs:803), passing the slice into ContainerSettings::build (already supports port_mappings, container_steps.rs:499) instead of &[].
- Add the SDK type (
sdk/node/src/state-aware-types.ts WslcProvisionConfig) and an E2E state-aware port-mapping config.
Both are self-contained: Gap 1 is tiny (one predicate + tests); Gap 2 is a field threaded through the daemon wire protocol into container creation.
Relevant area(s)
Windows (WSLc backend)
Brief description of your issue
The one-shot WSLc flow (
WSLContainerRunner::start_container) and the state-aware WSLc flow (WslcStateAwareRunner+ daemon) diverge on two networking behaviors that should be identical. A full one-shot-vs-state-aware policy comparison found filesystem handling is identical and the cooperative proxy is enforced identically (sameapply_cooperative_proxy_envon the workload; only the declaration site differs, which is structural and acceptable). Two real gaps remain:Gap 1 — Redundant host lists are handled inconsistently (fail-open in one-shot).
Both flows already reject meaningful per-host filtering at parse time (
config_parser.rs:1064,needs_host_filtering(), added in #681). But a redundant host list —defaultPolicy: "block"+blockedHosts, ordefaultPolicy: "allow"+allowedHosts— passes that gate (needs_host_filtering()→ false):validate_runner(wsl_container_runner.rs:636) also usesneeds_host_filtering(), so it accepts and silently ignores the list.reject_host_filtering(policy.rs:115) rejects on any non-empty list.This is a fail-open footgun: a user who writes
defaultPolicy: "allow"+allowedHosts: ["github.com"](a classic "I meant an allowlist" typo) gets all egress permitted in one-shot, with no error — the opposite of intent.Gap 2 — State-aware silently drops
experimental.wslc.portMappings.One-shot forwards ports (
wsl_container_runner.rs:1491→WslcSetContainerSettingsPortMappings). The state-aware provision configWslcProvisionPhase(wire.rs:550) deliberately omitsportMappings, and because theexperimentalblock is permissive (nodeny_unknown_fields), a state-aware config carryingportMappingsis silently ignored — no inbound forwarding, no error (container_steps.rs:803create_daemon_container passes&[]).Steps to reproduce
Gap 1:
network: { "defaultPolicy": "allow", "allowedHosts": ["github.com"] }.allowedHostslist is ignored).policy_validation.Gap 2:
experimental.wslc.portMappings— the host→container port forward is configured.portMappingsto a state-aware provision config.Expected behavior
allowedHosts/blockedHostson WSLc is rejected (WSLc cannot enforce per-host filtering).experimental.wslc.portMappings, or state-aware explicitly rejects it rather than silently ignoring it.Actual behavior
portMappings.Proposed actions
Gap 1 — make one-shot reject (align both flows to fail-closed):
config_parser.rs:1064frompolicy.needs_host_filtering()to reject any non-emptyallowed_hosts/blocked_hostsfor WSLc; reword the error message (drop the "allowedHosts with defaultPolicy='block'…" framing).validate_runner(wsl_container_runner.rs:636) for the parser-bypass path.config_parser.rsWSLc host-filtering tests,wsl_container_runner.rsvalidate_runner_*tests) to expect rejection; keep the bare-defaults-accepted tests.reject_host_filtering(policy.rs:115) already matches — no change.Gap 2 — make state-aware support
portMappings(align to one-shot):port_mappingstoWslcProvisionPhase(wire.rs:550) and regenerate the dev schema + SDK wire types (mxc_schema_gen).port_mappings: Vec<PortMapping>to the daemonProvisionConfig(daemon_protocol.rs:75).state_aware.rs::provision→ProvisionConfig→session_manager.rs→create_daemon_container(container_steps.rs:803), passing the slice intoContainerSettings::build(already supportsport_mappings,container_steps.rs:499) instead of&[].sdk/node/src/state-aware-types.tsWslcProvisionConfig) and an E2E state-aware port-mapping config.Both are self-contained: Gap 1 is tiny (one predicate + tests); Gap 2 is a field threaded through the daemon wire protocol into container creation.