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
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,13 @@ its existing routing and execution gates before binding to
consume this bound type. Optional provision configuration and optional fields
remain intact through validation, with defaults still owned by the backend.

High-level Rust callers construct `ProvisionRequest`,
`SandboxLifecycleRequest`, or `StateAwareExecRequest`. Those values adapt
High-level Rust callers use operation-specific functions under
`mxc_sdk::sandbox`. They pass an opaque `SandboxId` separately from
`ProvisionRequest`, `LifecycleRequest`, or `ExecRequest`, while authorization
and telemetry preferences remain in `OperationOptions`. Those values adapt
directly into `CommonRequestIR + StateAwareOperation` and share normalization
with the exact-contract lane; they are not serialized to JSON. Typed lifecycle
dispatch returns `StateAwareResult` and typed backend metadata without
with the exact-contract lane; they are not serialized to JSON. Provision,
lifecycle, and validation calls return distinct typed results without
constructing a JSON response envelope. `run_state_aware_json`,
`exec_sandbox_json`, and `exec_attached_json` remain for intentional raw
exact-contract use.
Expand Down
21 changes: 13 additions & 8 deletions docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ unrecognised prefix, and this is by design:
| Source | Behaviour for an unrecognised `sandboxId` prefix |
| ----------------------- | ------------------------------------------------ |
| SDK (TypeScript) | Throws `MxcError { code: 'malformed_id' }` before invoking `mxc_state_aware` or `mxc_state_aware_exec`. The SDK matches the prefix against the closed `StateAwareContainmentBackend` union it was compiled with; an unknown prefix is treated as a malformed id. See `sdk/node/src/state-aware-helper.ts`. |
| SDK (Rust) | `SandboxId::parse` accepts a syntactically valid opaque id without interpreting its prefix. Dispatch returns `MxcError { code: 'unsupported_containment' }` when that prefix is not registered. Empty ids, ids without prefix structure, and ids containing NUL are `malformed_id`. |
| Native FFI entry points | Return `MxcError { code: 'unsupported_containment' }`. The Rust dispatcher parses the prefix successfully but the prefix-to-backend lookup table has no entry for it. See `src/core/wxc_common/src/state_aware_dispatch.rs`. |

A recognised prefix with a malformed body is `malformed_id` from both sources
Expand Down Expand Up @@ -1041,10 +1042,10 @@ other state-aware backend, so caller error-handling code is portable across back
| Code | Meaning |
|---|---|
| `malformed_request` | Structural request error: malformed JSON, missing required field, unknown or phase-inappropriate field, recursively unknown backend-specific field, or invalid phase-specific shape |
| `unsupported_containment` | The backend named by `containment` (provision) or implied by the `sandboxId` prefix (non-provision) is not a recognised backend in this build. **SDK callers**: the SDK type-checks unknown `sandboxId` prefixes against the closed `StateAwareContainmentBackend` union *before* dispatching and instead throws `malformed_id` for an unknown prefix; `unsupported_containment` is reachable from the SDK only on the provision path. See §6.4 |
| `unsupported_containment` | The backend named by `containment` (provision) or implied by a syntactically valid `sandboxId` prefix (non-provision) is not recognised in this build. The TypeScript SDK checks its closed prefix union before dispatch and instead throws `malformed_id`; the typed Rust SDK keeps ids opaque and therefore returns `unsupported_containment` from dispatch, matching raw FFI requests. See §6.4. |
| `unsupported_phase` | The backend does not support the requested call mode (state-aware call against an ephemeral-only backend, or one-shot call against a state-aware-only backend) |
| `backend_unavailable` | The backend's runtime dependency is missing or unreachable (service not running, daemon stopped) |
| `malformed_id` | The `sandboxId` does not have a recognised backend prefix, or has a recognised prefix but does not deserialise into the backend's native form. **SDK callers** also see this error code for any non-provision call whose `sandboxId` prefix is not in `StateAwareContainmentBackend` |
| `malformed_id` | The `sandboxId` is structurally invalid or has a recognised prefix but does not deserialize into the backend's native form. The TypeScript SDK also uses this code for a prefix outside its closed `StateAwareContainmentBackend` union; typed Rust and raw FFI calls classify a syntactically valid unknown prefix as `unsupported_containment`. |
| `stale_id` | The `sandboxId` deserialised but refers to a resource the backend no longer recognises |
| `not_provisioned` | Phase requires a provisioned sandbox; none provided, or the id is in a pre-provision state |
| `not_started` | Phase requires a started sandbox; the id is provisioned but not started |
Expand Down Expand Up @@ -1125,8 +1126,11 @@ internal common parser and executable equivalence harness have been removed.

The Rust SDK also has a direct typed ingress lane. High-level lifecycle calls
construct `SdkStateAwareInput` from `ProvisionRequest`,
`SandboxLifecycleRequest`, or `StateAwareExecRequest`, then normalize it through
the same private `CommonRequestIR` and `StateAwareInput` seam:
`LifecycleRequest`, or `ExecRequest`. An opaque `SandboxId` is passed separately
for operations on an existing sandbox, and authorization, telemetry preference,
and other invocation controls are carried by `OperationOptions`. The combined
input then normalizes through the same private `CommonRequestIR` and
`StateAwareInput` seam:

```text
typed Rust request
Expand Down Expand Up @@ -1618,10 +1622,11 @@ fn dispatch_state_aware<B: StatefulSandboxBackend>(

`dispatch_state_aware_typed` returns `TypedDispatchOutcome` without serializing
backend metadata. The high-level Rust SDK maps that result into
`StateAwareResult` and typed backend metadata. The raw JSON lane wraps the same
typed dispatch result in `DispatchOutcome::Envelope` and serializes it for the
wire response. JSON response construction is therefore confined to raw/executor
entry points rather than being an implementation step of typed Rust calls.
`ProvisionResult`, `LifecycleResult`, or `ValidationResult` and typed backend
metadata. The raw JSON lane wraps the same typed dispatch result in
`DispatchOutcome::Envelope` and serializes it for the wire response. JSON
response construction is therefore confined to raw/executor entry points
rather than being an implementation step of typed Rust calls.

`resolve_backend(&parsed)` reads `parsed.containment()` when `phase() == Provision`; for the
other phases it reads the prefix from `parsed.sandbox_id()` and looks it up in the
Expand Down
59 changes: 29 additions & 30 deletions src/core/mxc-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,18 +424,21 @@ compile-time features — **WSLC** and **IsolationSession**.
## State-aware lifecycle

Beyond the one-shot `run` / `spawn_sandbox` paths, the SDK exposes the
state-aware sandbox lifecycle through typed Rust requests:
state-aware sandbox lifecycle under `mxc_sdk::sandbox`:

- `provision_sandbox`, `start_sandbox`, `stop_sandbox`, and
`deprovision_sandbox` return a typed `StateAwareResult`;
- `exec_sandbox_request` returns a live streaming `Sandbox`;
- `exec_attached_request` attaches the workload to this process's stdio and
- `sandbox::provision` returns a `ProvisionResult` with an opaque `SandboxId`;
- `sandbox::start`, `sandbox::stop`, and `sandbox::deprovision` return a
`LifecycleResult`;
- `sandbox::exec` returns a live streaming `Sandbox`;
- `sandbox::exec_attached` attaches the workload to this process's stdio and
returns a `WaitOutcome`;
- `dry_run_exec_sandbox` validates a typed exec request without running it.
- `sandbox::validate_*` validates the matching operation without executing it.

These high-level calls adapt Rust values directly into MXC's common request
model. They do not serialize or parse JSON, and typed backend results are
returned without constructing a JSON response envelope.
returned without constructing a JSON response envelope. Sandbox identity is a
separate typed argument rather than policy, while authorization, telemetry
preference, and other invocation controls live in `OperationOptions`.

Raw exact-contract entry points remain available for callers that intentionally
provide wire JSON:
Expand All @@ -451,8 +454,8 @@ Windows Sandbox requires `experimental`. The parameter is the in-process
equivalent of the executor's `--experimental` flag and is not a field in the
request JSON.

`StateAwareExecRequest` contains only backend-neutral process settings.
Backend-specific exec capabilities use
`ExecRequest` contains only backend-neutral process settings. Backend-specific
exec capabilities use
`set_backend_options(StateAwareExecBackendOptions)`. A backend rejects options
that it cannot enforce; currently only WSLc defines an option, for its
cooperative network proxy.
Expand All @@ -463,33 +466,29 @@ OS-side service.
```rust,no_run
use std::error::Error;
use mxc_sdk::{
exec_attached_request, provision_sandbox, start_sandbox, ProvisionRequest,
SandboxLifecycleRequest, StateAwareExecOptions, StateAwareExecRequest,
StateAwareOptions,
sandbox, ExecRequest, LifecycleRequest, OperationOptions, ProvisionRequest,
};

fn main() -> Result<(), Box<dyn Error>> {
let provisioned = provision_sandbox(
let provisioned = sandbox::provision(
ProvisionRequest::isolation_session("0.9.0-alpha", None),
StateAwareOptions::default(),
OperationOptions::default(),
)?;
// The returned `sandboxId` is opaque — carry it forward, never parse it.
let sandbox_id = provisioned.sandbox_id.expect("provision returned an id");
let sandbox_id = provisioned.sandbox_id;

// Start. The exec phase runs against a started session.
start_sandbox(
SandboxLifecycleRequest::new("0.9.0-alpha", &sandbox_id),
StateAwareOptions::default(),
sandbox::start(
&sandbox_id,
LifecycleRequest::new("0.9.0-alpha"),
OperationOptions::default(),
)?;

// Exec phase, attached: an interactive shell on this console.
let outcome = exec_attached_request(
StateAwareExecRequest::new(
"0.9.0-alpha",
sandbox_id,
"powershell.exe",
),
StateAwareExecOptions::default(),
let outcome = sandbox::exec_attached(
&sandbox_id,
ExecRequest::new("0.9.0-alpha", "powershell.exe"),
OperationOptions::default(),
)?;
let _ = outcome;
Ok(())
Expand All @@ -498,11 +497,11 @@ Ok(())

Three backends implement the state-aware lifecycle — IsolationSession, WSLc and
Windows Sandbox. IsolationSession and WSLc serve streaming typed exec through
`exec_sandbox_request`; WSLc exposes stdout/stderr only because its SDK has no
`sandbox::exec`; WSLc exposes stdout/stderr only because its SDK has no
process-input API. Windows Sandbox supports attached exec but cannot return
native exec pipes.

All three state-aware backends serve `exec_attached_request`. IsolationSession
All three state-aware backends serve `sandbox::exec_attached`. IsolationSession
also forwards stdin through a pseudo-console; Windows Sandbox drops terminal
input pending PTY support, and WSLc has no process-input API.

Expand Down Expand Up @@ -705,9 +704,9 @@ stream, so the sandbox's stderr arrives merged into stdout.

Windows Sandbox and WSLc relay attached output without interactive stdin.

`exec_attached` and `exec_attached_request` refuse with `MalformedRequest`
unless this process's stdout and stdin are both terminals; use `exec_sandbox`
or `exec_sandbox_request` for a workload with no terminal.
`sandbox::exec_attached` refuses with `MalformedRequest` unless this process's
stdout and stdin are both terminals; use `sandbox::exec` for a typed workload
with no terminal.

## Relationship to `mxc_engine` and the executor binaries

Expand Down
96 changes: 18 additions & 78 deletions src/core/mxc-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,24 +111,24 @@
//!
//! ## Choosing an entry point
//!
//! | | one-shot | typed state-aware | Stdio |
//! |-------------|---------------------|-----------------------------------------------|------------------------------------------|
//! | **capture** | [`run`] | `exec_sandbox_request(…)?.wait_with_output()` | captured |
//! | **handle** | [`spawn_sandbox`] | [`exec_sandbox_request`] | live pipes (stream, kill); no TTY |
//! | **attach** | *not available* | [`exec_attached_request`] | this process's stdio; TTY if it has one |
//! | | one-shot | typed state-aware | Stdio |
//! |-------------|-------------------|-------------------|-----------------------------------|
//! | **capture** | [`run`] | `sandbox::exec(…)?.wait_with_output()` | captured |
//! | **handle** | [`spawn_sandbox`] | [`sandbox::exec`] | live pipes (stream, kill); no TTY |
//! | **attach** | *not available* | [`sandbox::exec_attached`] | this process's stdio; TTY if present |
//!
//! [`provision_sandbox`], [`start_sandbox`], [`stop_sandbox`], and
//! [`deprovision_sandbox`] drive the envelope phases with typed Rust requests.
//! [`sandbox::provision`], [`sandbox::start`], [`sandbox::stop`], and
//! [`sandbox::deprovision`] drive the lifecycle with typed Rust requests.
//! [`run_state_aware_json`], [`exec_sandbox_json`], and [`exec_attached_json`]
//! are the separate raw exact-JSON lane. The crate README covers which backends
//! implement the lifecycle and how each is compiled in.
//!
//! ## Pty allocation
//!
//! Every entry point except [`exec_attached_request`] and [`exec_attached`]
//! Every entry point except [`sandbox::exec_attached`] and [`exec_attached`]
Comment thread
MGudgin marked this conversation as resolved.
//! wires the child's stdio to
//! ordinary pipes and allocates no pty. [`run`] captures both streams; with
//! [`spawn_sandbox`] or [`exec_sandbox_request`], stream the handle's
//! [`spawn_sandbox`] or [`sandbox::exec`], stream the handle's
//! `take_stdout`/`take_stderr`, or let [`wait`](Sandbox::wait) drain and
//! discard any untaken stream. WSLC exposes no stdin because its SDK has no
//! process-input API.
Expand All @@ -138,7 +138,7 @@
//! has one output stream, so the sandbox's stderr arrives merged into stdout.
//!
//! IsolationSession, WSLC, and Windows Sandbox serve attached exec through
//! [`exec_attached_request`] and [`exec_attached`]. IsolationSession additionally
//! [`sandbox::exec_attached`] and [`exec_attached`]. IsolationSession additionally
//! forwards stdin through a pseudo-console; Windows Sandbox drops terminal
//! input pending PTY support, and WSLC has no process-input API.
//!
Expand All @@ -155,7 +155,7 @@
//! `mxc-sdk` re-exports the curated surface and wraps the engine's streaming
//! handle in [`Sandbox`].

mod sandbox;
pub mod sandbox;

pub mod telemetry;

Expand All @@ -164,13 +164,13 @@ pub use mxc_engine::policy;
pub use mxc_engine::{
available_backends, available_tools_policy, build_request, build_request_with_containment,
platform_support, temporary_files_policy, user_profile_policy, AvailableBackend,
BackendCapability, BubblewrapNetworkSupport, Containment, Error, ErrorCode,
FilesystemPolicyResult, IsolationSessionProvisionMetadata, NetworkAction, NetworkEgressSection,
NetworkIngressSection, NetworkPeerSection, NetworkPortSection, NetworkProtocol,
NetworkRuleSection, PlatformSupport, ProvisionRequest, ProxyEnforcement, RuntimeConfigSection,
SandboxLifecycleRequest, SandboxPolicy, SandboxRequest, StateAwareExecBackendOptions,
StateAwareExecOptions, StateAwareExecRequest, StateAwareMetadata, StateAwareOptions,
StateAwareProvision, StateAwareResult, WslcSection,
BackendCapability, BubblewrapNetworkSupport, Containment, Error, ErrorCode, ExecRequest,
FilesystemPolicyResult, IsolationSessionProvisionMetadata, LifecycleRequest, LifecycleResult,
NetworkAction, NetworkEgressSection, NetworkIngressSection, NetworkPeerSection,
NetworkPortSection, NetworkProtocol, NetworkRuleSection, OperationOptions, PlatformSupport,
ProvisionMetadata, ProvisionRequest, ProvisionResult, ProxyEnforcement, RuntimeConfigSection,
SandboxId, SandboxPolicy, SandboxRequest, StateAwareExecBackendOptions, StateAwareProvision,
ValidationResult, WslcSection,
};

pub use sandbox::{
Expand Down Expand Up @@ -238,66 +238,6 @@ pub fn run_state_aware_json(
mxc_engine::run_state_aware_json(request_json, dry_run, experimental)
}

/// Provision a state-aware sandbox from a typed Rust request.
pub fn provision_sandbox(
request: ProvisionRequest,
options: StateAwareOptions,
) -> Result<StateAwareResult, Error> {
mxc_engine::provision_sandbox(request, options)
}

/// Start a provisioned state-aware sandbox from a typed Rust request.
pub fn start_sandbox(
request: SandboxLifecycleRequest,
options: StateAwareOptions,
) -> Result<StateAwareResult, Error> {
mxc_engine::start_sandbox(request, options)
}

/// Stop a state-aware sandbox from a typed Rust request.
pub fn stop_sandbox(
request: SandboxLifecycleRequest,
options: StateAwareOptions,
) -> Result<StateAwareResult, Error> {
mxc_engine::stop_sandbox(request, options)
}

/// Deprovision a state-aware sandbox from a typed Rust request.
pub fn deprovision_sandbox(
request: SandboxLifecycleRequest,
options: StateAwareOptions,
) -> Result<StateAwareResult, Error> {
mxc_engine::deprovision_sandbox(request, options)
}

/// Run a typed state-aware exec request as a live streaming sandbox.
pub fn exec_sandbox_request(
request: StateAwareExecRequest,
options: StateAwareExecOptions,
) -> Result<Sandbox, Error> {
mxc_engine::exec_sandbox_request(request, options).map(Sandbox::new)
}

/// Run a typed state-aware exec request attached to this process's stdio.
pub fn exec_attached_request(
request: StateAwareExecRequest,
options: StateAwareExecOptions,
) -> Result<WaitOutcome, Error> {
use wxc_common::state_aware_backend::ExecOutcome;
mxc_engine::exec_attached_request(request, options).map(|outcome| match outcome {
ExecOutcome::Exited(code) => WaitOutcome::Exited(code),
ExecOutcome::TimedOut => WaitOutcome::TimedOut,
})
}

/// Validate a typed state-aware exec request without running a workload.
pub fn dry_run_exec_sandbox(
request: StateAwareExecRequest,
experimental: bool,
) -> Result<StateAwareResult, Error> {
mxc_engine::dry_run_exec_sandbox(request, experimental)
}

/// Run the `exec` phase of a state-aware request (as a JSON string) as a **live
/// streaming** process, returning a [`Sandbox`] handle for output streaming,
/// waiting, and termination — exactly like [`spawn_sandbox`]. Backends that
Expand Down
Loading
Loading