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
24 changes: 1 addition & 23 deletions schemas/dev/mxc-config.schema.0.8.0-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,19 +260,8 @@
"type": "object"
},
"IsolationSession": {
"description": "IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`).",
"description": "IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting for the phases that take config (`provision` / `start`). `stop`, `deprovision`, and `exec` take no per-phase config payload: `stop` and `deprovision` are invoked with only the top-level `phase` and `sandboxId`, and `exec` additionally carries the top-level `process` block.",
"properties": {
"deprovision": {
"anyOf": [
{
"$ref": "#/definitions/IsolationSessionPhase"
},
{
"type": "null"
}
],
"description": "State-aware deprovision-phase configuration."
},
"provision": {
"anyOf": [
{
Expand All @@ -295,17 +284,6 @@
],
"description": "State-aware start-phase configuration."
},
"stop": {
"anyOf": [
{
"$ref": "#/definitions/IsolationSessionPhase"
},
{
"type": "null"
}
],
"description": "State-aware stop-phase configuration."
},
"user": {
"anyOf": [
{
Expand Down
10 changes: 1 addition & 9 deletions sdk/node/src/generated/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,9 @@ export interface Filesystem {
}

/**
* IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting (`provision` / `start` / `stop` / `deprovision`).
* IsolationSession backend config. Carries the one-shot `user` field and the per-phase state-aware nesting for the phases that take config (`provision` / `start`). `stop`, `deprovision`, and `exec` take no per-phase config payload: `stop` and `deprovision` are invoked with only the top-level `phase` and `sandboxId`, and `exec` additionally carries the top-level `process` block.
*/
export interface IsolationSession {
/**
* State-aware deprovision-phase configuration.
*/
deprovision?: IsolationSessionPhase | null;
/**
* State-aware provision-phase configuration.
*/
Expand All @@ -122,10 +118,6 @@ export interface IsolationSession {
* State-aware start-phase configuration.
*/
start?: IsolationSessionPhase | null;
/**
* State-aware stop-phase configuration.
*/
stop?: IsolationSessionPhase | null;
/**
* Optional Entra cloud-agent user bundle (one-shot).
*/
Expand Down
94 changes: 94 additions & 0 deletions src/backends/isolation_session/common/src/state_aware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,100 @@ mod tests {
);
}

// ====== Wire-model / backend config parity ======

// The generated JSON schema (`schemas/dev/`) and the SDK wire types
// (`sdk/node/src/generated/wire.ts`) are both emitted from
// `wxc_common::wire::IsolationSession`, while the phases that actually
// accept a config are the associated types on the impl above. On the
// state-aware path the wire model is never constructed β€” the dispatcher
// deserializes raw JSON straight into those associated types β€” so nothing
// couples the two at compile time. The tests below pin that contract from
// both directions: the key set the wire model advertises, that the `()`
// phases reject a payload, and that the phases which do take one still
// accept the payload the wire model describes.

#[test]
fn wire_model_nests_config_only_for_phases_that_take_one() {
// Field-by-field construction is deliberate: adding a per-phase field
// to the wire struct breaks this test's compilation, forcing a
// decision about whether the backend honors it.
let wire = wxc_common::wire::IsolationSession {
user: None,
provision: None,
start: None,
};
let value = serde_json::to_value(&wire).unwrap();
let mut keys: Vec<&str> = value
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
keys.sort_unstable();
assert_eq!(
keys,
["provision", "start", "user"],
"wire model nests a per-phase config for a phase the backend takes none for"
);
}

#[test]
fn phases_without_a_config_reject_a_payload() {
type StopConfig = <IsolationSessionRunner as StatefulSandboxBackend>::StopConfig;
type DeprovisionConfig =
<IsolationSessionRunner as StatefulSandboxBackend>::DeprovisionConfig;
type ExecConfig = <IsolationSessionRunner as StatefulSandboxBackend>::ExecConfig;

// These are `()`, which deserializes only from null, so any object in
// the slot is a hard error at dispatch.
let payload = serde_json::json!({ "user": { "upn": "a@b.com", "wamToken": "t" } });
assert!(
serde_json::from_value::<StopConfig>(payload.clone()).is_err(),
"stop accepted a config payload"
);
assert!(
serde_json::from_value::<DeprovisionConfig>(payload.clone()).is_err(),
"deprovision accepted a config payload"
);
assert!(
serde_json::from_value::<ExecConfig>(payload).is_err(),
"exec accepted a config payload"
);
}

#[test]
fn phases_with_a_config_accept_the_wire_payload() {
type ProvisionConfig = <IsolationSessionRunner as StatefulSandboxBackend>::ProvisionConfig;
type StartConfig = <IsolationSessionRunner as StatefulSandboxBackend>::StartConfig;

// Derive the payload from the wire type instead of a JSON literal: the
// wire model is only the schema source on this path, so a serde rename
// on either side would go unnoticed. Both config types are
// `#[serde(default)]` with no `deny_unknown_fields`, so a renamed key
// does not error β€” it drops the bundle and provisions a local sandbox
// for a caller who asked for an Entra one.
let phase = wxc_common::wire::IsolationSessionPhase {
user: Some(wxc_common::wire::IsolationUser {
upn: "alice@contoso.com".to_string(),
wam_token: "tok".to_string(),
}),
};
let payload = serde_json::to_value(&phase).unwrap();

let provision: ProvisionConfig = serde_json::from_value(payload.clone()).unwrap();
let u = provision
.user
.expect("provision dropped the wire user bundle");
assert_eq!(u.upn, "alice@contoso.com");
assert_eq!(u.wam_token, "tok");

let start: StartConfig = serde_json::from_value(payload).unwrap();
let u = start.user.expect("start dropped the wire user bundle");
assert_eq!(u.upn, "alice@contoso.com");
assert_eq!(u.wam_token, "tok");
}

fn request_with_filesystem_policy() -> ExecutionRequest {
ExecutionRequest {
policy: ContainerPolicy {
Expand Down
11 changes: 5 additions & 6 deletions src/core/wxc_common/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,11 @@ pub enum TransportProtocol {
}

/// IsolationSession backend config. Carries the one-shot `user` field and
/// the per-phase state-aware nesting (`provision` / `start` / `stop` /
/// `deprovision`).
/// the per-phase state-aware nesting for the phases that take config
/// (`provision` / `start`). `stop`, `deprovision`, and `exec` take no
/// per-phase config payload: `stop` and `deprovision` are invoked with only
/// the top-level `phase` and `sandboxId`, and `exec` additionally carries
/// the top-level `process` block.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-gen", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
Expand All @@ -496,10 +499,6 @@ pub struct IsolationSession {
pub provision: Option<IsolationSessionPhase>,
/// State-aware start-phase configuration.
pub start: Option<IsolationSessionPhase>,
/// State-aware stop-phase configuration.
pub stop: Option<IsolationSessionPhase>,
/// State-aware deprovision-phase configuration.
pub deprovision: Option<IsolationSessionPhase>,
}

/// Per-phase IsolationSession configuration (state-aware lifecycle).
Expand Down
Loading