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
7 changes: 7 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ logic. Backends with additional processes use several crates:
Shared parsing and normalization live in `wxc_common`; backend-specific policy
validation and enforcement live with each backend.

`mxc_engine/src/backend_registry.rs` owns backend registration metadata,
including experimental classification, keyed by the shared `ContainmentBackend`
enum. Runtime authorization consults that registry. Exact-contract publication,
build-feature availability, and host-capability probing remain separate; the
registry neither dispatches workloads nor adds backend dependencies to
`wxc_common`.

## Request flow

```mermaid
Expand Down
6 changes: 3 additions & 3 deletions docs/isolation-session/state-aware-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ phase contracts after taking those routing values from CLI arguments.

| Phase | `wxc-exec` | In-process |
|---|---|---|
| provision / start / stop / deprovision | `wxc-exec --operation <phase> [--sandbox-id <id>] --config …` | `mxc_sdk::run_state_aware_json`, `mxc_state_aware` |
| exec, attached to the caller's stdio | `wxc-exec --operation exec --sandbox-id <id> --config …` | `mxc_sdk::exec_attached`, `mxc_state_aware_exec_attached` |
| exec, caller drives the pipes | *(no CLI equivalent)* | `mxc_sdk::exec_sandbox`, `mxc_state_aware_exec` |
| provision / start / stop / deprovision | `wxc-exec --operation <phase> [--sandbox-id <id>] --config …` | `mxc_sdk::run_state_aware_json`, `mxc_run_state_aware_json` |
| exec, attached to the caller's stdio | `wxc-exec --operation exec --sandbox-id <id> --config …` | `mxc_sdk::exec_attached`, `mxc_exec_state_aware_attached_json` |
| exec, caller drives the pipes | *(no CLI equivalent)* | `mxc_sdk::exec_sandbox`, `mxc_exec_state_aware_json` |

Requirements on an in-process caller:

Expand Down
4 changes: 2 additions & 2 deletions docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ unrecognised prefix, and this is by design:

| Source | Behaviour for an unrecognised `sandboxId` prefix |
| ----------------------- | ------------------------------------------------ |
| SDK (TypeScript) | Throws `MxcError { code: 'malformed_id' }` before invoking `mxc_state_aware` or `mxc_state_aware_exec`. The SDK matches the prefix against the closed `StateAwareContainmentBackend` union it was compiled with; an unknown prefix is treated as a malformed id. See `sdk/node/src/state-aware-helper.ts`. |
| SDK (TypeScript) | Throws `MxcError { code: 'malformed_id' }` before invoking `mxc_run_state_aware_json` or `mxc_exec_state_aware_json`. The SDK matches the prefix against the closed `StateAwareContainmentBackend` union it was compiled with; an unknown prefix is treated as a malformed id. See `sdk/node/src/state-aware-helper.ts`. |
| SDK (Rust) | `SandboxId::parse` accepts a syntactically valid opaque id without interpreting its prefix. Dispatch returns `MxcError { code: 'unsupported_containment' }` when that prefix is not registered. Empty ids, ids without prefix structure, and ids containing NUL are `malformed_id`. |
| Native FFI entry points | Return `MxcError { code: 'unsupported_containment' }`. The Rust dispatcher parses the prefix successfully but the prefix-to-backend lookup table has no entry for it. See `src/core/wxc_common/src/state_aware_dispatch.rs`. |

Expand Down Expand Up @@ -1054,7 +1054,7 @@ other state-aware backend, so caller error-handling code is portable across back
| `malformed_request` | Structural request error: malformed JSON, missing required field, unknown or phase-inappropriate field, recursively unknown backend-specific field, or invalid phase-specific shape |
| `unsupported_containment` | The backend named by `containment` (provision) or implied by a syntactically valid `sandboxId` prefix (non-provision) is not recognised in this build. The TypeScript SDK checks its closed prefix union before dispatch and instead throws `malformed_id`; the typed Rust SDK keeps ids opaque and therefore returns `unsupported_containment` from dispatch, matching raw FFI requests. See §6.4. |
| `unsupported_phase` | The backend does not support the requested call mode (state-aware call against an ephemeral-only backend, or one-shot call against a state-aware-only backend) |
| `backend_unavailable` | The backend's runtime dependency is missing or unreachable (service not running, daemon stopped) |
| `backend_unavailable` | The backend's runtime dependency is missing or unreachable (service not running, daemon stopped), or the backend is experimental and the caller did not enable experimental features |
| `malformed_id` | The `sandboxId` is structurally invalid or has a recognised prefix but does not deserialize into the backend's native form. The TypeScript SDK also uses this code for a prefix outside its closed `StateAwareContainmentBackend` union; typed Rust and raw FFI calls classify a syntactically valid unknown prefix as `unsupported_containment`. |
| `stale_id` | The `sandboxId` deserialised but refers to a resource the backend no longer recognises |
| `not_provisioned` | Phase requires a provisioned sandbox; none provided, or the id is in a pre-provision state |
Expand Down
1 change: 1 addition & 0 deletions docs/telemetry/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ Excluded, and why:
| `telemetry`, internal `test` feature | No enforcement effect. |
| proxy `original_url` | Can embed `user:password@`. The host and port *are* hashed. |
| `dry_run`, `testing_features_enabled` | Invocation modes, not policy. |
| `experimental_enabled` | Authorizes selecting an experimental backend, not enforcement; changing it leaves policy identity unchanged. |

`network_enforcement_compatibility` is included because it changes how the
normalized network policy is interpreted and enforced.
Expand Down
118 changes: 99 additions & 19 deletions docs/versioning.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
# MXC Versioning Design

## Architecture at a glance

Versioning determines **which configuration contract is accepted**, not which
backend implementation runs or what the host can enforce. Keep these decisions
separate:

| Decision | Authority |
| --- | --- |
| Which fields and values exist? | The exact Rust contract types and registration in `mxc_config_contract`. Published contracts are immutable; the development contract can evolve. |
| Which exact contract does a typed SDK emit? | `sdkMajorTargets` in `schemas/schema-version.json`, checked against the exact Rust registry. Callers select a major-version SDK API, not an exact JSON version. |
| Which exact contract does raw JSON use? | The caller's declared, registered `version`. A version range or development opt-in cannot authorize another spelling. |
| Which backend and policy can run? | `mxc_engine` resolves the backend and checks authorization; host capabilities and backend validation determine what can actually be enforced. |

Typed authoring and raw JSON converge before backend execution:

```mermaid
flowchart LR
typed["Typed SDK policy or lifecycle request"]
target["SDK-owned published exact target"]
raw["Raw JSON with caller-declared version"]
contract["Registered exact Rust contract"]
adapter["Version-specific adapter"]
normalized["Shared normalization<br/>ExecutionRequest and typed lifecycle operation"]
engine["Engine routing and authorization"]
backend["Host-capability validation and enforcement"]

typed --> target --> contract
raw --> contract
contract --> adapter --> normalized --> engine --> backend
```

Rust builders construct exact contract values in memory. Node and .NET exact
writers serialize those values to JSON for the native boundary. In either
case, version-specific adapters and shared normalization own the conversion
to runtime requests; SDK policy types and generated schemas do not form
another native configuration authority. The
[native-ingress section](#native-ingress) identifies the deprecated binding
exceptions that remain while the migration is staged.

Publication, SDK targeting, and runtime authorization are independent:
opening `1.1.0-alpha` does not change V1's published target, and accepting
that exact development contract does not grant experimental backend access.
Conversely, experimental authorization does not make a field legal in an
older contract or override backend policy validation.

The sections below distinguish the [three version axes](#the-three-version-axes),
[contract shipping and parsing](#schema-shipping-model),
[SDK major targets](#high-level-sdk-major-targets), and
[backend authorization](#experimental-flag). Artifact regeneration belongs in
[Schema Code Generation](schema-codegen.md); backend execution flow is covered
by [Architecture](architecture.md).

## Core Concepts

### Policy = Intent
Expand Down Expand Up @@ -115,9 +167,10 @@ parser simply stops accepting those versions (the supported floor is
`0.6.0-alpha`). Released schemas are never edited or deleted.

The development artifact is generated from the exact
`mxc_config_contract::dev` model. It describes all eight closed one-shot and
state-aware roots, including recursively closed experimental structures, and
is the authoritative contract for declared `1.1.0-alpha` requests.
`mxc_config_contract::dev` model. It describes all eight closed one-shot and
state-aware roots, including recursively closed experimental structures. The
registered Rust types remain the authority for declared `1.1.0-alpha`
requests; the schema is their derived editor and validation artifact.

Raw JSON is parsed with the exact registered contract named by its `version`
field. High-level Rust, .NET, and Node v1 builders do not accept a caller-supplied
Expand Down Expand Up @@ -217,8 +270,10 @@ accepted by v1.0.

Schemas in `stable/` are immutable: they document the input shape that was
promised at release. They are **not** authoritative for runtime security
defaults. `wxc-exec` is the trust boundary and may apply stricter defaults
than a stable schema declares when a security issue requires it.
defaults. Native contract parsing and backend validation form the trust
boundary for both executor and library callers. Runtime enforcement may apply
stricter defaults than a stable schema declares when a security issue requires
it.

For example, an older stable schema may declare
`network.defaultPolicy` defaulting to `"allow"`. The runtime may treat an
Expand All @@ -234,17 +289,20 @@ Development features use their intended permanent top-level locations in the
mutable exact contract. JSON location, publication eligibility, and runtime
authorization are separate concerns. This gives editors full autocomplete and
validation without requiring a later field move when a feature graduates.
Today, the `--experimental` flag is a global runtime toggle that enables all
features which still require authorization; per-feature gating is under
consideration.
The engine-owned backend registry in
`src/core/mxc_engine/src/backend_registry.rs` records which backend selections
require runtime experimental authorization. Contract publication does not
implicitly change that classification. The flag does not enable otherwise
invalid fields or bypass backend enforcement.

**Rules:**
- **Published contract contents** — shipped, stable, and immutable.
- **Development contract contents** — mutable fields and roots at their
permanent locations. Inclusion does not imply runtime authorization.
- **Promotion:** When a feature is ready to ship, include it in the published
exact contract and remove its runtime experimental gate. Its JSON location
does not change.
- **Promotion:** Publish the feature in an exact stable contract without
changing its JSON location. Update backend experimental classification
separately when that backend is ready for production; publishing a field
alone does not remove a backend's authorization requirement.

### Published-contract history

Expand Down Expand Up @@ -293,6 +351,26 @@ baselines are captured when the v1.0 SDK surface is established rather than
through empty placeholder
descriptors.

### Native ingress

The exact JSON execution surface uses `mxc_run_json`, `mxc_spawn_json`, and
the state-aware JSON exports. Typed binding writers select the SDK-owned
contract; raw APIs preserve the caller's exact document. These exports use
the registered contract parser and take non-configuration controls, including
experimental authorization, as typed FFI arguments rather than JSON fields.
The [SDK conformance fixtures](../tests/policy/README.md#sdk-v1-conformance-fixtures)
pair high-level invocations with independently hand-authored expected exact
documents to check mapping intent across SDKs.

**Migration status:** Node/.NET one-shot execution and the .NET request probe
still use deprecated private binding ingress, including its legacy JSON
experimental switch. [Node migration](https://github.com/microsoft/mxc/pull/1350)
and [.NET migration](https://github.com/microsoft/mxc/pull/1351) move those
callers to exact JSON; [cleanup](https://github.com/microsoft/mxc/pull/1352)
then removes the private parser/exports and remaining SDK serde support.
Their detailed rollout is tracked in those migration changes, not by a
second configuration contract in this versioning design.

### Experimental Flag

The experimental flag must be supported at every layer of the stack:
Expand All @@ -305,14 +383,16 @@ lxc-exec config.json --experimental
wxc-exec.exe --experimental config.json
```

The parser **always** parses fields defined by the selected exact contract
regardless of the flag; parsing is flag-independent. The `--experimental` flag only sets
`request.experimental_enabled`:
- When set, the runners apply the parsed experimental features alongside the
stable features
- When unset, `experimental_enabled` is false and the runners **ignore** the
parsed features that still require authorization — no error, those features
are just not applied
The parser **always** parses fields defined by the selected exact contract;
parsing is flag-independent. The flag authorizes selecting an experimental
backend (MicroVM, Hyperlight, or Windows Sandbox). Without it, native refuses
the request with
`backend_unavailable` on every one-shot and state-aware entry point. The flag
is ignored for production backends, including production-backend fields in a
development contract; unsupported policy still fails closed rather than being
silently ignored. Contract version and backend authorization are separate.
The authorization switch is excluded from policy identity because it does not
change the selected backend's enforcement.

**2. SDK:** policy APIs come from `@microsoft/mxc-sdk/v1`; raw config
spawning comes from `@microsoft/mxc-sdk`.
Expand Down
13 changes: 10 additions & 3 deletions scripts/check-dotnet-bindings-codegen.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function listFiles(directory) {
// entry point. The generated file is excluded because it is the output under
// test.
const managedSource = join(repoRoot, "sdk", "dotnet", "Microsoft.Mxc.Sdk");
const REQUIRED_ENTRY_POINTS = [
const managedEntryPoints = [
...new Set(
listFiles(managedSource)
.filter((path) => path.endsWith(".cs") && path !== generated)
Expand All @@ -52,10 +52,15 @@ const REQUIRED_ENTRY_POINTS = [
.map((match) => match[1])
),
].sort();
if (REQUIRED_ENTRY_POINTS.length === 0) {
if (managedEntryPoints.length === 0) {
console.error("ERROR: found no NativeMethods.mxc_* call sites in the C# SDK");
process.exit(1);
}
// Exported entry points that no managed call site consumes yet.
const ABI_ONLY_ENTRY_POINTS = ["mxc_run_json", "mxc_spawn_json"];
const REQUIRED_ENTRY_POINTS = [
...new Set([...managedEntryPoints, ...ABI_ONLY_ENTRY_POINTS]),
].sort();

// Remove any stale copy so we prove codegen actually (re)produces it.
if (existsSync(generated)) {
Expand Down Expand Up @@ -96,6 +101,8 @@ if (missing.length > 0) {
const requiredSignatures = [
"mxc_run_request(byte* request_json_utf8, MxcRunResult* @out)",
"mxc_spawn_request(byte* request_json_utf8, MxcSandbox** out_handle, MxcErrorDetail* out_error)",
"mxc_run_json(byte* request_json_utf8, int experimental, MxcRunResult* @out)",
"mxc_spawn_json(byte* request_json_utf8, int experimental, MxcSandbox** out_handle, MxcErrorDetail* out_error)",
];
const missingSignatures = requiredSignatures.filter(
(signature) => !content.includes(signature)
Expand Down Expand Up @@ -164,6 +171,6 @@ if (notDeclared.length > 0) {
}

console.log(
`C# bindings codegen OK: generated every one of ${REQUIRED_ENTRY_POINTS.length} managed entry points; ` +
`C# bindings codegen OK: generated all ${REQUIRED_ENTRY_POINTS.length} required entry points; ` +
`${csbindgenInputs.length} csbindgen source(s) all declared as rerun-if-changed`
);
8 changes: 7 additions & 1 deletion scripts/versioning/validate-configs.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ for (const required of [schemaVer.min, schemaVer.maxSupported]) {
}

// Directories whose *.json files (recursively) are configs we expect to validate.
const CONFIG_DIRS = [join("tests", "examples"), join("tests", "configs")];
// `tests/policy/sdk-v1/expected` holds the exact documents every SDK must emit
// for the shared v1 policy goldens.
const CONFIG_DIRS = [
join("tests", "examples"),
join("tests", "configs"),
join("tests", "policy", "sdk-v1", "expected"),
];

// Files that are intentionally invalid (negative tests) and must NOT validate.
const exemptionsPath = join(repoRoot, "scripts", "versioning", "config-validation-exemptions.json");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public void ExecInSandbox_PassesTheExperimentalOptIn()
[Fact]
public void ExecInSandboxAttached_WithoutATerminal_ThrowsMalformedRequest()
{
// Crosses mxc_state_aware_exec_attached itself, which the envelope tests
// Crosses mxc_exec_state_aware_attached_json itself, which the envelope tests
// cannot: it is a separate entry point. That gate short-circuits ahead of
// backend dispatch, which is also why this test cannot pin the
// experimental opt-in.
Expand Down
6 changes: 3 additions & 3 deletions sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcLifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public static MxcSandboxProcess ExecInSandbox(
{
NativeSandbox* handle = null;
MxcErrorDetail error = default;
var status = NativeMethods.mxc_state_aware_exec(
var status = NativeMethods.mxc_exec_state_aware_json(
requestPtr, ExperimentalOptInFor(id), &handle, &error);
if (status != (int)ErrorCode.Success)
{
Expand Down Expand Up @@ -196,7 +196,7 @@ public static SandboxWaitResult ExecInSandboxAttached(
{
MxcExecOutcome outcome = default;
MxcErrorDetail error = default;
var status = NativeMethods.mxc_state_aware_exec_attached(
var status = NativeMethods.mxc_exec_state_aware_attached_json(
requestPtr, ExperimentalOptInFor(id), &outcome, &error);
if (status != (int)ErrorCode.Success)
{
Expand Down Expand Up @@ -618,7 +618,7 @@ private static void SetBackendConfig(
fixed (byte* requestPtr = requestBuf)
{
MxcStateAwareResult result = default;
var status = NativeMethods.mxc_state_aware(
var status = NativeMethods.mxc_run_state_aware_json(
requestPtr,
dryRun ? 1 : 0,
ExperimentalOptInFor(envelope),
Expand Down
2 changes: 1 addition & 1 deletion sdk/node/src/bindings/state-aware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function bindStateAwareNativeFacade(
native: ReturnType<typeof loadMxcFfi>,
): StateAwareNativeFacade {
const run = bindNativeFunction<StateAwareFunction>(native.handle, {
symbol: 'mxc_state_aware',
symbol: 'mxc_run_state_aware_json',
result: 'int32_t',
parameters: [
'const char *',
Expand Down
2 changes: 1 addition & 1 deletion sdk/node/src/bindings/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ function bindStreamingNativeFacade(
},

stateAwareExec: bindNativeFunction(handle, {
symbol: 'mxc_state_aware_exec',
symbol: 'mxc_exec_state_aware_json',
result: 'int32_t',
parameters: [
'const char *',
Expand Down
2 changes: 1 addition & 1 deletion sdk/node/tests/unit/state-aware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ describe('provisionSandbox', () => {
});
});

it('throws an MxcError carrying backend_unavailable when mxc_state_aware reports it', async () => {
it('throws an MxcError carrying backend_unavailable when mxc_run_state_aware_json reports it', async () => {
installStateAwareError({
code: 'backend_unavailable',
message: 'isolation session API not available on this host',
Expand Down
Loading
Loading