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
40 changes: 33 additions & 7 deletions docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,26 @@ if discriminator.phase.is_some() {
}
```

`parse_rolling_state_aware_wire_input` is the rolling source adapter. It
extracts and validates the raw experimental block, masks that block for base
wire deserialization, and produces:

```rust
StateAwareWireInput {
config, // common wire fields; experimental is always None
experimental_raw, // lossless backend payload
source_text, // exact decoded request text
}
```

`convert_wire_state_aware` composes that source adapter with the shared
`normalize_state_aware` seam, which owns request-kind and containment
validation, one-shot-section rejection, common policy conversion,
post-provision Network presence handling, and typed telemetry population. The
exact development adapter produces the same neutral representation so the
private exact router can enter this seam without duplicating rolling
normalization.

The trailing-command path keeps the exact phase probe separate because it owns
error routing. Backend selection and command editing share one
duplicate-preserving raw root parse, and the resulting effective document then
Expand All @@ -1140,22 +1160,28 @@ enters `parse_mxc_request_json` directly.

`wire::MxcConfig` is closed (`deny_unknown_fields`) on its stable surface, so
unknown fields are rejected at the trust boundary. `phase` maps to the
`wire::Phase` enum. The `experimental` block stays permissive and is captured as
a raw `serde_json::Value` on the state-aware path so the dispatcher can type each
backend's per-phase config from it (`experimental.<backend>.<phase>`).
`wire::Phase` enum. The rolling `experimental` block stays permissive and is
captured as a raw `serde_json::Value`; the exact contract closes the same shape
before its adapter preserves the source value. In both cases
`config.experimental` is cleared, and the dispatcher types each backend's
per-phase config from `experimental_raw`
(`experimental.<backend>.<phase>`). This raw value is the temporary transport
between structural parsing and backend phase deserialization.

Per-phase requirements (`containment` for `provision`, `sandboxId` for the
others) are enforced in the conversion step, not at the deserializer. The
one-shot path rejects `phase` / `sandboxId`, and the state-aware path rejects
one-shot-only sections (`seatbelt` / `processContainer` / `lxc` / `lifecycle`),
so each mode only accepts its valid fields.

Conversion populates the cross-cutting wire fields (`filesystem`, `network`,
Normalization populates the cross-cutting wire fields (`filesystem`, `network`,
`ui`) into `ExecutionRequest.policy` (a `ContainerPolicy`) exactly as the
one-shot path does, and `process` populates `ExecutionRequest`'s flat
`script_code` / `working_directory` / `script_timeout` / `env` fields. The
state-aware-only fields (`phase`, `sandboxId`, `experimental.<backend>.<phase>`)
are extracted alongside the `ExecutionRequest` and bundled into a
`script_code` / `working_directory` / `script_timeout` / `env` fields. Typed
telemetry is populated from the neutral config's top-level `config.telemetry`;
`experimental_raw` remains available for backend phase configuration. The state-aware-only fields
(`phase`, `sandboxId`, `experimental.<backend>.<phase>`) are bundled with the
`ExecutionRequest` in a
`ParsedStateAwareRequest` domain model — `{ request: ExecutionRequest, phase:
Phase, containment: Option<ContainmentBackend>, sandbox_id: Option<String>,
experimental_raw: Option<serde_json::Value>, source_text: Option<Box<str>> }` —
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ mod tests {
assert_eq!(adapted.config.version, Some("0.9.0-alpha".to_string()));
assert!(matches!(adapted.config.phase, Some(wire::Phase::Exec)));
assert_eq!(adapted.config.sandbox_id, Some("sandbox-id".to_string()));
assert!(adapted.config.experimental.is_none());
assert!(adapted.config.process.is_some());

let process = adapted.config.process.unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,9 +432,14 @@ pub(super) fn deprovision_into_wire(request: contract::DeprovisionRequest) -> wi
}

pub(super) fn into_state_aware_wire_input(
config: wire::MxcConfig,
mut config: wire::MxcConfig,
source_text: &str,
) -> Result<StateAwareWireInput, serde_json::Error> {
// State-aware backend data is carried losslessly in `experimental_raw`.
// Clear the redundant rolling-wire copy before the shared normalizer sees
// the input, matching the rolling parser's canonical representation.
config.experimental = None;

Ok(StateAwareWireInput {
config,
experimental_raw: extract_experimental_value(source_text)?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,31 @@
use super::super::{
contract, extract_experimental_value, into_state_aware_wire_input, start_into_wire, wire,
};
use crate::config_parser::parse_rolling_state_aware_wire_input;
use crate::state_aware_wire::StateAwareWireInput;
use serde_json::value::RawValue;

#[derive(serde::Deserialize)]
struct ExperimentalProbe<'a> {
#[serde(borrow, default)]
experimental: Option<&'a RawValue>,
}

pub(super) fn assert_config_matches_rolling_state_aware_wire_input(
source: &str,
adapted_config: wire::MxcConfig,
) {
let probe: ExperimentalProbe<'_> = serde_json::from_str(source).unwrap();
let rolling = parse_rolling_state_aware_wire_input(source, probe.experimental).unwrap();
let adapted = into_state_aware_wire_input(adapted_config, source).unwrap();

assert_eq!(
serde_json::to_value(adapted.config).unwrap(),
serde_json::to_value(rolling.config).unwrap()
);
assert_eq!(adapted.experimental_raw, rolling.experimental_raw);
assert_eq!(adapted.source_text, rolling.source_text);
}

#[test]
fn extract_experimental_value_returns_none_when_absent() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

use super::super::{contract, deprovision_into_wire, wire};
use super::common::assert_config_matches_rolling_state_aware_wire_input;

const MINIMAL_REQUEST_JSON: &str = r#"{
"version": "0.9.0-alpha",
Expand Down Expand Up @@ -125,32 +126,27 @@ fn null_comment_maps_expected_wire_field() {
}

// Deserialization match tests
pub(super) fn assert_matches_current_wire_deserialization(json: &str) {
let current: wire::MxcConfig = crate::config_deserialize::from_str(json).unwrap();
pub(super) fn assert_matches_rolling_state_aware_wire_input(json: &str) {
let adapted = adapt(json);

assert_eq!(
serde_json::to_value(adapted).unwrap(),
serde_json::to_value(current).unwrap()
);
assert_config_matches_rolling_state_aware_wire_input(json, adapted);
}

#[test]
fn minimal_request_matches_current_wire_deserialization() {
fn minimal_request_matches_rolling_state_aware_wire_input() {
let json = MINIMAL_REQUEST_JSON;
assert_matches_current_wire_deserialization(json);
assert_matches_rolling_state_aware_wire_input(json);
}

#[test]
fn request_with_all_fields_matches_current_wire_deserialization() {
fn request_with_all_fields_matches_rolling_state_aware_wire_input() {
let json = ALL_FIELDS_REQUEST_JSON;
assert_matches_current_wire_deserialization(json);
assert_matches_rolling_state_aware_wire_input(json);
}

#[test]
fn empty_experimental_sections_match_current_wire_deserialization() {
for fields in [r#""experimental": {}"#, r#""telemetry": {}"#] {
assert_matches_current_wire_deserialization(&request_with_fields(fields));
assert_matches_rolling_state_aware_wire_input(&request_with_fields(fields));
}
}

Expand All @@ -162,5 +158,5 @@ fn empty_identifier_strings_match_current_wire_deserialization() {
"sandboxId": ""
}"#;

assert_matches_current_wire_deserialization(json);
assert_matches_rolling_state_aware_wire_input(json);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

use super::super::{contract, exec_into_wire, wire};
use super::common::assert_config_matches_rolling_state_aware_wire_input;

const MINIMAL_REQUEST_JSON: &str = r#"{
"version": "0.9.0-alpha",
Expand Down Expand Up @@ -203,38 +204,33 @@ fn null_comment_maps_expected_wire_field() {
}

// Deserialization match tests
pub(super) fn assert_matches_current_wire_deserialization(json: &str) {
let current: wire::MxcConfig = crate::config_deserialize::from_str(json).unwrap();
pub(super) fn assert_matches_rolling_state_aware_wire_input(json: &str) {
let adapted = adapt(json);

assert_eq!(
serde_json::to_value(adapted).unwrap(),
serde_json::to_value(current).unwrap()
);
assert_config_matches_rolling_state_aware_wire_input(json, adapted);
}

#[test]
fn minimal_request_matches_current_wire_deserialization() {
fn minimal_request_matches_rolling_state_aware_wire_input() {
let json = MINIMAL_REQUEST_JSON;
assert_matches_current_wire_deserialization(json);
assert_matches_rolling_state_aware_wire_input(json);
}

#[test]
fn request_with_all_fields_matches_current_wire_deserialization() {
fn request_with_all_fields_matches_rolling_state_aware_wire_input() {
let json = ALL_FIELDS_REQUEST_JSON;
assert_matches_current_wire_deserialization(json);
assert_matches_rolling_state_aware_wire_input(json);
}

#[test]
fn empty_experimental_sections_match_current_wire_deserialization() {
for fields in [r#""experimental": {}"#, r#""telemetry": {}"#] {
assert_matches_current_wire_deserialization(&request_with_fields(fields));
assert_matches_rolling_state_aware_wire_input(&request_with_fields(fields));
}
}

#[test]
fn empty_network_section_matches_current_wire_deserialization() {
assert_matches_current_wire_deserialization(&request_with_fields(r#""network": {}"#));
fn empty_network_section_matches_rolling_state_aware_wire_input() {
assert_matches_rolling_state_aware_wire_input(&request_with_fields(r#""network": {}"#));
}

#[test]
Expand All @@ -249,5 +245,5 @@ fn empty_identifier_strings_match_current_wire_deserialization() {
}
}"#;

assert_matches_current_wire_deserialization(json);
assert_matches_rolling_state_aware_wire_input(json);
}
Loading
Loading