Skip to content

State-aware exec streaming contract gaps (FFI in-process + CLI executor framing) #843

Description

Description of the task

Track and fix the state-aware exec streaming contract gaps surfaced as review comments on #806 and #810. These are three distinct failure modes that share one theme — how exec output and typed errors are framed across the executor/library boundary — and are best fixed together because they touch the same code.

All three still stand on main after #829 ("Relay live exec pipes to executor stdio"), which added the ExecConsumer (Executor vs Library) plumbing and a generic relay_exec_to_stdio, but did not update WSLc's exec (it takes _consumer: ExecConsumer and still returns null pipes) or change the executor's stdout error framing.

1. The require_experimental_optin gate blocks the in-process (FFI/Rust-SDK) path.
require_experimental_optin (src/core/mxc_engine/src/state_aware.rs:43,52,73,109) rejects the experimental backends (WSLc / IsolationSession / WindowsSandbox) unless request.experimental_enabled is set. The CLI sets this from --experimental, but the JSON/FFI entry points parse via convert_wire_state_awareconvert_wire_config, which hardcodes experimental_enabled = false (src/core/wxc_common/src/config_parser.rs:1335). mxc_sdk::exec_sandbox / mxc_state_aware_exec expose no opt-in. Result: every in-process SDK/FFI exec against a wslc: (or IsolationSession/WindowsSandbox) sandbox returns backend_unavailable before dispatch.

Fix (small, ~2 lines): set experimental_enabled = true on the trusted JSON/FFI entry points after parse (linking the native library is itself the opt-in), or thread an explicit opt-in through the SDK/FFI request path.

2. WSLc exec returns null pipe handles and blocks (the ExecConsumer::Library path).
WslcStateAwareRunner::exec (src/backends/wslc/common/src/state_aware.rs:135-206) relays the daemon stream synchronously into process-global stdout/stderr, blocks to exit, then returns an ExecHandle with null_pipe_handle() for stdout/stderr/stdin. Post-#829 it receives an ExecConsumer but ignores it (_consumer, line 140). Correct for the CLI/ExecConsumer::Executor path (self-relay + null pipes is the documented call-through), but the FFI path (state_aware_dispatch.rs:97 passes ExecConsumer::Library) wraps the handle in ExecSandboxProcess, which expects live pipes + non-blocking spawn — so mxc_state_aware_exec blocks until exit and take_stdout/take_stderr return None, contradicting the live MxcSandboxProcess contract.

Fix (moderate, ~150-250 lines): when consumer == ExecConsumer::Library, create real OS pipes, run exec_streaming on a background thread whose callback writes to the pipe write-ends, and return an ExecHandle carrying the read-ends + a waiter that joins the thread. Keep the ExecConsumer::Executor branch on the internal self-relay (or hand live pipes to the generic relay_exec_to_stdio #829 landed). Add stdin forwarding and handle-ownership care. IsolationSession and WindowsSandbox share the identical null-pipe pattern and should adopt the same shared helper.

3. Executor stdout error-framing collision on post-admission failures (ExecConsumer::Executor).
On the CLI/executor path, exec streams user bytes to stdout as they arrive. On a post-admission failure (timeout mid-output, daemon truncation, backend failure after some bytes streamed), exec_streaming returns Err, and run_state_aware_main finalizes via StateAwareExit::Error(json)println!(envelope); exit(1) (src/core/wxc/src/main.rs:457,484) — appending the JSON error envelope to the same stdout that already carries user output. On the SDK side, execInSandboxAsync calls tryParseErrorEnvelope, which runs JSON.parse(stdout.trim()) over the whole buffer (sdk/node/src/state-aware-helper.ts:172-185); mixed [user output] + {"error":…} is not valid JSON → returns null → no typed MxcError is thrown → the call resolves with { exitCode: 1, stdout: <mixed> }. Pre-admission failures (bad policy, sandbox not found) still stream nothing and produce a clean envelope, so only post-admission failures are affected.

Fix (moderate; coordinated Rust + SDK): give the exec path separable framing for post-admission failures — e.g., route the exec error envelope to stderr (leaving user output as the sole owner of stdout) with the SDK reading the typed error from stderr, or length-prefix/frame the streams. Whatever is chosen must keep pre-admission envelopes working and keep the CLI human-readable.

Additional context

Metadata

Metadata

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions