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
17 changes: 15 additions & 2 deletions sdk/dotnet/Microsoft.Mxc.Sdk/MxcLifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,16 @@ public static MxcSandboxProcess ExecInSandbox(SandboxId id, string command)
{
NativeSandbox* handle = null;
MxcErrorDetail error = default;
var status = NativeMethods.mxc_state_aware_exec(requestPtr, &handle, &error);
// `experimental` is 0 deliberately. Two things are missing: the engine's
// `isolation_session` feature is not enabled in the library this project
// builds, so that backend has no dispatch arm and the request ends at
// `unsupported_phase`; and the request shape emitted here predates that
// backend's Preview migration. Passing 1 without fixing both trades
// `backend_unavailable` for `unsupported_phase`, which
// `AssertNoUsableBackend` already tolerates β€” the tests would stay green
// while nothing worked. Change it together with the feature and the shape.
var status = NativeMethods.mxc_state_aware_exec(
requestPtr, /*experimental*/ 0, &handle, &error);
if (status != (int)ErrorCode.Success)
{
// See MxcSandbox.Spawn: the release belongs in `finally` so a throw
Expand Down Expand Up @@ -250,7 +259,11 @@ private static void SetBackendConfig(JsonObject envelope, string phase, string k
fixed (byte* requestPtr = requestBuf)
{
MxcStateAwareResult result = default;
var status = NativeMethods.mxc_state_aware(requestPtr, /*dry_run*/ 0, &result);
// `experimental` is 0 for the reason given in ExecInSandbox: the backend
// is not compiled into this build and the request shape is stale, so
// opting in would only change which error surfaces.
var status = NativeMethods.mxc_state_aware(
requestPtr, /*dry_run*/ 0, /*experimental*/ 0, &result);
try
{
if (status != (int)ErrorCode.Success)
Expand Down
19 changes: 9 additions & 10 deletions src/backends/isolation_session/common/src/state_aware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,18 +263,17 @@ impl StatefulSandboxBackend for IsolationSessionRunner {
///
/// 1. It needs a host running the OS-side isolation-session service, which
/// CI and most dev machines do not have.
/// 2. More fundamentally, the path is unreachable through the public
/// in-process entry points even *on* such a host:
/// `mxc_engine::exec_state_aware` calls `require_experimental_optin`,
/// and `config_parser` hardcodes `experimental_enabled: false` with the
/// only setter sitting on the one-shot builder. Until the state-aware
/// entry points take an `experimental` parameter, every in-process call
/// against this backend is rejected before it reaches `exec`.
/// 2. The path additionally needs the `mxc_engine/isolation_session`
/// feature to be forwarded by `mxc-sdk`, which declares only `wslc`
/// today β€” so an in-process caller cannot select this backend even on
/// such a host. The experimental gate is no longer the obstacle: the
/// state-aware entry points now take an `experimental` parameter, so
/// `require_experimental_optin` is satisfiable from a library caller.
///
/// A test could fake its way past (2) by building the request in-crate with
/// the flag set, but that scaffolding would have to be deleted as soon as
/// the real opt-in lands. The end-to-end proof belongs with the change that
/// makes this path publicly reachable.
/// the feature forced, but that scaffolding would have to be deleted as soon
/// as the feature is forwarded for real. The end-to-end proof belongs with
/// the change that makes this path publicly reachable.
///
/// What *is* pinned here: the consumer split itself
/// (`wants_interactive_console`), and β€” in `wxc_common` β€” that
Expand Down
37 changes: 26 additions & 11 deletions src/core/mxc-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,32 +285,47 @@ backend the library supports.
Beyond the one-shot `run` / `spawn_sandbox` paths, the SDK exposes the
state-aware sandbox lifecycle from a wire-format request JSON string:

- `run_state_aware_json(request_json, dry_run)` drives the **envelope phases** β€”
`provision`, `start`, `stop`, `deprovision` (and a dry run of any phase) β€” and
returns the response-envelope JSON string.
- `exec_sandbox(request_json)` runs the `exec` phase as a **live streaming**
`Sandbox` (the same handle `spawn_sandbox` returns).
- `run_state_aware_json(request_json, dry_run, experimental)` drives the
**envelope phases** β€” `provision`, `start`, `stop`, `deprovision` (and a dry
run of any phase) β€” and returns the response-envelope JSON string.
- `exec_sandbox(request_json, experimental)` runs the `exec` phase as a **live
streaming** `Sandbox` (the same handle `spawn_sandbox` returns).

Every state-aware backend is experimental, so `experimental` is the in-process
equivalent of the executor's `--experimental` flag: without it the request is
refused with `ErrorCode::BackendUnavailable` before any work happens. It is an
API parameter, not a field in the request JSON.

The example below is illustrative: it selects IsolationSession, which needs
`mxc_engine/isolation_session`, and this crate does not forward that feature
today β€” so it returns `ErrorCode::UnsupportedPhase` as written.

```rust,no_run
use mxc_sdk::{run_state_aware_json, exec_sandbox};

// Envelope phase: provision returns { "result": { "sandboxId": ... } }.
// Provision. IsolationSession accepts only the canonical unrestricted-network
// acknowledgment; an absent policy defaults to `block`, which it refuses.
let provisioned = run_state_aware_json(
r#"{"phase":"provision","containment":"isolation_session"}"#,
false,
r#"{"phase":"provision","containment":"isolation_session",
"network":{"defaultPolicy":"allow","allowLocalNetwork":true}}"#,
false, // dry_run
true, // experimental
)?;

// Exec phase: a live streaming handle.
let mut proc = exec_sandbox(
r#"{"phase":"exec","sandboxId":"iso:...","process":{"commandLine":"echo hi"}}"#,
true, // experimental
)?;
Comment thread
adpa-ms marked this conversation as resolved.
let _ = proc.wait();
# Ok::<(), Box<dyn std::error::Error>>(())
```

The only in-tree state-aware backend, IsolationSession, is Windows-only and
experimental (it needs its OS-side service); on a host/build without it these
return an `Error` with `ErrorCode::UnsupportedPhase`.
Three backends implement the state-aware lifecycle β€” IsolationSession, WSLc and
Windows Sandbox β€” all Windows-only, and only IsolationSession serves a streaming
`exec` in-process. Branch on the error code: a backend whose feature is not
compiled in answers `ErrorCode::BackendUnavailable`, and one with no arm on the
path you called answers `ErrorCode::UnsupportedPhase`.

## Supported backends

Expand Down
35 changes: 29 additions & 6 deletions src/core/mxc-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,11 @@
//! `stop`, `deprovision` (and a dry run of any phase) β€” taking the wire-format
//! request JSON and returning the response-envelope JSON. [`exec_sandbox`] runs
//! the `exec` phase as a live streaming [`Sandbox`], the same handle
//! [`spawn_sandbox`] returns. The only in-tree state-aware backend
//! (IsolationSession) is Windows-only and needs its OS-side service.
//! [`spawn_sandbox`] returns. Three backends implement the lifecycle β€”
//! IsolationSession, WSLc and Windows Sandbox, all Windows-only β€” but only
//! IsolationSession serves a streaming `exec` in-process; see [`exec_sandbox`]
//! for what the other two return there, and the crate README for how each one
//! is compiled in.
//!
//! ## No pty
//!
Expand Down Expand Up @@ -190,8 +193,19 @@ pub fn run(request: SandboxRequest) -> Result<Output, Error> {
/// The request JSON is the same wire format the executor accepts (an object with
/// a `phase` field). Errors (malformed request, unsupported phase, backend
/// failures) come back as an [`Error`] with the matching [`ErrorCode`].
pub fn run_state_aware_json(request_json: &str, dry_run: bool) -> Result<String, Error> {
mxc_engine::run_state_aware_json(request_json, dry_run)
///
/// `experimental` is the in-process equivalent of the executor's
/// `--experimental` flag. The experimental backends β€” WindowsSandbox,
/// IsolationSession and WSLc β€” are refused with
/// [`ErrorCode::BackendUnavailable`] unless it is set, before any work is done.
/// It is an API parameter rather than a field in the request JSON so that a
/// config cannot grant itself experimental access.
pub fn run_state_aware_json(
request_json: &str,
dry_run: bool,
experimental: bool,
) -> Result<String, Error> {
mxc_engine::run_state_aware_json(request_json, dry_run, experimental)
}

/// Run the `exec` phase of a state-aware request (as a JSON string) as a **live
Expand All @@ -200,6 +214,15 @@ pub fn run_state_aware_json(request_json: &str, dry_run: bool) -> Result<String,
///
/// The request JSON must be an `exec`-phase state-aware request (with a
/// `sandboxId` identifying a started sandbox). No pty is allocated.
pub fn exec_sandbox(request_json: &str) -> Result<Sandbox, Error> {
mxc_engine::exec_state_aware_json(request_json).map(Sandbox::new)
///
/// `experimental` opts in to the experimental backends, as for
/// [`run_state_aware_json`]. **IsolationSession is the only backend that serves
/// a streaming `exec` in-process**: the streaming dispatcher asks for
/// `ExecConsumer::Library`, which WSLc refuses outright because it relays to the
/// executor's stdio, and Windows Sandbox has no streaming arm at all
/// (`unsupported_phase`). IsolationSession's arm additionally needs
/// `mxc_engine/isolation_session`, which this crate does not forward yet β€” so
/// today this entry point cannot return a live handle from a stock build.
pub fn exec_sandbox(request_json: &str, experimental: bool) -> Result<Sandbox, Error> {
mxc_engine::exec_state_aware_json(request_json, experimental).map(Sandbox::new)
}
16 changes: 11 additions & 5 deletions src/core/mxc-sdk/src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,17 @@ pub enum WaitOutcome {
/// confirms that process, and a descendant the workload backgrounded is
/// reclaimed when the sandbox is stopped and deprovisioned rather than here.
///
/// That state-aware route is not reachable from this crate yet:
/// [`exec_sandbox`](crate::exec_sandbox) parses without the experimental
/// opt-in, so an experimental backend is refused before dispatch. The
/// distinction is documented here because it is what implementors build
/// against, and because it becomes observable the moment that opt-in lands.
/// That state-aware route is reachable from this crate once the caller
/// passes the `experimental` opt-in to
/// [`exec_sandbox`](crate::exec_sandbox) and the backend is compiled in via
/// `mxc_engine/isolation_session`, which this crate does not forward yet.
/// The two are refused differently: without the opt-in the experimental gate
/// rejects the request before dispatch
/// ([`ErrorCode::BackendUnavailable`](crate::ErrorCode::BackendUnavailable)),
/// while without the feature it passes the gate and finds no arm to dispatch
/// to ([`ErrorCode::UnsupportedPhase`](crate::ErrorCode::UnsupportedPhase)).
/// The distinction is documented here because it is what implementors build
/// against.
TimedOut,
}

Expand Down
109 changes: 98 additions & 11 deletions src/core/mxc-sdk/tests/state_aware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@
//! (`run_state_aware_json` / `exec_sandbox`).
//!
//! These exercise request parsing, phase routing, and error mapping without a
//! live host backend. The only in-tree state-aware backend (IsolationSession)
//! is Windows-only and needs the OS-side IsoSessionOps service, so the actual
//! provision/exec paths are covered by the executor E2E suites; here we assert
//! the SDK facade's contract (parse, reject one-shot, reject non-dry-run exec,
//! surface unsupported_phase for a backend without a state-aware impl).
//! live host backend. All three state-aware backends β€” IsolationSession, WSLc
//! and Windows Sandbox β€” are Windows-only, and IsolationSession additionally
//! needs the OS-side IsoSessionOps service, so the real lifecycle paths are
//! exercised by the executor E2E suites instead.
//!
//! Those suites drive the `ExecConsumer::Executor` path, **not** the
//! `ExecConsumer::Library` path [`exec_sandbox`] uses β€” that one has no
//! end-to-end coverage yet, as the testing-gap note on IsolationSession's `exec`
//! records. So the assertions here deliberately stop at the facade's contract:
//! parse, reject one-shot, reject non-dry-run exec, surface unsupported_phase
//! for a backend without a state-aware impl, and honour the experimental opt-in
//! β€” which stays host-independent because the gate runs before backend dispatch.

use mxc_sdk::{exec_sandbox, run_state_aware_json, ErrorCode};

#[test]
fn run_state_aware_json_rejects_one_shot_config() {
// No `phase` field => one-shot config, not a lifecycle request.
let json = r#"{"version":"0.8.0-alpha","process":{"commandLine":"echo hi"}}"#;
let err = run_state_aware_json(json, false).expect_err("one-shot must be rejected");
let err = run_state_aware_json(json, false, false).expect_err("one-shot must be rejected");
assert_eq!(err.code, ErrorCode::MalformedRequest);
}

Expand All @@ -26,22 +33,24 @@ fn run_state_aware_json_rejects_non_dry_run_exec() {
// A non-dry-run exec streams; it must be routed through exec_sandbox, not
// the envelope entry point.
let json = r#"{"phase":"exec","sandboxId":"isolationsession:abc","process":{"commandLine":"echo hi"}}"#;
let err = run_state_aware_json(json, false).expect_err("non-dry-run exec must be rejected");
let err =
run_state_aware_json(json, false, false).expect_err("non-dry-run exec must be rejected");
assert_eq!(err.code, ErrorCode::MalformedRequest);
assert!(err.message.contains("exec"));
}

#[test]
fn run_state_aware_json_malformed_json_is_malformed_request() {
let err = run_state_aware_json("{ not json", false).expect_err("bad JSON must be rejected");
let err =
run_state_aware_json("{ not json", false, false).expect_err("bad JSON must be rejected");
assert_eq!(err.code, ErrorCode::MalformedRequest);
}

#[test]
fn exec_sandbox_rejects_non_exec_phase() {
let json = r#"{"phase":"provision","containment":"isolation_session"}"#;
// `Sandbox` is not `Debug`, so match rather than `expect_err`.
match exec_sandbox(json) {
match exec_sandbox(json, false) {
Ok(_) => panic!("a provision request is not an exec"),
Err(err) => assert_eq!(err.code, ErrorCode::MalformedRequest),
}
Expand All @@ -50,7 +59,7 @@ fn exec_sandbox_rejects_non_exec_phase() {
#[test]
fn exec_sandbox_rejects_one_shot_config() {
let json = r#"{"version":"0.8.0-alpha","process":{"commandLine":"echo hi"}}"#;
match exec_sandbox(json) {
match exec_sandbox(json, false) {
Ok(_) => panic!("one-shot must be rejected"),
Err(err) => assert_eq!(err.code, ErrorCode::MalformedRequest),
}
Expand All @@ -67,7 +76,85 @@ fn exec_sandbox_rejects_one_shot_config() {
#[test]
fn unregistered_backend_prefix_is_unsupported_containment() {
let json = r#"{"phase":"start","sandboxId":"nosuchbackend:abc123"}"#;
let err = run_state_aware_json(json, false)
let err = run_state_aware_json(json, false, false)
.expect_err("an unregistered sandbox-id prefix has no backend");
assert_eq!(err.code, ErrorCode::UnsupportedContainment);
}

/// Without the opt-in, an experimental backend is refused β€” and the refusal is
/// host- and feature-independent, because `require_experimental_optin` runs
/// before backend dispatch on every platform.
#[test]
fn experimental_backend_is_refused_without_the_optin() {
let json = r#"{"phase":"provision","containment":"windows_sandbox"}"#;
let err = run_state_aware_json(json, true, false)
.expect_err("an experimental backend without the opt-in must be refused");
assert_eq!(err.code, ErrorCode::BackendUnavailable);
assert!(
err.message.contains("experimental"),
"the refusal should say what is missing, got: {}",
err.message
);
}

/// With the opt-in, the same request gets **past** the gate.
///
/// The assertion is deliberately "not `BackendUnavailable`" rather than a
/// specific success: what happens next varies by host and build features (a dry
/// run on Windows, `unsupported_phase` elsewhere), and pinning that would make
/// this a host test. What it discriminates is the thing this change adds β€” if
/// the parameter were dropped on the way down, the gate would still refuse and
/// this fails. A dry run keeps it side-effect-free.
#[test]
fn the_optin_admits_an_experimental_backend() {
let json = r#"{"phase":"provision","containment":"windows_sandbox"}"#;
if let Err(err) = run_state_aware_json(json, true, true) {
assert_ne!(
err.code,
ErrorCode::BackendUnavailable,
"the opt-in was passed, so the experimental gate must not refuse: {}",
err.message
);
}
}

/// The gate's refusal carries no failing-API detail, because no platform API is
/// in flight when it fires β€” the same shape a malformed request produces.
#[test]
fn the_refusal_carries_no_api_call_detail() {
let json = r#"{"phase":"provision","containment":"windows_sandbox"}"#;
let err = run_state_aware_json(json, true, false).expect_err("must be refused");
assert_eq!(err.operation, None);
assert_eq!(err.native_code, None);
assert_eq!(err.remediation, None);
}

/// The **streaming** entry point honours the opt-in too, not just the envelope
/// one.
///
/// Both are needed: they take separate paths to the same gate, so a change that
/// hardcoded the flag in only one of them would leave the other's tests green.
/// A `wsb:` id routes to Windows Sandbox, which has no streaming-exec arm β€” so
/// once past the gate it lands on `unsupported_phase`, and the two outcomes are
/// distinguishable without a host, a feature, or any backend work. (A `wslc:`
/// id would not discriminate: its feature-off arm also answers
/// `backend_unavailable`, which is the very code the gate returns.)
#[test]
fn exec_honours_the_optin_on_its_own_path() {
let json = r#"{"phase":"exec","sandboxId":"wsb:0a1b2c3d","process":{"commandLine":"echo hi"}}"#;

match exec_sandbox(json, false) {
Ok(_) => panic!("without the opt-in the gate must refuse"),
Err(err) => assert_eq!(err.code, ErrorCode::BackendUnavailable),
}

match exec_sandbox(json, true) {
Ok(_) => panic!("windows_sandbox serves no streaming exec"),
Err(err) => assert_ne!(
err.code,
ErrorCode::BackendUnavailable,
"the opt-in was passed, so the gate must not refuse: {}",
err.message
),
}
}
Loading
Loading