From 79938e0d820013e05963e4ae3215b85bfe6e403a Mon Sep 17 00:00:00 2001 From: Richie Gomez Date: Wed, 19 Aug 2026 16:11:17 -0700 Subject: [PATCH 1/7] Add Apple Container config surface Recognize typed experimental apple_container requests across the 0.8 contract, Rust and TypeScript builders, and generated artifacts. Keep execution fail-closed until the runtime backend is implemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b062729b-d6f3-47f6-9df0-6c1d5c8a1e5c --- schemas/dev/mxc-config.schema.0.8.0-dev.json | 47 +++++++ sdk/node/src/generated/wire.ts | 25 +++- sdk/node/src/index.ts | 2 + sdk/node/src/sandbox.ts | 44 +++++- sdk/node/src/types.ts | 19 ++- sdk/node/tests/unit/sandbox.test.ts | 64 ++++++++- sdk/node/tests/unit/wire-conformance.test.ts | 11 +- src/core/mxc-sdk/src/lib.rs | 6 +- .../src/dev/experimental.rs | 20 ++- src/core/mxc_config_contract/src/dev/mod.rs | 4 +- .../mxc_config_contract/src/dev/one_shot.rs | 2 + .../tests/v0_8_0_alpha/enums.rs | 1 + .../tests/v0_8_0_alpha/experimental.rs | 2 + .../experimental/apple_container.rs | 70 +++++++++ src/core/mxc_engine/src/dispatch.rs | 30 ++++ src/core/mxc_engine/src/lib.rs | 4 +- src/core/mxc_engine/src/policy.rs | 84 ++++++++++- src/core/mxc_engine/src/run.rs | 72 +++++++++- src/core/wxc_common/src/cmdline.rs | 3 +- src/core/wxc_common/src/config_parser.rs | 133 +++++++++++++++++- src/core/wxc_common/src/models.rs | 21 +++ src/core/wxc_common/src/wire.rs | 19 +++ 22 files changed, 654 insertions(+), 29 deletions(-) create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/apple_container.rs diff --git a/schemas/dev/mxc-config.schema.0.8.0-dev.json b/schemas/dev/mxc-config.schema.0.8.0-dev.json index d4d99bb991..40712f6f2c 100644 --- a/schemas/dev/mxc-config.schema.0.8.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.8.0-dev.json @@ -5,6 +5,35 @@ "description": "MXC container execution configuration. Defines the recommended config format for both one-shot and state-aware sandbox lifecycle requests. A few deprecated field spellings not listed here are also accepted via serde aliases.", "additionalProperties": false, "definitions": { + "AppleContainer": { + "description": "Apple Container backend config.", + "properties": { + "cpuCount": { + "description": "Requested virtual CPU count.", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "image": { + "description": "OCI image reference. The image must provide `/bin/sh`.", + "type": "string" + }, + "memoryMb": { + "description": "Requested memory limit in megabytes.", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "image" + ], + "type": "object" + }, "BaseProcessUi": { "additionalProperties": false, "description": "BaseProcessContainer UI isolation settings.", @@ -184,12 +213,30 @@ "bubblewrap" ], "type": "string" + }, + { + "description": "Apple Container VM backend (experimental, macOS).", + "enum": [ + "apple_container" + ], + "type": "string" } ] }, "Experimental": { "description": "Experimental features (only honored with `--experimental`). This block is intentionally **permissive** (no `deny_unknown_fields`): experimental backends are in flux, so the schema documents the known shapes for editor help without rejecting in-progress fields. The strict, closed contract is the stable (top-level) surface.", "properties": { + "apple_container": { + "anyOf": [ + { + "$ref": "#/definitions/AppleContainer" + }, + { + "type": "null" + } + ], + "description": "Apple Container backend config (macOS)." + }, "isolation_session": { "anyOf": [ { diff --git a/sdk/node/src/generated/wire.ts b/sdk/node/src/generated/wire.ts index 08363feef5..8d7cb69674 100644 --- a/sdk/node/src/generated/wire.ts +++ b/sdk/node/src/generated/wire.ts @@ -15,6 +15,25 @@ * Regenerate with: * cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- --ts sdk/node/src/generated/wire.ts */ +/** + * Apple Container backend config. + */ +export interface AppleContainer { + /** + * Requested virtual CPU count. + */ + cpuCount?: number | null; + /** + * OCI image reference. The image must provide `/bin/sh`. + */ + image: string; + /** + * Requested memory limit in megabytes. + */ + memoryMb?: number | null; + [k: string]: unknown; +} + /** * BaseProcessContainer UI isolation settings. */ @@ -68,12 +87,16 @@ export type ClipboardPolicy = "none" | "read" | "write" | "all"; /** * Containment backend (abstract intent or concrete backend). */ -export type Containment = "process" | "processcontainer" | "vm" | "windows_sandbox" | "lxc" | "microvm" | "hyperlight" | "wslc" | "seatbelt" | "isolation_session" | "bubblewrap"; +export type Containment = "process" | "processcontainer" | "vm" | "windows_sandbox" | "lxc" | "microvm" | "hyperlight" | "wslc" | "seatbelt" | "isolation_session" | "bubblewrap" | "apple_container"; /** * Experimental features (only honored with `--experimental`). This block is intentionally **permissive** (no `deny_unknown_fields`): experimental backends are in flux, so the schema documents the known shapes for editor help without rejecting in-progress fields. The strict, closed contract is the stable (top-level) surface. */ export interface Experimental { + /** + * Apple Container backend config (macOS). + */ + apple_container?: AppleContainer | null; /** * IsolationSession backend config (Windows). */ diff --git a/sdk/node/src/index.ts b/sdk/node/src/index.ts index d3b50fa9b1..a1ce5f072b 100644 --- a/sdk/node/src/index.ts +++ b/sdk/node/src/index.ts @@ -38,6 +38,7 @@ export { ContainmentTypes, ContainmentBackend, ExperimentalBackends, + AppleContainerConfig, ContainerConfig, PlatformSupport, UiCapabilitySupport, @@ -51,6 +52,7 @@ export { // Export sandbox spawning functions export { createConfigFromPolicy, + createAppleContainerConfig, spawnSandbox, spawnSandboxAsync, spawnSandboxFromConfig, diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index 350032fd74..9cf04f55a5 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -6,7 +6,13 @@ import * as os from 'os'; import { spawn, ChildProcess } from 'child_process'; import { randomBytes } from "crypto"; import { parse as semverParse } from 'semver'; -import { SandboxPolicy, ContainerConfig, ContainmentType, ContainmentBackend } from './types.js'; +import { + AppleContainerConfig, + SandboxPolicy, + ContainerConfig, + ContainmentType, + ContainmentBackend, +} from './types.js'; import { prepareSpawn, diagLogVersion, applyLinuxNetworkPolicy } from './helper.js'; import { diagLog } from './diagnostic.js'; import { MxcError, mxcErrorFromEnvelope } from './errors.js'; @@ -362,6 +368,42 @@ export function createConfigFromPolicy( throw new Error(`Containment type '${containment}' is not yet supported.`); } +/** + * Creates an experimental Apple Container config for macOS. + */ +export function createAppleContainerConfig( + policy: SandboxPolicy, + options: AppleContainerConfig, + containerName?: string, +): ContainerConfig { + if (os.platform() !== 'darwin') { + throw new Error('The apple_container backend is only supported on macOS.'); + } + if (policy.version !== SUPPORTED_VERSION) { + throw new Error(`Apple Container requires policy version '${SUPPORTED_VERSION}'.`); + } + if (!options.image.trim()) { + throw new Error('Apple Container image is required.'); + } + if (options.cpuCount !== undefined && + (!Number.isInteger(options.cpuCount) || options.cpuCount <= 0)) { + throw new Error('Apple Container cpuCount must be a positive integer.'); + } + if (options.memoryMb !== undefined && + (!Number.isSafeInteger(options.memoryMb) || options.memoryMb <= 0)) { + throw new Error('Apple Container memoryMb must be a positive integer.'); + } + + const config = createConfigFromPolicy(policy, 'process', containerName); + delete config.seatbelt; + config.containment = 'apple_container'; + config.experimental = { + ...config.experimental, + apple_container: { ...options }, + }; + return config; +} + /** * Builds a sandbox payload JSON object from the sandbox policy. * @param script The command line script to execute diff --git a/sdk/node/src/types.ts b/sdk/node/src/types.ts index de029318bd..04528b0be1 100644 --- a/sdk/node/src/types.ts +++ b/sdk/node/src/types.ts @@ -101,13 +101,14 @@ export type ContainmentBackend = | 'hyperlight' | 'seatbelt' | 'isolation_session' - | 'bubblewrap'; + | 'bubblewrap' + | 'apple_container'; /** * Containment values (abstract intent or concrete backend) that require * the `--experimental` flag. */ -export const ExperimentalBackends: readonly (ContainmentType | ContainmentBackend)[] = ['microvm', 'windows_sandbox', 'hyperlight', 'wslc', 'isolation_session']; +export const ExperimentalBackends: readonly (ContainmentType | ContainmentBackend)[] = ['microvm', 'windows_sandbox', 'hyperlight', 'wslc', 'isolation_session', 'apple_container']; /** * Clipboard access policy levels @@ -263,6 +264,18 @@ export interface WslcConfig { portMappings?: PortMapping[]; } +/** + * Apple Container backend configuration. + */ +export interface AppleContainerConfig { + /** OCI image reference. The image must provide `/bin/sh`. */ + image: string; + /** Requested virtual CPU count. Omit to let Apple Container choose. */ + cpuCount?: number; + /** Requested memory limit in megabytes. Omit to let Apple Container choose. */ + memoryMb?: number; +} + /** * Port mapping for host↔container port forwarding. */ @@ -327,6 +340,8 @@ export interface ContainerConfig { experimental?: { /** WSLC SDK configuration for Linux containers from Windows */ wslc?: WslcConfig; + /** Apple Container configuration for macOS */ + apple_container?: AppleContainerConfig; /** Telemetry configuration for experimental TraceLogging ETW support */ telemetry?: TelemetryConfig; }; diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index 054346cba0..7ca751948d 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -3,7 +3,13 @@ import { describe, it } from 'node:test'; import assert from 'node:assert'; -import { buildSandboxPayload, createConfigFromPolicy, spawnSandbox, spawnSandboxFromConfig } from '../../src/sandbox.js'; +import { + buildSandboxPayload, + createAppleContainerConfig, + createConfigFromPolicy, + spawnSandbox, + spawnSandboxFromConfig, +} from '../../src/sandbox.js'; import { resolveExecutableAndArgs } from '../../src/helper.js'; import { ContainerConfig, SandboxPolicy, SandboxingMethod } from '../../src/types.js'; import { platformSkip } from './test-helpers.js'; @@ -787,6 +793,55 @@ describe('createConfigFromPolicy', () => { restore(); } }); + + it('should build an Apple Container 0.8 config without a Seatbelt section', () => { + mockDarwin(); + try { + const config = createAppleContainerConfig( + { version: '0.8.0-alpha' }, + { + image: 'docker.io/library/alpine:3.23', + cpuCount: 2, + memoryMb: 1024, + }, + 'apple-test', + ); + + assert.strictEqual(config.containment, 'apple_container'); + assert.strictEqual(config.containerId, 'apple-test'); + assert.strictEqual(config.seatbelt, undefined); + assert.deepStrictEqual(config.experimental?.apple_container, { + image: 'docker.io/library/alpine:3.23', + cpuCount: 2, + memoryMb: 1024, + }); + } finally { + restore(); + } + }); + + it('should reject invalid Apple Container builder inputs', () => { + mockDarwin(); + try { + assert.throws( + () => createAppleContainerConfig({ version: '0.7.0-alpha' }, { image: 'alpine' }), + /requires policy version/, + ); + assert.throws( + () => createAppleContainerConfig({ version: '0.8.0-alpha' }, { image: ' ' }), + /image is required/, + ); + assert.throws( + () => createAppleContainerConfig( + { version: '0.8.0-alpha' }, + { image: 'alpine', cpuCount: 0 }, + ), + /cpuCount/, + ); + } finally { + restore(); + } + }); }); describe('network validation', () => { @@ -1076,6 +1131,13 @@ describe('resolveExecutableAndArgs (containment validation)', { skip: platformSk ); }); + it('should require experimental mode for apple_container', () => { + assert.throws( + () => resolveExecutableAndArgs(makeConfig('apple_container'), { executablePath: fakeExe }), + { message: /experimental mode/ }, + ); + }); + it('should NOT require experimental mode for explicit lxc containment', function (this: { skip: (reason?: string) => void }) { if (process.platform !== 'linux') { this.skip('lxc is Linux-only'); diff --git a/sdk/node/tests/unit/wire-conformance.test.ts b/sdk/node/tests/unit/wire-conformance.test.ts index 753c7c3f4b..cd33da3ac7 100644 --- a/sdk/node/tests/unit/wire-conformance.test.ts +++ b/sdk/node/tests/unit/wire-conformance.test.ts @@ -53,6 +53,7 @@ import type { ProcessContainerConfig, BaseProcessUiConfig, WslcConfig, + AppleContainerConfig, PortMapping as PublicPortMapping, LxcConfig, SeatbeltConfig, @@ -71,6 +72,7 @@ import type { ProcessContainer as WireProcessContainer, BaseProcessUi as WireBaseProcessUi, Wslc as WireWslc, + AppleContainer as WireAppleContainer, PortMapping as WirePortMapping, Lxc as WireLxc, Seatbelt as WireSeatbelt, @@ -133,6 +135,7 @@ type _UiVals = AssertTrue>; type _ProcessContainerVals = AssertTrue>; type _BaseProcessUiVals = AssertTrue>; type _WslcVals = AssertTrue>; +type _AppleContainerVals = AssertTrue>; type _PortMappingVals = AssertTrue>; type _SeatbeltVals = AssertTrue>; type _LxcVals = AssertTrue, WireLxc>>; @@ -153,6 +156,7 @@ type _LifecycleKeys = AssertTrue, never>>; type _BaseProcessUiKeys = AssertTrue, never>>; type _WslcKeys = AssertTrue, never>>; +type _AppleContainerKeys = AssertTrue, never>>; type _PortMappingKeys = AssertTrue, never>>; type _SeatbeltKeys = AssertTrue, never>>; @@ -199,6 +203,7 @@ type _BaseProcessUiWireKeys = AssertTrue, 'provision'>>; +type _AppleContainerWireKeys = AssertTrue, never>>; type _PortMappingWireKeys = AssertTrue, never>>; type _LxcWireKeys = AssertTrue, never>>; @@ -229,14 +234,14 @@ export type WireConformanceAssertions = [ _Clipboard, _Containment, _NetDefaultPolicy, _NetEnforcement, _BaseProcessUiIsolation, _PortProtocol, _ProcessVals, _LifecycleVals, _FilesystemVals, _NetworkVals, _UiVals, - _ProcessContainerVals, _BaseProcessUiVals, _WslcVals, _PortMappingVals, + _ProcessContainerVals, _BaseProcessUiVals, _WslcVals, _AppleContainerVals, _PortMappingVals, _SeatbeltVals, _LxcVals, _ProcessKeys, _LifecycleKeys, _FilesystemKeys, _NetworkKeys, _UiKeys, - _ProcessContainerKeys, _BaseProcessUiKeys, _WslcKeys, _PortMappingKeys, + _ProcessContainerKeys, _BaseProcessUiKeys, _WslcKeys, _AppleContainerKeys, _PortMappingKeys, _SeatbeltKeys, _LxcKeys, _RootVals, _RootKeys, _ProcessWireKeys, _LifecycleWireKeys, _FilesystemWireKeys, _NetworkWireKeys, - _UiWireKeys, _BaseProcessUiWireKeys, _WslcWireKeys, _PortMappingWireKeys, + _UiWireKeys, _BaseProcessUiWireKeys, _WslcWireKeys, _AppleContainerWireKeys, _PortMappingWireKeys, _LxcWireKeys, _ProcessContainerWireKeys, _SeatbeltWireKeys, _RootWireKeys, ]; diff --git a/src/core/mxc-sdk/src/lib.rs b/src/core/mxc-sdk/src/lib.rs index 9c997e94a1..3de710b7b4 100644 --- a/src/core/mxc-sdk/src/lib.rs +++ b/src/core/mxc-sdk/src/lib.rs @@ -139,9 +139,9 @@ mod sandbox; 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, Containment, Error, ErrorCode, FilesystemPolicyResult, PlatformSupport, - SandboxPolicy, SandboxRequest, WslcSection, + platform_support, temporary_files_policy, user_profile_policy, AppleContainerSection, + AvailableBackend, BackendCapability, Containment, Error, ErrorCode, FilesystemPolicyResult, + PlatformSupport, SandboxPolicy, SandboxRequest, WslcSection, }; pub use sandbox::{ diff --git a/src/core/mxc_config_contract/src/dev/experimental.rs b/src/core/mxc_config_contract/src/dev/experimental.rs index 2f8d3dfb07..bb4680f7c4 100644 --- a/src/core/mxc_config_contract/src/dev/experimental.rs +++ b/src/core/mxc_config_contract/src/dev/experimental.rs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use super::primitives::NonEmptyString; use super::primitives::OptionalField; -use std::num::NonZeroU16; +use std::num::{NonZeroU16, NonZeroU32, NonZeroU64}; /// Placeholder feature used to exercise experimental configuration plumbing. #[derive(Debug, serde::Deserialize)] @@ -90,6 +91,20 @@ pub struct OneShotWslc { pub port_mappings: OptionalField>, } +/// One-shot Apple Container backend settings. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct OneShotAppleContainer { + /// OCI image reference. The image must provide `/bin/sh`. + pub image: NonEmptyString, + /// Requested virtual CPU count. + #[serde(default)] + pub cpu_count: OptionalField, + /// Requested memory limit in megabytes. + #[serde(default)] + pub memory_mb: OptionalField, +} + /// Experimental settings. #[derive(Debug, serde::Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -103,6 +118,9 @@ pub struct OneShotExperimental { /// Optional one-shot WSLC backend settings. #[serde(default)] pub wslc: OptionalField, + /// Optional one-shot Apple Container backend settings. + #[serde(rename = "apple_container", default)] + pub apple_container: OptionalField, /// Optional telemetry override. #[serde(default)] pub telemetry: OptionalField, diff --git a/src/core/mxc_config_contract/src/dev/mod.rs b/src/core/mxc_config_contract/src/dev/mod.rs index 78626ba097..e189f8aeb1 100644 --- a/src/core/mxc_config_contract/src/dev/mod.rs +++ b/src/core/mxc_config_contract/src/dev/mod.rs @@ -111,8 +111,8 @@ mod primitives; mod stable; pub use experimental::{ - OneShotExperimental, OneShotWindowsSandbox, OneShotWslc, PortMapping, Telemetry, TestFeature, - TransportProtocol, + OneShotAppleContainer, OneShotExperimental, OneShotWindowsSandbox, OneShotWslc, PortMapping, + Telemetry, TestFeature, TransportProtocol, }; pub use network::{DefaultNetworkPolicy, Network, NetworkEnforcementMode, NetworkProxy}; pub use one_shot::{Containment as OneShotContainment, Request as OneShotRequest}; diff --git a/src/core/mxc_config_contract/src/dev/one_shot.rs b/src/core/mxc_config_contract/src/dev/one_shot.rs index 3b09d7550d..12f668adfa 100644 --- a/src/core/mxc_config_contract/src/dev/one_shot.rs +++ b/src/core/mxc_config_contract/src/dev/one_shot.rs @@ -39,6 +39,8 @@ pub enum Containment { Wslc => ["wslc"], /// Select the Windows IsolationSession backend. IsolationSession => ["isolation_session"], + /// Select the Apple Container backend. + AppleContainer => ["apple_container"], } } diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/enums.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/enums.rs index 8258a99464..3ed1671bc1 100644 --- a/src/core/mxc_config_contract/tests/v0_8_0_alpha/enums.rs +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/enums.rs @@ -18,6 +18,7 @@ fn accepts_every_containment_value() { "hyperlight", "isolation_session", "wslc", + "apple_container", ] { let json = format!( r#"{{ diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental.rs index 1d8719bd31..1c0f1e6bd8 100644 --- a/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental.rs +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#[path = "experimental/apple_container.rs"] +mod apple_container; #[path = "experimental/root.rs"] mod root; #[path = "experimental/test_and_telemetry.rs"] diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/apple_container.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/apple_container.rs new file mode 100644 index 0000000000..b945d95828 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/apple_container.rs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid, assert_valid}; + +fn request(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "containment": "apple_container", + "experimental": {{ + "apple_container": {{{fields}}} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ) +} + +#[test] +fn accepts_required_image_and_optional_resources() { + assert_valid(&request(r#""image": "docker.io/library/alpine:3.23""#)); + assert_valid(&request( + r#""image": "docker.io/library/alpine:3.23", "cpuCount": 1, "memoryMb": 1"#, + )); + assert_valid(&request(&format!( + r#""image": "docker.io/library/alpine:3.23", "cpuCount": {}, "memoryMb": {}"#, + u32::MAX, + u64::MAX + ))); +} + +#[test] +fn rejects_missing_empty_or_non_string_image() { + for fields in [ + "", + r#""image": """#, + r#""image": null"#, + r#""image": 123"#, + r#""image": true"#, + r#""image": []"#, + r#""image": {}"#, + ] { + assert_invalid(&request(fields)); + } +} + +#[test] +fn rejects_invalid_resource_values() { + for field in ["cpuCount", "memoryMb"] { + for value in ["0", "-1", "1.5", r#""1""#, "true", "[]", "{}"] { + assert_invalid(&request(&format!( + r#""image": "docker.io/library/alpine:3.23", "{field}": {value}"# + ))); + } + } + assert_invalid(&request( + r#""image": "docker.io/library/alpine:3.23", "cpuCount": 4294967296"#, + )); + assert_invalid(&request( + r#""image": "docker.io/library/alpine:3.23", "memoryMb": 18446744073709551616"#, + )); +} + +#[test] +fn rejects_unknown_or_duplicate_fields() { + assert_invalid(&request( + r#""image": "docker.io/library/alpine:3.23", "unknown": true"#, + )); + assert_invalid(&request(r#""image": "first", "image": "second""#)); +} diff --git a/src/core/mxc_engine/src/dispatch.rs b/src/core/mxc_engine/src/dispatch.rs index 79a4ac288e..acac63fa07 100644 --- a/src/core/mxc_engine/src/dispatch.rs +++ b/src/core/mxc_engine/src/dispatch.rs @@ -70,6 +70,16 @@ pub fn spawn_runner( ContainmentBackend::Bubblewrap => spawn_bubblewrap(request, logger), ContainmentBackend::ProcessContainer => spawn_process_container(request, logger), ContainmentBackend::Wslc => spawn_wslc(request, logger), + ContainmentBackend::AppleContainer => { + if !request.experimental_enabled { + return Err(MxcError::malformed_request( + "Apple Container is an experimental feature. Use --experimental flag.", + )); + } + Err(MxcError::unsupported_containment( + "Apple Container configuration is recognized, but streaming execution is not implemented", + )) + } other => Err(MxcError::unsupported_containment(format!( "the mxc engine does not yet support streaming for the '{}' backend", other.wire_name() @@ -295,6 +305,26 @@ mod tests { assert_eq!(err.code, MxcErrorCode::MalformedRequest); } + #[test] + fn streaming_apple_container_is_gated_then_unimplemented() { + let mut request = build_request(&minimal_policy(), None).expect("build_request"); + request.inner.containment = ContainmentBackend::AppleContainer; + let mut logger = Logger::new(Mode::Buffer); + + let error = match spawn_runner(&request.inner, &mut logger) { + Ok(_) => panic!("Apple Container must require experimental opt-in"), + Err(error) => error, + }; + assert_eq!(error.code, MxcErrorCode::MalformedRequest); + + request.inner.experimental_enabled = true; + let error = match spawn_runner(&request.inner, &mut logger) { + Ok(_) => panic!("Apple Container streaming must remain unimplemented"), + Err(error) => error, + }; + assert_eq!(error.code, MxcErrorCode::UnsupportedContainment); + } + #[test] fn streaming_rejects_unsupported_containment() { // LXC has no streaming path in the library; selecting it must surface a diff --git a/src/core/mxc_engine/src/lib.rs b/src/core/mxc_engine/src/lib.rs index e5fe40fb08..ceaa51500c 100644 --- a/src/core/mxc_engine/src/lib.rs +++ b/src/core/mxc_engine/src/lib.rs @@ -46,8 +46,8 @@ pub use platform::isolation_session_available; pub use platform::{platform_support, PlatformSupport}; pub use policy::{ available_tools_policy, build_request, build_request_with_containment, temporary_files_policy, - user_profile_policy, Containment, FilesystemPolicyResult, SandboxPolicy, SandboxRequest, - WslcSection, + user_profile_policy, AppleContainerSection, Containment, FilesystemPolicyResult, SandboxPolicy, + SandboxRequest, WslcSection, }; pub use probe::{available_backends, AvailableBackend, BackendCapability}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] diff --git a/src/core/mxc_engine/src/policy.rs b/src/core/mxc_engine/src/policy.rs index 410cdfd856..3b93835f5c 100644 --- a/src/core/mxc_engine/src/policy.rs +++ b/src/core/mxc_engine/src/policy.rs @@ -573,6 +573,37 @@ pub enum Containment { /// **Experimental** — the request must have experimental features enabled /// ([`SandboxRequest::set_experimental`]) or the spawn is rejected. Wslc(WslcSection), + /// Apple Container backend, configured by [`AppleContainerSection`]. + /// + /// **Experimental** — the request must have experimental features enabled. + /// The initial config-only implementation rejects execution until the + /// Apple Container management and runtime layers are added. + AppleContainer(AppleContainerSection), +} + +/// Apple Container settings, mirroring `experimental.apple_container`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AppleContainerSection { + /// OCI image reference. The image must provide `/bin/sh`. + pub image: String, + /// Requested virtual CPU count. `None` lets Apple Container choose. + pub cpu_count: Option, + /// Requested memory limit in megabytes. `None` lets Apple Container choose. + pub memory_mb: Option, +} + +impl AppleContainerSection { + fn wire(&self) -> serde_json::Value { + use serde_json::json; + let mut config = json!({ "image": self.image }); + if let Some(cpu_count) = self.cpu_count { + config["cpuCount"] = json!(cpu_count); + } + if let Some(memory_mb) = self.memory_mb { + config["memoryMb"] = json!(memory_mb); + } + config + } } /// WSL Container settings, mirroring the SDK's `experimental.wslc` config @@ -775,8 +806,9 @@ impl SandboxRequest { /// analogue of the SDK's `SandboxSpawnOptions.experimental` and the /// executor's `--experimental` flag. /// - /// Experimental backends (today: [`Containment::Wslc`]) refuse to spawn - /// unless this is set, so the gate stays fail-closed. + /// Experimental backends (today: [`Containment::Wslc`] and + /// [`Containment::AppleContainer`]) refuse to spawn unless this is set, so + /// the gate stays fail-closed. pub fn set_experimental(&mut self, enabled: bool) -> &mut Self { self.inner.experimental_enabled = enabled; self @@ -906,7 +938,10 @@ fn build_wire_config( // anyway to stay consistent with the SDK rather than diverging — keeping the // two ports reconciled matters more than being stricter here. let accepts_host_rules_without_outbound = cfg!(any(target_os = "linux", target_os = "macos")) - || matches!(containment, Containment::Wslc(_)); + || matches!( + containment, + Containment::Wslc(_) | Containment::AppleContainer(_) + ); if let Some(net) = &policy.network { if !accepts_host_rules_without_outbound @@ -935,6 +970,9 @@ fn build_wire_config( match containment { Containment::Process => apply_host_process_backend(&mut config, policy, &container_id), Containment::Wslc(wslc) => apply_wslc_backend(&mut config, wslc), + Containment::AppleContainer(apple_container) => { + apply_apple_container_backend(&mut config, apple_container) + } } Ok(config) } @@ -1048,6 +1086,15 @@ fn apply_wslc_backend(config: &mut serde_json::Value, wslc: &WslcSection) { config["experimental"] = json!({ "wslc": wslc.wire() }); } +fn apply_apple_container_backend( + config: &mut serde_json::Value, + apple_container: &AppleContainerSection, +) { + use serde_json::json; + config["containment"] = json!("apple_container"); + config["experimental"] = json!({ "apple_container": apple_container.wire() }); +} + /// Promote network enforcement to `firewall` when host rules are present and /// no cooperative proxy is configured — the Linux counterpart of the SDK's /// `applyLinuxNetworkPolicy`. @@ -1113,6 +1160,37 @@ mod tests { ); } + #[test] + fn apple_container_builder_maps_required_and_optional_config() { + let policy: super::SandboxPolicy = + serde_json::from_str(r#"{ "version": "0.8.0-alpha" }"#).expect("minimal policy parses"); + let section = super::AppleContainerSection { + image: "docker.io/library/alpine:3.23".to_string(), + cpu_count: Some(2), + memory_mb: Some(1024), + }; + + let request = super::build_request_with_containment( + &policy, + &super::Containment::AppleContainer(section), + Some("apple-test"), + ) + .expect("Apple Container config builds"); + + assert_eq!( + request.inner.containment, + wxc_common::models::ContainmentBackend::AppleContainer + ); + let config = request + .inner + .experimental + .apple_container + .expect("Apple Container config should be present"); + assert_eq!(config.image, "docker.io/library/alpine:3.23"); + assert_eq!(config.cpu_count, Some(2)); + assert_eq!(config.memory_mb, Some(1024)); + } + #[test] fn proxy_builtin_test_server_true_is_accepted() { let spec: ProxySpec = diff --git a/src/core/mxc_engine/src/run.rs b/src/core/mxc_engine/src/run.rs index b17ea9f2f6..4c9a99d9c7 100644 --- a/src/core/mxc_engine/src/run.rs +++ b/src/core/mxc_engine/src/run.rs @@ -23,8 +23,9 @@ //! //! Backend selection is per-host: the Windows body drives the ProcessContainer //! fallback tiers plus the Windows experimental backends; the Linux body -//! mirrors `lxc-exec` (Bubblewrap / LXC / experimental); the macOS body always -//! resolves to Seatbelt. +//! mirrors `lxc-exec` (Bubblewrap / LXC / experimental); the macOS body retains +//! its Seatbelt fallback except for explicit Apple Container requests, which +//! must never run under a weaker backend. use wxc_common::logger::Logger; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -199,6 +200,7 @@ fn resolve_runner_inner_windows( ContainmentBackend::Seatbelt => Err(MxcError::unsupported_containment( "Seatbelt backend is only available on macOS (use mxc-exec-mac)", )), + ContainmentBackend::AppleContainer => reject_unimplemented_apple_container(request), ContainmentBackend::Vm => Err(MxcError::unsupported_containment( "VM backend not yet implemented", )), @@ -287,6 +289,7 @@ fn resolve_runner_inner( "MicroVM is an experimental feature. Use --experimental flag.", )); } + #[cfg(feature = "microvm")] { Ok(ResolvedRunner::without_guard(Box::new( @@ -310,6 +313,7 @@ fn resolve_runner_inner( &request.lifecycle, ), ))), + ContainmentBackend::AppleContainer => reject_unimplemented_apple_container(request), ref other => { logger.log_line(&format!( "Note: containment {other:?} unsupported on lxc-exec; falling back to LXC." @@ -326,8 +330,9 @@ fn resolve_runner_inner( } // --------------------------------------------------------------------------- -// macOS: always Seatbelt (the SDK selects it on darwin; be lenient and log a -// note if the request asked for something else). +// macOS: retain the existing Seatbelt fallback while Apple Container is +// recognized but deliberately unavailable until its management/runtime +// increments land. // --------------------------------------------------------------------------- #[cfg(target_os = "macos")] @@ -337,6 +342,9 @@ fn resolve_runner_inner( ) -> Result { use wxc_common::sandbox_process::Runner; + if request.containment == ContainmentBackend::AppleContainer { + return reject_unimplemented_apple_container(request); + } if request.containment != ContainmentBackend::Seatbelt { logger.log_line("Note: Overriding containment backend to Seatbelt on macOS."); } @@ -345,6 +353,62 @@ fn resolve_runner_inner( )))) } +fn reject_unimplemented_apple_container( + request: &ExecutionRequest, +) -> Result { + if !request.experimental_enabled { + return Err(MxcError::malformed_request( + "Apple Container is an experimental feature. Use --experimental flag.", + )); + } + + Err(MxcError::unsupported_containment( + "Apple Container configuration is recognized, but runtime execution is not implemented", + )) +} + +#[cfg(all(test, target_os = "macos"))] +mod macos_tests { + use super::*; + use wxc_common::logger::Mode; + use wxc_common::mxc_error::MxcErrorCode; + + #[test] + fn apple_container_requires_experimental_opt_in() { + let request = ExecutionRequest { + containment: ContainmentBackend::AppleContainer, + ..Default::default() + }; + let mut logger = Logger::new(Mode::Buffer); + + let error = match resolve_runner_inner(&request, &mut logger) { + Ok(_) => panic!("Apple Container must require experimental opt-in"), + Err(error) => error, + }; + + assert_eq!(error.code, MxcErrorCode::MalformedRequest); + assert!(error.message.contains("--experimental")); + } + + #[test] + fn apple_container_does_not_fall_back_to_seatbelt() { + let request = ExecutionRequest { + containment: ContainmentBackend::AppleContainer, + experimental_enabled: true, + ..Default::default() + }; + let mut logger = Logger::new(Mode::Buffer); + + let error = match resolve_runner_inner(&request, &mut logger) { + Ok(_) => panic!("Apple Container must not fall back to Seatbelt"), + Err(error) => error, + }; + + assert_eq!(error.code, MxcErrorCode::UnsupportedContainment); + assert!(error.message.contains("not implemented")); + } +} + // --------------------------------------------------------------------------- // Any other host: no backend. // --------------------------------------------------------------------------- diff --git a/src/core/wxc_common/src/cmdline.rs b/src/core/wxc_common/src/cmdline.rs index 4eeb79d76f..97475d0ec6 100644 --- a/src/core/wxc_common/src/cmdline.rs +++ b/src/core/wxc_common/src/cmdline.rs @@ -42,7 +42,8 @@ impl CommandLineContext { ContainmentBackend::Wslc | ContainmentBackend::Lxc | ContainmentBackend::Seatbelt - | ContainmentBackend::Bubblewrap => Self::PosixShell, + | ContainmentBackend::Bubblewrap + | ContainmentBackend::AppleContainer => Self::PosixShell, ContainmentBackend::ProcessContainer | ContainmentBackend::Vm | ContainmentBackend::MicroVm diff --git a/src/core/wxc_common/src/config_parser.rs b/src/core/wxc_common/src/config_parser.rs index c3e393771b..0af553cca2 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -8,10 +8,10 @@ use crate::encoding::base64_decode; use crate::error::WxcError; use crate::logger::Logger; use crate::models::{ - CaptureDenialsConfig, CaptureDenialsMode, ContainerPolicy, ContainmentBackend, - ExecutionRequest, ExperimentalConfig, LifecycleConfig, LxcConfig, NetworkEnforcementMode, - NetworkPolicy, PortMapping, ProxyAddress, ProxyConfig, SeatbeltConfig, TelemetryConfig, - TestFeatureConfig, UiPolicy, WindowsSandboxConfig, WslcConfig, + AppleContainerConfig, CaptureDenialsConfig, CaptureDenialsMode, ContainerPolicy, + ContainmentBackend, ExecutionRequest, ExperimentalConfig, LifecycleConfig, LxcConfig, + NetworkEnforcementMode, NetworkPolicy, PortMapping, ProxyAddress, ProxyConfig, SeatbeltConfig, + TelemetryConfig, TestFeatureConfig, UiPolicy, WindowsSandboxConfig, WslcConfig, }; use crate::mxc_error::MxcError; use crate::state_aware_request::{MxcRequest, ParsedStateAwareRequest, Phase}; @@ -288,11 +288,18 @@ const SUPPORTED_VERSION: &str = ">=0.6, <=0.8"; #[cfg(test)] const CURRENT_SCHEMA_VERSION: &str = "0.8.0-alpha"; +const CURRENT_APPLE_CONTAINER_SCHEMA_VERSION: &str = "0.8.0-alpha"; + /// Known `experimental.` keys. Used by validation code to flag /// experimental backend sections that don't match the selected /// `containment`. Add a new entry when promoting a backend to a top-level /// section or graduating one from experimental. -const KNOWN_EXPERIMENTAL_BACKENDS: &[&str] = &["windows_sandbox", "wslc", "isolation_session"]; +const KNOWN_EXPERIMENTAL_BACKENDS: &[&str] = &[ + "windows_sandbox", + "wslc", + "isolation_session", + "apple_container", +]; /// Validate that the schema version (semver) is supported by this binary. /// Compares major.minor only — patch and pre-release labels are ignored. @@ -578,6 +585,9 @@ fn present_backend_sections(cfg: &wire::MxcConfig) -> Vec<&'static str> { if experimental.wslc.is_some() { push(ContainmentBackend::Wslc); } + if experimental.apple_container.is_some() { + push(ContainmentBackend::AppleContainer); + } if experimental.isolation_session.is_some() { push(ContainmentBackend::IsolationSession); } @@ -841,6 +851,18 @@ fn convert_wire_config( let containment = map_wire_containment(cfg.containment.as_ref()); validate_single_backend_section(containment.clone(), &present_backend_sections)?; + if containment == ContainmentBackend::AppleContainer { + if schema_version != CURRENT_APPLE_CONTAINER_SCHEMA_VERSION { + return Err(WxcError::ConfigParse(format!( + "Apple Container requires config version '{CURRENT_APPLE_CONTAINER_SCHEMA_VERSION}'" + ))); + } + if !present_backend_sections.contains(&"experimental.apple_container") { + return Err(WxcError::ConfigParse( + "Apple Container requires the 'experimental.apple_container' section".to_string(), + )); + } + } // LXC configuration let lxc_config = match cfg.lxc { @@ -1420,6 +1442,30 @@ fn convert_wire_config( } else { None }; + let apple_container = if let Some(ac) = raw_exp.apple_container { + if ac.image.trim().is_empty() { + return Err(WxcError::ConfigParse( + "experimental.apple_container.image must not be empty".to_string(), + )); + } + if ac.cpu_count == Some(0) { + return Err(WxcError::ConfigParse( + "experimental.apple_container.cpuCount must be greater than zero".to_string(), + )); + } + if ac.memory_mb == Some(0) { + return Err(WxcError::ConfigParse( + "experimental.apple_container.memoryMb must be greater than zero".to_string(), + )); + } + Some(AppleContainerConfig { + image: ac.image, + cpu_count: ac.cpu_count, + memory_mb: ac.memory_mb, + }) + } else { + None + }; if raw_exp.seatbelt.is_some() { let msg = "'experimental.seatbelt' has moved to the stable section; \ use top-level 'seatbelt' instead." @@ -1433,6 +1479,7 @@ fn convert_wire_config( test, windows_sandbox, wslc, + apple_container, telemetry, } } else { @@ -5657,6 +5704,82 @@ mod tests { ); } + #[test] + fn apple_container_config_maps_to_domain_model() { + let json = r#"{ + "version": "0.8.0-alpha", + "containment": "apple_container", + "process": {"commandLine": "echo hi"}, + "experimental": { + "apple_container": { + "image": "docker.io/library/alpine:3.23", + "cpuCount": 2, + "memoryMb": 1024 + } + } + }"#; + let encoded = base64_encode(json.as_bytes()); + let mut logger = test_logger(); + + let request = load_request(&encoded, &mut logger, true).unwrap(); + + assert_eq!(request.containment, ContainmentBackend::AppleContainer); + let config = request + .experimental + .apple_container + .expect("Apple Container config should be present"); + assert_eq!(config.image, "docker.io/library/alpine:3.23"); + assert_eq!(config.cpu_count, Some(2)); + assert_eq!(config.memory_mb, Some(1024)); + } + + #[test] + fn apple_container_requires_version_and_backend_section() { + for json in [ + r#"{ + "containment": "apple_container", + "process": {"commandLine": "echo hi"}, + "experimental": {"apple_container": {"image": "alpine:3.23"}} + }"#, + r#"{ + "version": "0.7.0-alpha", + "containment": "apple_container", + "process": {"commandLine": "echo hi"}, + "experimental": {"apple_container": {"image": "alpine:3.23"}} + }"#, + r#"{ + "version": "0.8.0-alpha", + "containment": "apple_container", + "process": {"commandLine": "echo hi"} + }"#, + ] { + let encoded = base64_encode(json.as_bytes()); + let mut logger = test_logger(); + assert!(load_request(&encoded, &mut logger, true).is_err()); + } + } + + #[test] + fn apple_container_rejects_empty_image_and_zero_resources() { + for fields in [ + r#""image": """#, + r#""image": "alpine:3.23", "cpuCount": 0"#, + r#""image": "alpine:3.23", "memoryMb": 0"#, + ] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "containment": "apple_container", + "process": {{"commandLine": "echo hi"}}, + "experimental": {{"apple_container": {{{fields}}}}} + }}"# + ); + let encoded = base64_encode(json.as_bytes()); + let mut logger = test_logger(); + assert!(load_request(&encoded, &mut logger, true).is_err()); + } + } + #[test] fn containment_seatbelt_accepted() { let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "seatbelt"}"#; diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index 0e18d36c63..3a83d076cc 100644 --- a/src/core/wxc_common/src/models.rs +++ b/src/core/wxc_common/src/models.rs @@ -48,6 +48,10 @@ pub enum ContainmentBackend { /// create namespace-isolated processes without root privileges. /// Selected on the wire as `"bubblewrap"`. Bubblewrap, + /// Apple Container — Linux OCI workload in a Virtualization.framework VM. + /// Experimental and available only on Apple silicon macOS hosts. + #[serde(rename = "apple_container")] + AppleContainer, } impl ContainmentBackend { @@ -64,6 +68,7 @@ impl ContainmentBackend { ContainmentBackend::IsolationSession => "isolation_session", ContainmentBackend::Seatbelt => "seatbelt", ContainmentBackend::Bubblewrap => "bubblewrap", + ContainmentBackend::AppleContainer => "apple_container", } } @@ -76,6 +81,7 @@ impl ContainmentBackend { ContainmentBackend::Lxc => Some("lxc"), ContainmentBackend::WindowsSandbox => Some("experimental.windows_sandbox"), ContainmentBackend::Wslc => Some("experimental.wslc"), + ContainmentBackend::AppleContainer => Some("experimental.apple_container"), ContainmentBackend::Seatbelt => Some("seatbelt"), ContainmentBackend::IsolationSession => Some("experimental.isolation_session"), ContainmentBackend::Bubblewrap @@ -130,6 +136,7 @@ impl From for ContainmentBackend { W::Seatbelt => Self::Seatbelt, W::IsolationSession => Self::IsolationSession, W::Bubblewrap => Self::Bubblewrap, + W::AppleContainer => Self::AppleContainer, } } } @@ -770,6 +777,18 @@ pub struct WslcConfig { pub port_mappings: Vec, } +/// Configuration for the experimental Apple Container backend. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AppleContainerConfig { + /// OCI image reference. The image must provide `/bin/sh`. + pub image: String, + /// Requested virtual CPU count. `None` lets Apple Container choose. + pub cpu_count: Option, + /// Requested memory limit in megabytes. `None` lets Apple Container choose. + pub memory_mb: Option, +} + impl Default for WslcConfig { fn default() -> Self { Self { @@ -831,6 +850,8 @@ pub struct ExperimentalConfig { pub windows_sandbox: Option, /// WSL Container (WSLC SDK) backend (experimental). pub wslc: Option, + /// Apple Container backend (experimental, macOS). + pub apple_container: Option, /// Telemetry configuration (experimental). pub telemetry: Option, } diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index ec489d19f3..443af09817 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -158,6 +158,8 @@ pub enum Containment { IsolationSession, /// Unprivileged Linux bubblewrap sandbox. Bubblewrap, + /// Apple Container VM backend (experimental, macOS). + AppleContainer, } /// Process execution settings. @@ -473,6 +475,8 @@ pub struct Experimental { pub windows_sandbox: Option, /// WSL container backend config. pub wslc: Option, + /// Apple Container backend config (macOS). + pub apple_container: Option, /// IsolationSession backend config (Windows). pub isolation_session: Option, /// Seatbelt backend config (pre-promotion alias). @@ -544,6 +548,21 @@ pub struct Wslc { pub provision: Option, } +/// Apple Container backend config. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "schema-gen", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct AppleContainer { + /// OCI image reference. The image must provide `/bin/sh`. + pub image: String, + /// Requested virtual CPU count. + #[cfg_attr(feature = "schema-gen", schemars(range(min = 1)))] + pub cpu_count: Option, + /// Requested memory limit in megabytes. + #[cfg_attr(feature = "schema-gen", schemars(range(min = 1)))] + pub memory_mb: Option, +} + /// Per-phase WSLc **provision** configuration (state-aware lifecycle), nested /// under `experimental.wslc.provision`. Carries only what the amortized daemon /// session honors: the container image (or a local tarball to import). From 4c25874f3737b594b583f8ea8fcd49ea7cd6b319 Mon Sep 17 00:00:00 2001 From: Richie Gomez Date: Wed, 19 Aug 2026 16:30:52 -0700 Subject: [PATCH 2/7] Fix Apple Container contract integration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b062729b-d6f3-47f6-9df0-6c1d5c8a1e5c --- docs/schema.md | 6 +++ schemas/dev/mxc-config.schema.0.8.0-dev.json | 1 + sdk/node/README.md | 22 ++++++++-- sdk/node/src/sandbox.ts | 8 +++- sdk/node/tests/unit/sandbox.test.ts | 7 +++ src/core/mxc-sdk/README.md | 37 +++++++++++++++- .../config_contract_adapters/dev/one_shot.rs | 16 +++++++ .../dev/one_shot_tests/experimental.rs | 43 +++++++++++++++++++ src/core/wxc_common/src/wire.rs | 1 + 9 files changed, 135 insertions(+), 6 deletions(-) diff --git a/docs/schema.md b/docs/schema.md index cdbcd31284..d003a9eab7 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -117,6 +117,11 @@ production configs and the dev schema when working on experimental features: { "windowsPort": 8080, "containerPort": 80, "protocol": "tcp" } ] }, + "apple_container": { // Apple Container settings (macOS, 0.8 only) + "image": "docker.io/library/alpine:3.23", // Required OCI image with /bin/sh + "cpuCount": 2, // Optional positive virtual CPU count + "memoryMb": 1024 // Optional positive memory limit in MB + }, "seatbelt": { // macOS sandbox settings (macOS only) "profileOverride": null, // Optional raw TinyScheme profile (escape hatch) "guiAccess": false, // Allow GUI Mach services / IOKit / pty for window-drawing apps @@ -270,6 +275,7 @@ force a particular backend. | `"hyperlight"` | MicroVM isolation via Hyperlight + Unikraft with an embedded CPython snapshot (experimental) | | `"isolation_session"` | Windows isolation session — runs the workload as a freshly-provisioned, per-execution isolated user account in its own OS-managed session (experimental). Dual-mode: one-shot and state-aware. | | `"seatbelt"` | macOS sandbox isolation (Seatbelt) | +| `"apple_container"` | Experimental Apple Container configuration surface for macOS. Requires schema version `0.8.0-alpha`, `--experimental`, and `experimental.apple_container.image`. Runtime execution is currently fail-closed with `unsupported_containment`. | | `"bubblewrap"` | Unprivileged Linux sandboxing via Bubblewrap/user namespaces (experimental) | Only the backend section matching the selected `containment` value is accepted; diff --git a/schemas/dev/mxc-config.schema.0.8.0-dev.json b/schemas/dev/mxc-config.schema.0.8.0-dev.json index 40712f6f2c..072e49046a 100644 --- a/schemas/dev/mxc-config.schema.0.8.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.8.0-dev.json @@ -18,6 +18,7 @@ }, "image": { "description": "OCI image reference. The image must provide `/bin/sh`.", + "minLength": 1, "type": "string" }, "memoryMb": { diff --git a/sdk/node/README.md b/sdk/node/README.md index 531232f394..6806812b3b 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -63,7 +63,7 @@ child.on('close', (code) => console.log('exit:', code)); Pick `0.7.0-alpha` for new code on any supported platform. -> **Stable schemas document only the non-experimental surface.** Experimental backends (`windows_sandbox`, `wslc`, `microvm`, `hyperlight`, `isolation_session`), the `experimental.*` block, and state-aware lifecycle live in `0.8.0-dev`. The parser still accepts them when paired with `--experimental` regardless of which schema your config validates against — schema choice affects editor validation, not runtime behavior. +> **Stable schemas document only the non-experimental surface.** Experimental backends (`windows_sandbox`, `wslc`, `microvm`, `hyperlight`, `isolation_session`, `apple_container`), the `experimental.*` block, and state-aware lifecycle live in `0.8.0-dev`. The parser still accepts them when paired with `--experimental` regardless of which schema your config validates against — schema choice affects editor validation, not runtime behavior. > **Network host allow/block lists are not implemented on Windows.** `network.allowedHosts` / `network.blockedHosts` have no enforcement on this platform — use `network.defaultPolicy` (`allow` / `block`) or `network.proxy` to constrain network access. @@ -73,9 +73,9 @@ Pick `0.7.0-alpha` for new code on any supported platform. | --- | --- | --- | --- | | Windows 11 24H2+ (verified on 25H2) | `processcontainer` | `windows_sandbox`, `wslc`, `microvm`, `isolation_session` | `processcontainer`: 26100 (24H2)
`isolation_session`: 26300.8553 ([Insider Preview](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26300-8553)) | | Linux x64 / ARM64 | `bubblewrap` | `lxc` | — | -| macOS ARM64 (schema `0.7.0-alpha`+) | `seatbelt` | — | — | +| macOS ARM64 (schema `0.7.0-alpha`+) | `seatbelt` | `apple_container` (config only) | — | -The default `processcontainer`, `bubblewrap`, `lxc`, and `seatbelt` backends work out of the box. **Experimental backends** (`windows_sandbox`, `wslc`, `microvm`, `isolation_session`, `hyperlight`) require `{ experimental: true }` in `SandboxSpawnOptions` when you spawn — see [Choosing a Backend](#choosing-a-backend). +The default `processcontainer`, `bubblewrap`, `lxc`, and `seatbelt` backends work out of the box. **Experimental backends** (`windows_sandbox`, `wslc`, `microvm`, `isolation_session`, `hyperlight`, `apple_container`) require `{ experimental: true }` in `SandboxSpawnOptions` when you spawn — see [Choosing a Backend](#choosing-a-backend). > **Hyperlight** is an opt-in build flavor (Linux x64 and Windows x64) gated by the `--with-hyperlight` cargo feature. Default shipped binaries do not include it; build from source with `build.bat --with-hyperlight` (Windows) or the equivalent cargo invocation on Linux. @@ -201,6 +201,7 @@ console.log(result.stdout); | `microvm` | `microvm` | Windows | Experimental | [`docs/nanvix-microvm/nanvix.md`](https://github.com/microsoft/mxc/blob/main/docs/nanvix-microvm/nanvix.md) — MicroVM via NanVix on Windows Hypervisor Platform | | `wslc` | (concrete only) | Windows | Experimental | [`docs/wsl/wsl-container-getting-started.md`](https://github.com/microsoft/mxc/blob/main/docs/wsl/wsl-container-getting-started.md) | | `isolation_session` | (concrete only) | Windows | Experimental | [`docs/isolation-session/oneshot.md`](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/oneshot.md) | +| `apple_container` | (concrete only) | macOS ARM64 | Experimental, config only | — | Experimental backends require `{ experimental: true }` in `SandboxSpawnOptions`: @@ -210,6 +211,21 @@ config.process!.commandLine = 'cmd /c whoami'; const pty = spawnSandboxFromConfig(config, { experimental: true }); ``` +Apple Container has a dedicated builder because it requires an OCI image and +schema version `0.8.0-alpha`: + +```typescript +const config = createAppleContainerConfig( + { version: '0.8.0-alpha' }, + { image: 'docker.io/library/alpine:3.23', cpuCount: 2, memoryMb: 1024 }, +); +config.process!.commandLine = 'echo hello'; +``` + +This increment exposes and validates the configuration contract only. +`spawnSandboxFromConfig(config, { experimental: true })` currently fails with +`unsupported_containment` before creating Apple Container resources. + Backend-specific tuning lives on the returned `ContainerConfig`. The full set of fields per backend is in the JSON schemas — they're the source of truth: - Stable backends: [`schemas/stable/`](https://github.com/microsoft/mxc/tree/main/schemas/stable/) diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index 9cf04f55a5..b1327b44a3 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -370,6 +370,9 @@ export function createConfigFromPolicy( /** * Creates an experimental Apple Container config for macOS. + * + * Runtime execution is not implemented yet; spawning the returned config + * currently fails with `unsupported_containment` before creating resources. */ export function createAppleContainerConfig( policy: SandboxPolicy, @@ -386,8 +389,9 @@ export function createAppleContainerConfig( throw new Error('Apple Container image is required.'); } if (options.cpuCount !== undefined && - (!Number.isInteger(options.cpuCount) || options.cpuCount <= 0)) { - throw new Error('Apple Container cpuCount must be a positive integer.'); + (!Number.isInteger(options.cpuCount) || options.cpuCount <= 0 || + options.cpuCount > 0xFFFF_FFFF)) { + throw new Error('Apple Container cpuCount must be a positive 32-bit integer.'); } if (options.memoryMb !== undefined && (!Number.isSafeInteger(options.memoryMb) || options.memoryMb <= 0)) { diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index 7ca751948d..5500762c20 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -838,6 +838,13 @@ describe('createConfigFromPolicy', () => { ), /cpuCount/, ); + assert.throws( + () => createAppleContainerConfig( + { version: '0.8.0-alpha' }, + { image: 'alpine', cpuCount: 0x1_0000_0000 }, + ), + /cpuCount/, + ); } finally { restore(); } diff --git a/src/core/mxc-sdk/README.md b/src/core/mxc-sdk/README.md index 030ba54574..f94cf0b103 100644 --- a/src/core/mxc-sdk/README.md +++ b/src/core/mxc-sdk/README.md @@ -336,13 +336,48 @@ default): |---------|-------------------------------------------------|-------------------------| | Linux | Bubblewrap | `Containment::Process` | | macOS | Seatbelt | `Containment::Process` | +| macOS | Apple Container (config only) | `Containment::AppleContainer` | | Windows | ProcessContainer (AppContainer + BaseContainer) | `Containment::Process` | | Windows | WSLC (WSL Container) | `Containment::Wslc` | -Any other backend (Windows Sandbox, IsolationSession, MicroVM, Hyperlight, LXC) +Any other backend (Windows Sandbox, IsolationSession, MicroVM, Hyperlight, LXC, +Apple Container) returns an [`Error`] with [`ErrorCode::UnsupportedContainment`]; drive the standalone executor binaries for those. +### Apple Container (experimental config surface) + +Apple Container requests require schema version `0.8.0-alpha`, an OCI image, +and experimental opt-in: + +```rust,no_run +use mxc_sdk::{ + build_request_with_containment, AppleContainerSection, Containment, SandboxPolicy, +}; + +let policy = SandboxPolicy { + version: "0.8.0-alpha".to_string(), + filesystem: None, + network: None, + ui: None, + timeout_ms: None, + capture_denials: None, +}; +let apple = AppleContainerSection { + image: "docker.io/library/alpine:3.23".to_string(), + cpu_count: Some(2), + memory_mb: Some(1024), +}; +let mut request = + build_request_with_containment(&policy, &Containment::AppleContainer(apple), None)?; +request.set_script("echo hello").set_experimental(true); +# Ok::<(), mxc_sdk::Error>(()) +``` + +This increment builds and validates the request only. Both [`run`] and +[`spawn_sandbox`] return [`ErrorCode::UnsupportedContainment`] before creating +resources. + ### WSLC (experimental) WSLC runs a Linux container on a Windows host through the WSLC SDK. It is diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs index 5291720a35..59d746a13f 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs @@ -24,6 +24,7 @@ fn convert_containment(value: contract::OneShotContainment) -> wire::Containment contract::OneShotContainment::Hyperlight => wire::Containment::Hyperlight, contract::OneShotContainment::Wslc => wire::Containment::Wslc, contract::OneShotContainment::IsolationSession => wire::Containment::IsolationSession, + contract::OneShotContainment::AppleContainer => wire::Containment::AppleContainer, } } @@ -327,6 +328,19 @@ fn convert_wslc(value: contract::OneShotWslc) -> wire::Wslc { } } +fn convert_apple_container(value: contract::OneShotAppleContainer) -> wire::AppleContainer { + let contract::OneShotAppleContainer { + image, + cpu_count, + memory_mb, + } = value; + wire::AppleContainer { + image: image.into_inner(), + cpu_count: cpu_count.into_option().map(|value| value.get()), + memory_mb: memory_mb.into_option().map(|value| value.get()), + } +} + fn convert_telemetry(value: contract::Telemetry) -> wire::Telemetry { let contract::Telemetry { enabled } = value; wire::Telemetry { @@ -339,12 +353,14 @@ fn convert_experimental(value: contract::OneShotExperimental) -> wire::Experimen test, windows_sandbox, wslc, + apple_container, telemetry, } = value; wire::Experimental { test: test.into_option().map(convert_test), windows_sandbox: windows_sandbox.into_option().map(convert_windows_sandbox), wslc: wslc.into_option().map(convert_wslc), + apple_container: apple_container.into_option().map(convert_apple_container), isolation_session: None, seatbelt: None, telemetry: telemetry.into_option().map(convert_telemetry), diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs index cc5d42df8a..74101e157a 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs @@ -64,6 +64,21 @@ const WSLC_REQUEST_JSON: &str = r#"{ } }"#; +const APPLE_CONTAINER_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "apple_container", + "process": { + "commandLine": "echo hello" + }, + "experimental": { + "apple_container": { + "image": "docker.io/library/alpine:3.23", + "cpuCount": 2, + "memoryMb": 1024 + } + } +}"#; + #[test] fn windows_sandbox_maps_expected_wire_fields() { let wire = adapt(WINDOWS_SANDBOX_REQUEST_JSON); @@ -160,6 +175,25 @@ fn wslc_maps_expected_wire_fields() { assert!(experimental.telemetry.is_none()); } +#[test] +fn apple_container_maps_expected_wire_fields() { + let wire = adapt(APPLE_CONTAINER_REQUEST_JSON); + + assert!(matches!( + wire.containment, + Some(super::wire::Containment::AppleContainer) + )); + + let experimental = wire.experimental.expect("experimental should be populated"); + let apple_container = experimental + .apple_container + .expect("apple_container should be populated"); + + assert_eq!(apple_container.image, "docker.io/library/alpine:3.23"); + assert_eq!(apple_container.cpu_count, Some(2)); + assert_eq!(apple_container.memory_mb, Some(1024)); +} + #[test] fn windows_sandbox_matches_current_wire_deserialization() { assert_matches_current_wire_deserialization(WINDOWS_SANDBOX_REQUEST_JSON); @@ -170,6 +204,11 @@ fn wslc_matches_current_wire_deserialization() { assert_matches_current_wire_deserialization(WSLC_REQUEST_JSON); } +#[test] +fn apple_container_matches_current_wire_deserialization() { + assert_matches_current_wire_deserialization(APPLE_CONTAINER_REQUEST_JSON); +} + #[test] fn test_feature_and_telemetry_match_current_wire_deserialization() { assert_matches_current_wire_deserialization(TEST_FEATURE_AND_TELEMETRY_REQUEST_JSON); @@ -205,6 +244,10 @@ const DEVELOPMENT_CONTAINMENT_CASES: &[DevelopmentContainmentCase] = &[ input: "isolation_session", expected: "isolation_session", }, + DevelopmentContainmentCase { + input: "apple_container", + expected: "apple_container", + }, ]; #[test] diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index 443af09817..80dcee7d78 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -554,6 +554,7 @@ pub struct Wslc { #[serde(rename_all = "camelCase")] pub struct AppleContainer { /// OCI image reference. The image must provide `/bin/sh`. + #[cfg_attr(feature = "schema-gen", schemars(length(min = 1)))] pub image: String, /// Requested virtual CPU count. #[cfg_attr(feature = "schema-gen", schemars(range(min = 1)))] From d1369cf99c389b559b9184f11c9187a3f6abc26a Mon Sep 17 00:00:00 2001 From: Richie Gomez Date: Wed, 26 Aug 2026 09:55:26 -0700 Subject: [PATCH 3/7] Address Apple Container review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b062729b-d6f3-47f6-9df0-6c1d5c8a1e5c --- sdk/node/src/sandbox.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index cf4325f4b4..95a2d39b3a 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -19,6 +19,7 @@ import { MxcError, mxcErrorFromEnvelope } from './errors.js'; const SUPPORTED_VERSION = '0.9.0-alpha'; const MIN_VERSION = '0.6.0-alpha'; +const MAX_U32 = 0xFFFF_FFFF; /** * Generates a random 8-character alphanumeric string for the app container name. @@ -441,6 +442,8 @@ export function createConfigFromPolicy( /** * Creates an experimental Apple Container config for macOS. * + * TODO(#972): Remove the config-only warning when Apple Container runtime + * execution lands. * Runtime execution is not implemented yet; spawning the returned config * currently fails with `unsupported_containment` before creating resources. */ @@ -460,7 +463,7 @@ export function createAppleContainerConfig( } if (options.cpuCount !== undefined && (!Number.isInteger(options.cpuCount) || options.cpuCount <= 0 || - options.cpuCount > 0xFFFF_FFFF)) { + options.cpuCount > MAX_U32)) { throw new Error('Apple Container cpuCount must be a positive 32-bit integer.'); } if (options.memoryMb !== undefined && From 9eb2900119da777801bb30f33cd16ec993d42afb Mon Sep 17 00:00:00 2001 From: Richie Gomez Date: Wed, 26 Aug 2026 10:05:20 -0700 Subject: [PATCH 4/7] Address additional Apple Container review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b062729b-d6f3-47f6-9df0-6c1d5c8a1e5c --- .../dev/mxc-config.schema.0.9.0-alpha.json | 25 ++++ src/core/mxc-sdk/README.md | 1 - src/core/mxc-sdk/src/lib.rs | 6 +- .../mxc_config_contract/src/dev/one_shot.rs | 113 +++++++++++++++--- .../mxc_config_contract/src/dev/schema.rs | 46 ++++++- .../tests/v0_9_0_alpha/enums.rs | 22 +++- 6 files changed, 192 insertions(+), 21 deletions(-) diff --git a/schemas/dev/mxc-config.schema.0.9.0-alpha.json b/schemas/dev/mxc-config.schema.0.9.0-alpha.json index 798607bc90..e4e282fdff 100644 --- a/schemas/dev/mxc-config.schema.0.9.0-alpha.json +++ b/schemas/dev/mxc-config.schema.0.9.0-alpha.json @@ -950,6 +950,31 @@ "macos_sandbox" ] } + }, + { + "else": true, + "if": { + "properties": { + "containment": { + "const": "apple_container" + } + }, + "required": [ + "containment" + ] + }, + "then": { + "properties": { + "experimental": { + "required": [ + "apple_container" + ] + } + }, + "required": [ + "experimental" + ] + } } ], "description": "A complete one-shot `0.9.0-alpha` configuration request.", diff --git a/src/core/mxc-sdk/README.md b/src/core/mxc-sdk/README.md index 9ac7ad5d8f..394e11562f 100644 --- a/src/core/mxc-sdk/README.md +++ b/src/core/mxc-sdk/README.md @@ -416,7 +416,6 @@ let policy = SandboxPolicy { network: None, ui: None, timeout_ms: None, - capture_denials: None, }; let apple = AppleContainerSection { image: "docker.io/library/alpine:3.23".to_string(), diff --git a/src/core/mxc-sdk/src/lib.rs b/src/core/mxc-sdk/src/lib.rs index af0493f4ab..65cb81132e 100644 --- a/src/core/mxc-sdk/src/lib.rs +++ b/src/core/mxc-sdk/src/lib.rs @@ -49,6 +49,7 @@ //! | ProcessContainer (AppContainer / BaseContainer) | Windows | [`Containment::Process`] | //! | Explicit ProcessContainer configuration | Windows | [`Containment::ProcessContainer`] | //! | WSLC (WSL Container) | Windows | [`Containment::Wslc`] | +//! | Apple Container (config only) | macOS | [`Containment::AppleContainer`] | //! //! WSLC is **experimental**: build with the crate's `wslc` feature, and call //! [`SandboxRequest::set_experimental(true)`](SandboxRequest::set_experimental) @@ -58,7 +59,10 @@ //! Backends with no [`Containment`] variant return an [`Error`] with //! [`ErrorCode::UnsupportedContainment`]; drive the standalone executor //! binaries for those. IsolationSession refuses the one-shot surface the same -//! way, and is reached through the state-aware lifecycle below. +//! way, and is reached through the state-aware lifecycle below. Apple +//! Container currently exposes configuration only: [`run`] and +//! [`spawn_sandbox`] return [`ErrorCode::UnsupportedContainment`] before +//! creating resources. //! //! # Diagnosing a failure //! diff --git a/src/core/mxc_config_contract/src/dev/one_shot.rs b/src/core/mxc_config_contract/src/dev/one_shot.rs index 3dd3f04d25..30dcaf291a 100644 --- a/src/core/mxc_config_contract/src/dev/one_shot.rs +++ b/src/core/mxc_config_contract/src/dev/one_shot.rs @@ -44,56 +44,137 @@ string_enum! { } /// A complete one-shot `0.9.0-alpha` configuration request. -#[derive(Debug, serde::Deserialize)] +#[derive(Debug)] #[cfg_attr(feature = "schema-gen", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schema-gen", schemars(rename = "OneShotRequest"))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr( + feature = "schema-gen", + serde(rename_all = "camelCase", deny_unknown_fields) +)] pub struct Request { /// Optional JSON Schema reference for editor validation. - #[serde(rename = "$schema", default)] + #[cfg_attr(feature = "schema-gen", serde(rename = "$schema", default))] pub schema: OptionalField, /// Optional human-readable annotation ignored by the runtime. - #[serde(rename = "_comment", default)] + #[cfg_attr(feature = "schema-gen", serde(rename = "_comment", default))] pub comment: OptionalField, /// The exact contract version marker. pub version: Version, /// Optional externally assigned container identifier. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub container_id: OptionalField, /// Optional containment selection. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub containment: OptionalField, /// Optional lifecycle settings. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub lifecycle: OptionalField, /// The process to execute. pub process: Process, /// Optional filesystem policy. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub filesystem: OptionalField, /// Optional fallback consent. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub fallback: OptionalField, /// Optional network policy. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub network: OptionalField, /// Optional cross-platform user-interface policy. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub ui: OptionalField, /// Optional ProcessContainer settings. /// The legacy `appContainer` spelling is accepted as an alias. - #[serde(alias = "appContainer", default)] + #[cfg_attr(feature = "schema-gen", serde(alias = "appContainer", default))] pub process_container: OptionalField, /// Optional LXC distribution settings. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub lxc: OptionalField, /// Optional macOS Seatbelt configuration. - #[serde(alias = "macos_sandbox", default)] + #[cfg_attr(feature = "schema-gen", serde(alias = "macos_sandbox", default))] pub seatbelt: OptionalField, /// Optional runtime configuration settings. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub runtime_config: OptionalField, /// Optional experimental settings. - #[serde(default)] + #[cfg_attr(feature = "schema-gen", serde(default))] pub experimental: OptionalField, } + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct UncheckedRequest { + #[serde(rename = "$schema", default)] + schema: OptionalField, + #[serde(rename = "_comment", default)] + comment: OptionalField, + version: Version, + #[serde(default)] + container_id: OptionalField, + #[serde(default)] + containment: OptionalField, + #[serde(default)] + lifecycle: OptionalField, + process: Process, + #[serde(default)] + filesystem: OptionalField, + #[serde(default)] + fallback: OptionalField, + #[serde(default)] + network: OptionalField, + #[serde(default)] + ui: OptionalField, + #[serde(alias = "appContainer", default)] + process_container: OptionalField, + #[serde(default)] + lxc: OptionalField, + #[serde(alias = "macos_sandbox", default)] + seatbelt: OptionalField, + #[serde(default)] + runtime_config: OptionalField, + #[serde(default)] + experimental: OptionalField, +} + +impl<'de> serde::Deserialize<'de> for Request { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + let request = UncheckedRequest::deserialize(deserializer)?; + if matches!( + request.containment.as_ref(), + Some(Containment::AppleContainer) + ) && request + .experimental + .as_ref() + .and_then(|experimental| experimental.apple_container.as_ref()) + .is_none() + { + return Err(D::Error::custom( + "apple_container containment requires experimental.apple_container", + )); + } + + Ok(Self { + schema: request.schema, + comment: request.comment, + version: request.version, + container_id: request.container_id, + containment: request.containment, + lifecycle: request.lifecycle, + process: request.process, + filesystem: request.filesystem, + fallback: request.fallback, + network: request.network, + ui: request.ui, + process_container: request.process_container, + lxc: request.lxc, + seatbelt: request.seatbelt, + runtime_config: request.runtime_config, + experimental: request.experimental, + }) + } +} diff --git a/src/core/mxc_config_contract/src/dev/schema.rs b/src/core/mxc_config_contract/src/dev/schema.rs index 0bbe1bae32..4b50ea6499 100644 --- a/src/core/mxc_config_contract/src/dev/schema.rs +++ b/src/core/mxc_config_contract/src/dev/schema.rs @@ -113,6 +113,29 @@ fn exclude_duplicate_alias( })); } +fn require_apple_container_settings(definitions: &mut Value) { + let one_shot = definitions["OneShotRequest"] + .as_object_mut() + .expect("one-shot request definition"); + let constraints = one_shot + .entry("allOf") + .or_insert_with(|| Value::Array(Vec::new())) + .as_array_mut() + .expect("allOf constraints"); + constraints.push(branch( + discriminator("containment", "apple_container"), + json!({ + "properties": { + "experimental": { + "required": ["apple_container"] + } + }, + "required": ["experimental"] + }), + Value::Bool(true), + )); +} + /// Generates the unrendered JSON Schema for the mutable `0.9.0-alpha` /// development contract. /// @@ -160,6 +183,7 @@ pub fn development_schema() -> Value { "seatbelt", "macos_sandbox", ); + require_apple_container_settings(&mut definitions); json!({ "$schema": "http://json-schema.org/draft-07/schema#", @@ -268,7 +292,27 @@ mod tests { .as_array() .expect("alias constraints") .len(), - 2 + 3 + ); + } + + #[test] + fn one_shot_schema_requires_apple_container_settings() { + let schema = development_schema(); + let constraints = schema["definitions"]["OneShotRequest"]["allOf"] + .as_array() + .expect("one-shot constraints"); + let apple_container = constraints + .iter() + .find(|constraint| { + constraint["if"]["properties"]["containment"]["const"] == json!("apple_container") + }) + .expect("Apple Container constraint"); + + assert_eq!(apple_container["then"]["required"], json!(["experimental"])); + assert_eq!( + apple_container["then"]["properties"]["experimental"]["required"], + json!(["apple_container"]) ); } diff --git a/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs b/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs index cc381ab718..e47c77c946 100644 --- a/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs +++ b/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs @@ -5,7 +5,7 @@ use crate::common::{assert_invalid, assert_valid}; // Enum value tests #[test] -fn accepts_every_containment_value() { +fn accepts_every_unconditional_containment_value() { for containment in [ "process", "processcontainer", @@ -18,7 +18,6 @@ fn accepts_every_containment_value() { "hyperlight", "isolation_session", "wslc", - "apple_container", ] { let json = format!( r#"{{ @@ -32,6 +31,25 @@ fn accepts_every_containment_value() { } } +#[test] +fn rejects_apple_container_without_experimental_settings() { + assert_invalid( + r#"{ + "version": "0.9.0-alpha", + "containment": "apple_container", + "process": {"commandLine": "echo"} + }"#, + ); + assert_invalid( + r#"{ + "version": "0.9.0-alpha", + "containment": "apple_container", + "experimental": {}, + "process": {"commandLine": "echo"} + }"#, + ); +} + #[test] fn rejects_invalid_containment_value() { assert_invalid( From 51f2510329389404b112e467ac3f10ce07bc0e19 Mon Sep 17 00:00:00 2001 From: Richie Gomez Date: Wed, 26 Aug 2026 11:20:56 -0700 Subject: [PATCH 5/7] Fix Apple Container adapter tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b062729b-d6f3-47f6-9df0-6c1d5c8a1e5c --- sdk/node/README.md | 5 ++++- .../dev/one_shot_tests/experimental.rs | 4 ---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sdk/node/README.md b/sdk/node/README.md index 98d2c291fc..63f6120337 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -62,9 +62,12 @@ child.on('close', (code) => console.log('exit:', code)); | `0.8.0-alpha` | Stable (current) | [`schemas/stable/mxc-config.schema.0.8.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.8.0-alpha.json) | | `0.9.0-alpha` | Dev (experimental backends, the `experimental.*` block, state-aware sandbox lifecycle) | [`schemas/dev/mxc-config.schema.0.9.0-dev.json`](https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.9.0-dev.json) | -Pick `0.8.0-alpha` for new code on any supported platform. +Pick `0.8.0-alpha` for new code on any supported platform unless selecting +Apple Container, which requires `0.9.0-alpha`. > **Stable schemas document only the non-experimental surface.** Experimental backends (`windows_sandbox`, `wslc`, `microvm`, `hyperlight`, `isolation_session`, `apple_container`), the `experimental.*` block, and state-aware lifecycle live in `0.9.0-dev`. The parser still accepts them when paired with `--experimental` regardless of which schema your config validates against — schema choice affects editor validation, not runtime behavior. +> Apple Container is the exception: its config version must be exactly +> `0.9.0-alpha`. > **Network host allow/block lists are not implemented on Windows.** `network.allowedHosts` / `network.blockedHosts` have no enforcement on this platform — use `network.defaultPolicy` (`allow` / `block`) or `network.proxy` to constrain network access. diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs index af13543651..553a0e0d95 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs @@ -244,10 +244,6 @@ const DEVELOPMENT_CONTAINMENT_CASES: &[DevelopmentContainmentCase] = &[ input: "isolation_session", expected: "isolation_session", }, - DevelopmentContainmentCase { - input: "apple_container", - expected: "apple_container", - }, ]; #[test] From 583b4013554f4e4bf201e788e33fb6ba4ff96e08 Mon Sep 17 00:00:00 2001 From: Richie Gomez Date: Wed, 26 Aug 2026 11:48:40 -0700 Subject: [PATCH 6/7] Tighten Apple Container config validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b062729b-d6f3-47f6-9df0-6c1d5c8a1e5c --- .../dev/mxc-config.schema.0.9.0-alpha.json | 6 +- sdk/node/README.md | 2 + sdk/node/src/generated/v0_9_0_alpha/wire.ts | 4 +- sdk/node/src/sandbox.ts | 1 + sdk/node/tests/unit/sandbox.test.ts | 10 ++- .../src/dev/experimental.rs | 4 +- src/core/mxc_config_contract/src/dev/mod.rs | 2 +- .../mxc_config_contract/src/dev/primitives.rs | 72 +++++++++++++++++++ .../experimental/apple_container.rs | 2 + 9 files changed, 97 insertions(+), 6 deletions(-) diff --git a/schemas/dev/mxc-config.schema.0.9.0-alpha.json b/schemas/dev/mxc-config.schema.0.9.0-alpha.json index e4e282fdff..34b93896f2 100644 --- a/schemas/dev/mxc-config.schema.0.9.0-alpha.json +++ b/schemas/dev/mxc-config.schema.0.9.0-alpha.json @@ -147,7 +147,7 @@ "type": "integer" }, "image": { - "$ref": "#/definitions/NonEmptyString", + "$ref": "#/definitions/NonBlankString", "description": "OCI image reference. The image must provide `/bin/sh`." }, "memoryMb": { @@ -799,6 +799,10 @@ }, "type": "object" }, + "NonBlankString": { + "pattern": "\\S", + "type": "string" + }, "NonEmptyString": { "minLength": 1, "type": "string" diff --git a/sdk/node/README.md b/sdk/node/README.md index 63f6120337..4372f72b94 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -285,6 +285,8 @@ Apple Container has a dedicated builder because it requires an OCI image and schema version `0.9.0-alpha`: ```typescript +import { createAppleContainerConfig } from '@microsoft/mxc-sdk'; + const config = createAppleContainerConfig( { version: '0.9.0-alpha' }, { image: 'docker.io/library/alpine:3.23', cpuCount: 2, memoryMb: 1024 }, diff --git a/sdk/node/src/generated/v0_9_0_alpha/wire.ts b/sdk/node/src/generated/v0_9_0_alpha/wire.ts index 225bb7d3eb..ebd084b7e5 100644 --- a/sdk/node/src/generated/v0_9_0_alpha/wire.ts +++ b/sdk/node/src/generated/v0_9_0_alpha/wire.ts @@ -23,7 +23,7 @@ export interface AppleContainer { /** * OCI image reference. The image must provide `/bin/sh`. */ - image: NonEmptyString; + image: NonBlankString; /** * Requested memory limit in megabytes. */ @@ -413,6 +413,8 @@ export interface NetworkRule { to?: NetworkPeer[]; } +export type NonBlankString = string; + export type NonEmptyString = string; export type OneShotContainment = "process" | "processcontainer" | "appcontainer" | "lxc" | "bubblewrap" | "seatbelt" | "macos_sandbox" | "vm" | "windows_sandbox" | "microvm" | "hyperlight" | "wslc" | "isolation_session" | "apple_container"; diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index 95a2d39b3a..21a8bb9b75 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -473,6 +473,7 @@ export function createAppleContainerConfig( const config = createConfigFromPolicy(policy, 'process', containerName); delete config.seatbelt; + delete config.processContainer; config.containment = 'apple_container'; config.experimental = { ...config.experimental, diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index 77ebcd1046..26715d123e 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -992,7 +992,14 @@ describe('createConfigFromPolicy', () => { mockDarwin(); try { const config = createAppleContainerConfig( - { version: '0.9.0-alpha' }, + { + version: '0.9.0-alpha', + processContainer: { + network: { + allowedProxyPeer: 'Contoso.Proxy_1234567890abc', + }, + }, + }, { image: 'docker.io/library/alpine:3.23', cpuCount: 2, @@ -1004,6 +1011,7 @@ describe('createConfigFromPolicy', () => { assert.strictEqual(config.containment, 'apple_container'); assert.strictEqual(config.containerId, 'apple-test'); assert.strictEqual(config.seatbelt, undefined); + assert.strictEqual(config.processContainer, undefined); assert.deepStrictEqual(config.experimental?.apple_container, { image: 'docker.io/library/alpine:3.23', cpuCount: 2, diff --git a/src/core/mxc_config_contract/src/dev/experimental.rs b/src/core/mxc_config_contract/src/dev/experimental.rs index 9cc8eeb4cd..c493881dcb 100644 --- a/src/core/mxc_config_contract/src/dev/experimental.rs +++ b/src/core/mxc_config_contract/src/dev/experimental.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use super::primitives::NonEmptyString; +use super::primitives::NonBlankString; use super::primitives::OptionalField; use std::num::{NonZeroU16, NonZeroU32, NonZeroU64}; @@ -102,7 +102,7 @@ pub struct OneShotWslc { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct OneShotAppleContainer { /// OCI image reference. The image must provide `/bin/sh`. - pub image: NonEmptyString, + pub image: NonBlankString, /// Requested virtual CPU count. #[serde(default)] pub cpu_count: OptionalField, diff --git a/src/core/mxc_config_contract/src/dev/mod.rs b/src/core/mxc_config_contract/src/dev/mod.rs index 9fe2bf0adf..35ffa5779b 100644 --- a/src/core/mxc_config_contract/src/dev/mod.rs +++ b/src/core/mxc_config_contract/src/dev/mod.rs @@ -239,7 +239,7 @@ pub use network::{ NetworkIngress, NetworkPeer, NetworkPort, NetworkProtocol, NetworkProxy, NetworkRule, }; pub use one_shot::{Containment as OneShotContainment, Request as OneShotRequest}; -pub use primitives::{NonEmptyString, OptionalField, True}; +pub use primitives::{NonBlankString, NonEmptyString, OptionalField, True}; pub use request::{parse_request, Request, RequestParseError}; #[cfg(feature = "schema-gen")] pub use schema::development_schema; diff --git a/src/core/mxc_config_contract/src/dev/primitives.rs b/src/core/mxc_config_contract/src/dev/primitives.rs index acacdb8d86..725424b67d 100644 --- a/src/core/mxc_config_contract/src/dev/primitives.rs +++ b/src/core/mxc_config_contract/src/dev/primitives.rs @@ -150,6 +150,62 @@ impl<'de> Deserialize<'de> for NonEmptyString { } } +/// A string containing at least one non-whitespace character. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NonBlankString(String); + +#[cfg(feature = "schema-gen")] +impl schemars::JsonSchema for NonBlankString { + fn schema_name() -> String { + "NonBlankString".to_string() + } + + fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, StringValidation}; + + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), + string: Some(Box::new(StringValidation { + pattern: Some(r"\S".to_string()), + ..Default::default() + })), + ..Default::default() + }) + } +} + +impl NonBlankString { + /// Creates a validated string. + /// + /// Returns an error when `value` contains only whitespace. + pub fn new(value: String) -> Result { + if value.trim().is_empty() { + Err("string must contain a non-whitespace character".to_string()) + } else { + Ok(Self(value)) + } + } + + /// Returns the validated string as a borrowed string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consumes the wrapper and returns the validated string. + pub fn into_inner(self) -> String { + self.0 + } +} + +impl<'de> Deserialize<'de> for NonBlankString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Self::new(String::deserialize(deserializer)?).map_err(de::Error::custom) + } +} + /// A JSON array that must contain at least one element. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NonEmptyVec(Vec); @@ -236,6 +292,18 @@ mod tests { assert_eq!(error, "string must not be empty"); } + #[test] + fn non_blank_string_accepts_surrounding_whitespace() { + let value = NonBlankString::new(" alpine ".to_string()).unwrap(); + assert_eq!(value.as_str(), " alpine "); + } + + #[test] + fn non_blank_string_rejects_whitespace_only() { + let error = NonBlankString::new(" \t\n".to_string()).unwrap_err(); + assert_eq!(error, "string must contain a non-whitespace character"); + } + #[cfg(feature = "schema-gen")] #[test] fn constrained_primitive_schemas_match_deserialization() { @@ -249,6 +317,10 @@ mod tests { let non_empty = serde_json::to_value(NonEmptyString::json_schema(&mut generator)).unwrap(); assert_eq!(non_empty["type"], "string"); assert_eq!(non_empty["minLength"], 1); + + let non_blank = serde_json::to_value(NonBlankString::json_schema(&mut generator)).unwrap(); + assert_eq!(non_blank["type"], "string"); + assert_eq!(non_blank["pattern"], r"\S"); } #[cfg(feature = "schema-gen")] diff --git a/src/core/mxc_config_contract/tests/v0_9_0_alpha/experimental/apple_container.rs b/src/core/mxc_config_contract/tests/v0_9_0_alpha/experimental/apple_container.rs index 2ac2782025..ec9aa8be3e 100644 --- a/src/core/mxc_config_contract/tests/v0_9_0_alpha/experimental/apple_container.rs +++ b/src/core/mxc_config_contract/tests/v0_9_0_alpha/experimental/apple_container.rs @@ -34,6 +34,8 @@ fn rejects_missing_empty_or_non_string_image() { for fields in [ "", r#""image": """#, + r#""image": " ""#, + r#""image": "\t\n""#, r#""image": null"#, r#""image": 123"#, r#""image": true"#, From b2ae67eff58c59763b9aafd9ef9b28216bedb245 Mon Sep 17 00:00:00 2001 From: Richie Gomez Date: Wed, 26 Aug 2026 12:00:36 -0700 Subject: [PATCH 7/7] Align rolling Apple Container schema Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b062729b-d6f3-47f6-9df0-6c1d5c8a1e5c --- schemas/dev/mxc-config.schema.0.9.0-dev.json | 35 +++++++- src/core/wxc_common/src/wire.rs | 89 +++++++++++++++++++- 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/schemas/dev/mxc-config.schema.0.9.0-dev.json b/schemas/dev/mxc-config.schema.0.9.0-dev.json index 0283369211..6d0ef6f8fc 100644 --- a/schemas/dev/mxc-config.schema.0.9.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.9.0-dev.json @@ -4,6 +4,39 @@ "title": "MXC Configuration", "description": "MXC container execution configuration. Defines the recommended config format for both one-shot and state-aware sandbox lifecycle requests. A few deprecated field spellings not listed here are also accepted via serde aliases.", "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "containment": { + "const": "apple_container" + } + }, + "required": [ + "containment" + ] + }, + "then": { + "properties": { + "experimental": { + "allOf": [ + { + "$ref": "#/definitions/Experimental" + }, + { + "required": [ + "apple_container" + ] + } + ] + } + }, + "required": [ + "experimental" + ] + } + } + ], "definitions": { "AppleContainer": { "description": "Apple Container backend config.", @@ -19,7 +52,7 @@ }, "image": { "description": "OCI image reference. The image must provide `/bin/sh`.", - "minLength": 1, + "pattern": "\\S", "type": "string" }, "memoryMb": { diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index 23ec41d994..88b40791c8 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -665,7 +665,10 @@ pub struct Wslc { #[serde(rename_all = "camelCase")] pub struct AppleContainer { /// OCI image reference. The image must provide `/bin/sh`. - #[cfg_attr(feature = "schema-gen", schemars(length(min = 1)))] + #[cfg_attr( + feature = "schema-gen", + schemars(schema_with = "non_blank_string_schema") + )] pub image: String, /// Requested virtual CPU count. #[cfg_attr(feature = "schema-gen", schemars(range(min = 1)))] @@ -675,6 +678,22 @@ pub struct AppleContainer { pub memory_mb: Option, } +#[cfg(feature = "schema-gen")] +fn non_blank_string_schema( + _generator: &mut schemars::gen::SchemaGenerator, +) -> schemars::schema::Schema { + use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, StringValidation}; + + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), + string: Some(Box::new(StringValidation { + pattern: Some(r"\S".to_string()), + ..Default::default() + })), + ..Default::default() + }) +} + /// Per-phase WSLc **provision** configuration (state-aware lifecycle), nested /// under `experimental.wslc.provision`. Carries only what the amortized daemon /// session honors: the container image (or a local tarball to import). @@ -792,9 +811,46 @@ mod schema_gen { let schema = schemars::schema_for!(MxcConfig); let mut value = serde_json::to_value(&schema).expect("schema serialises to JSON value"); mxc_schema_support::prepare_schema(&mut value, SCHEMA_ID); + require_apple_container_settings(&mut value); value } + fn require_apple_container_settings(schema: &mut serde_json::Value) { + let root = schema + .as_object_mut() + .expect("generated schema root is an object"); + let constraints = root + .entry("allOf") + .or_insert_with(|| serde_json::Value::Array(Vec::new())) + .as_array_mut() + .expect("root allOf constraints"); + constraints.push(serde_json::json!({ + "if": { + "properties": { + "containment": { + "const": "apple_container" + } + }, + "required": ["containment"] + }, + "then": { + "properties": { + "experimental": { + "allOf": [ + { + "$ref": "#/definitions/Experimental" + }, + { + "required": ["apple_container"] + } + ] + } + }, + "required": ["experimental"] + } + })); + } + /// Emit the SDK's wire TypeScript types directly from the same generated schema /// model — no third-party generator. The output is a drift oracle /// (`sdk/node/src/generated/wire.ts`): the SDK's hand-written public types are @@ -806,6 +862,37 @@ mod schema_gen { let value = schema_value(); mxc_schema_support::emit_ts(&value) } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn apple_container_schema_matches_runtime_requirements() { + let schema = schema_value(); + + assert_eq!( + schema["definitions"]["AppleContainer"]["properties"]["image"]["pattern"], + r"\S" + ); + + let constraints = schema["allOf"].as_array().expect("root allOf constraints"); + let apple_container = constraints + .iter() + .find(|constraint| { + constraint["if"]["properties"]["containment"]["const"] == "apple_container" + }) + .expect("Apple Container constraint"); + assert_eq!( + apple_container["then"]["required"], + serde_json::json!(["experimental"]) + ); + assert_eq!( + apple_container["then"]["properties"]["experimental"]["allOf"][1]["required"], + serde_json::json!(["apple_container"]) + ); + } + } } #[cfg(feature = "schema-gen")]