Skip to content
Open
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
111 changes: 98 additions & 13 deletions docs/development/architecture/container-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,12 @@ telemetry is populated from the common top-level `telemetry` field.
`operation: StateAwareOperation` fields with read-only accessors. The operation
is the sole phase authority. Provision carries
`IsolationSession(Option<IsolationSessionProvisionConfig>)`, `WindowsSandbox`,
or `Wslc(Option<WslcProvisionConfig>)`; later variants each carry a required
or `Wslc(Option<WslcProvisionConfig>)`. `ProvisionSession` carries
`Option<WslcProvisionSessionConfig>` and is WSLc-only; `DeprovisionSession`
carries a required `session_id`, reachable through `session_id()` rather than
`sandbox_id()`. Because routing never reads a session id, operation validation
rejects one that is not `wslcsess:` followed by 32 lowercase hex digits, the form
the WSLc daemon mints. The sandbox-addressed variants each carry a required
`sandbox_id`, with no backend-specific configuration today. Exec process
settings stay in the common request.

Expand All @@ -1075,10 +1080,14 @@ the parser and schema generation.
### 9.2 The trait

Backends implement the trait with two consts (id prefix and backend key, Β§5),
associated types for each phase's config and metadata, and method overrides where they
have substantive work.
associated types for each sandbox phase's config and metadata, and method overrides
where they have substantive work.
Most methods have default no-op bodies; only `exec` is strictly required. Use `()` for
any associated type the backend does not need.
any associated type the backend does not need. The two session phases are the
exception: they take the concrete `WslcProvisionSessionConfig`, return no backend
metadata, and their defaults refuse with `unsupported_phase`, so a backend that
owns no session needs no code to say so, and a dry run of a session phase
cannot report success on a backend that cannot perform it.

```rust
pub trait StatefulSandboxBackend {
Expand All @@ -1103,6 +1112,19 @@ pub trait StatefulSandboxBackend {
type StopMetadata: serde::Serialize;
type DeprovisionMetadata: serde::Serialize;

/// Optional. Default refuses: a backend that owns no session cannot mint
/// one. Override when the backend hosts sandboxes inside a session.
fn provision_session(
&mut self,
_request: &ExecutionRequest,
_config: Option<WslcProvisionSessionConfig>,
) -> Result<ProvisionSessionResult, MxcError> {
Err(unsupported_session_operation(
Self::BACKEND_KEY,
"provisioning a session",
))
}

/// Optional. Default mints `<ID_PREFIX>:<random-token>` for a stateless-
/// underneath backend; override when the backend has native provision work
/// (e.g., allocating a session, registering with the underlying service).
Expand Down Expand Up @@ -1180,9 +1202,22 @@ pub trait StatefulSandboxBackend {
Ok(DeprovisionResult { metadata: None })
}

/// Optional. Default refuses, for the same reason as `provision_session`.
fn deprovision_session(
&mut self,
_session_id: &str,
_request: &ExecutionRequest,
) -> Result<(), MxcError> {
Err(unsupported_session_operation(
Self::BACKEND_KEY,
"deprovisioning a session",
))
}

/// Per-phase validation hooks. Called by the dispatch layer before the
/// corresponding phase method. Default: accept all requests. Override to
/// add backend-specific checks (config field semantics, policy honor
/// corresponding phase method. Default: accept all requests, except for the
/// session phases, which refuse exactly as their phase method does. Override
/// to add backend-specific checks (config field semantics, policy honor
/// enforcement, id format checks beyond the prefix). Failures surface as
/// the chosen `MxcError` code.
fn validate_provision(
Expand All @@ -1193,6 +1228,17 @@ pub trait StatefulSandboxBackend {
Ok(())
}

fn validate_provision_session(
&self,
_request: &ExecutionRequest,
_config: Option<&WslcProvisionSessionConfig>,
) -> Result<(), MxcError> {
Err(unsupported_session_operation(
Self::BACKEND_KEY,
"provisioning a session",
))
}

fn validate_start(
&self,
_sandbox_id: &str,
Expand Down Expand Up @@ -1228,13 +1274,28 @@ pub trait StatefulSandboxBackend {
) -> Result<(), MxcError> {
Ok(())
}

fn validate_deprovision_session(
&self,
_session_id: &str,
_request: &ExecutionRequest,
) -> Result<(), MxcError> {
Err(unsupported_session_operation(
Self::BACKEND_KEY,
"deprovisioning a session",
))
}
}

pub struct ProvisionResult<M> {
pub sandbox_id: String,
pub metadata: Option<M>,
}

pub struct ProvisionSessionResult {
pub session_id: String,
}

pub struct StartResult<M> {
pub metadata: Option<M>,
}
Expand Down Expand Up @@ -1378,11 +1439,13 @@ surface introduces none, so the trait stays minimal and reuses `ExecutionRequest
/// Typed backend result before raw response-envelope encoding.
enum TypedDispatchOutcome<P, S, T, D> {
DryRun,
ProvisionSession(ProvisionSessionResult),
Provision(ProvisionResult<P>),
Start(StartResult<S>),
ExecCompleted { exit_code: i32 },
Stop(StopResult<T>),
Deprovision(DeprovisionResult<D>),
DeprovisionSession,
}

/// Dispatch outcome. Distinguishes structured-envelope responses (non-exec phases or
Expand Down Expand Up @@ -1417,6 +1480,12 @@ fn dispatch_state_aware<B: StatefulSandboxBackend>(
) -> Result<DispatchOutcome, MxcError> {
let (request, operation) = bound.into_parts();
match operation {
BoundStateAwareOperation::ProvisionSession(config) => {
backend.validate_provision_session(&request, config.as_ref())?;
if dry_run { return Ok(DispatchOutcome::Envelope(empty_envelope())); }
let result = backend.provision_session(&request, config)?;
Ok(DispatchOutcome::Envelope(provision_session_envelope(result)))
}
BoundStateAwareOperation::Provision(config) => {
backend.validate_provision(&request, config.as_ref())?;
if dry_run { return Ok(DispatchOutcome::Envelope(empty_envelope())); }
Expand Down Expand Up @@ -1451,6 +1520,12 @@ fn dispatch_state_aware<B: StatefulSandboxBackend>(
let result = backend.deprovision(&sandbox_id, &request, config)?;
Ok(DispatchOutcome::Envelope(deprovision_envelope(result)))
}
BoundStateAwareOperation::DeprovisionSession { session_id } => {
backend.validate_deprovision_session(&session_id, &request)?;
if dry_run { return Ok(DispatchOutcome::Envelope(empty_envelope())); }
backend.deprovision_session(&session_id, &request)?;
Ok(DispatchOutcome::Envelope(empty_envelope()))
}
}
}
```
Expand All @@ -1463,10 +1538,14 @@ metadata. The path accepting "MXC request JSON" wraps the same typed dispatch re
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
registered prefix table. Mismatches surface as `unsupported_containment` (unrecognised
prefix) or `malformed_id` (no prefix structure) per Β§8.
`resolve_backend(&parsed)` reads `parsed.containment()` for the phases where
`phase.routes_by_containment()` is true (`provision-session`, `provision`, and
`deprovision-session`); for the sandbox-addressed phases it reads the prefix from
`parsed.sandbox_id()` and looks it up in the registered prefix table. A session id
carries no backend prefix and is deliberately absent from that table, which is why both
session phases declare a containment even though one of them also carries an id.
Mismatches surface as `unsupported_containment` (unrecognised prefix) or `malformed_id`
(no prefix structure) per Β§8.

`bind_isolation_session`, `bind_windows_sandbox`, and `bind_wslc` live in
`mxc_common::state_aware_binding`, without backend crate dependencies. The engine
Expand Down Expand Up @@ -1677,11 +1756,17 @@ provides default no-op bodies otherwise. The default `provision` mints a synthet
(allocating a session, registering with the underlying service) override and produce
their own structured id.

`provision_session` and `deprovision_session` are the exception: their defaults refuse
with `unsupported_phase`, and they take a concrete WSLc session configuration rather
than an associated type, because only WSLc owns a session a sandbox can be placed into.
A backend that owns no session needs no code at all to decline them.

Override `validate_<phase>` hooks for backend-specific pre-execution checks (config
field semantics, policy honor enforcement, id format verification beyond prefix
matching). Defaults are no-ops; only override the phases the backend has checks for.
Validation runs before the phase method; failures short-circuit and surface as typed
`MxcError` codes without invoking the backend.
matching). Defaults are no-ops for the five sandbox phases and refusals for the two
session phases, matching their phase methods; only override the phases the backend has
checks for. Validation runs before the phase method; failures short-circuit and surface
as typed `MxcError` codes without invoking the backend.

### 11.3 Define typed `*Config` interfaces in the SDK

Expand Down
2 changes: 1 addition & 1 deletion docs/development/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Shared terms: [consumer glossary](../glossary.md).

| Existing term | Preferred wording | Specific meaning or example |
|---|---|---|
| State-aware / stateful API | Container lifecycle operations | Provision, start, exec, stop, deprovision; implemented by `StatefulSandboxBackend`. Use "manual lifecycle operations" when emphasizing caller-managed calls and cleanup. |
| State-aware / stateful API | Container lifecycle operations | Provision, start, exec, stop, deprovision, bracketed by session provision and deprovision; implemented by `StatefulSandboxBackend`. Use "manual lifecycle operations" when emphasizing caller-managed calls and cleanup. |
| One-shot | Create-and-run execution | Make container, run workload, clean up. |
| Exact contract / exact registered contract | Contract | Versioned request rules in `mxc_contract`; identify the version, operation, and provision backend. A schema describes the contract. |
| Wire format / wire contract | Interface-specific format / contract | Name the format: ["MXC request JSON"](../schema.md#mxc-request-json) or daemon binary frames. |
Expand Down
Loading
Loading