Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ The SDK auto-discovers native binaries by checking `sdk/node/bin/<target-triple>
- **Dev schema**: the in-progress schema lives in [`schemas/dev/`](../schemas/dev). It is **generated** from the Rust wire model (`src/core/wxc_common/src/wire.rs`) by the `mxc_schema_gen` tool β€” **do not hand-edit it**. To change the dev schema, edit the wire model and regenerate with `cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- schemas/dev/mxc-config.schema.<dev>.json`. `scripts/versioning/check-schema-codegen.js` is a CI gate that regenerates and fails if the committed schema drifts. See [`docs/schema-codegen.md`](../docs/schema-codegen.md).
- **Generated SDK wire types**: `sdk/node/src/generated/wire.ts` is **generated** from the same wire model by the `mxc_schema_gen --ts` TypeScript emitter (`wxc_common::ts_emit`, no third-party generator) β€” **do not hand-edit it**. It is a drift oracle (not public API); the SDK unit test `sdk/node/tests/unit/wire-conformance.test.ts` asserts the hand-written public types in `sdk/node/src/types.ts` conform to it, and `scripts/versioning/check-sdk-types-codegen.js` is a CI gate that fails if the committed file drifts. Regenerate with `cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- --ts sdk/node/src/generated/wire.ts`.
- **Canonical schema-version source**: `schemas/schema-version.json` β€” the single source of truth for the schema-version constants (min/maxSupported/state-aware/stable/dev). `scripts/versioning/check-schema-versions.js` enforces that the Rust parser, SDK, and schema filenames all agree with it; do not hand-edit a schema-version constant without updating the canonical file. See [`docs/versioning.md`](../docs/versioning.md) for the full design.
- **Dev schema compatibility**: `scripts/versioning/check-dev-schema-compat.js` is a CI gate that compares the dev schema at the pull-request base against the dev schema at HEAD and **fails on any structural restriction** β€” a removed property, a new `required`, a narrowed `type`, a tightened bound. There is no per-field escape hatch. Because one dev schema validates configs declaring every supported version, surface a supported version can use has to stay in it. Make a breaking change **additively**: keep the old fields, add the new shape alongside them, and let the supported-version window govern which may be used. Deleting is legitimate only once `min` in `schemas/schema-version.json` rises past the surface being dropped.
- **Version availability**: a wire field may declare the schema-version range it is valid in with `#[mxc_version(since = "0.8")]` / `#[mxc_version(until = "0.7")]` on the `wire.rs` field (see `src/core/mxc_version_derive/`). One declaration feeds both the parser (which enforces it right after deserialisation, before backend dispatch) and schema generation (which publishes `x-mxc-since` / `x-mxc-until`). `version` is **required** in every config. Annotation is opt-in β€” an unannotated field is valid across the whole supported range β€” and adding one is a behavioural change, so it is checked against the frozen schemas by `scripts/versioning/check-version-availability.js`. Put a range on the **containing field**, never inside a struct shared with `experimental` (e.g. `Seatbelt`), and never under `experimental` itself (that block was open before 0.8, so schema presence understates what has always been accepted). See [`docs/versioning.md`](../docs/versioning.md#version-availability).
- **Dev schema compatibility**: `scripts/versioning/check-dev-schema-compat.js` is a CI gate that compares the dev schema at the pull-request base against the dev schema at HEAD and **fails on any structural restriction** β€” a removed property, a new `required`, a narrowed `type`, a tightened bound. There is no per-field escape hatch. Because one dev schema validates configs declaring every supported version, surface a supported version can use has to stay in it. Make a breaking change **additively**: keep the old fields, add the new shape alongside them, and let the supported-availability range govern which may be used. Deleting is legitimate only once `min` in `schemas/schema-version.json` rises past the surface being dropped.
- Config files can reference schemas via `"$schema"` for editor validation. `scripts/versioning/validate-configs.js` validates the `tests/examples` + `tests/configs` corpus against the dev schema in CI.

### Key documentation (`docs/`)
Expand Down Expand Up @@ -192,7 +193,7 @@ The workspace is organized into six top-level directories under `src/`:

| Directory | Purpose | Examples |
|-----------|---------|----------|
| `core/` | Cross-platform foundation + per-platform aggregator binaries | `wxc_common/`, `wxc/`, `lxc/`, `mxc_darwin/`, `mxc_engine/`, `mxc-sdk/`, `mxc_pty/`, `mxc_build_common/`, `learning_mode_core/`, `generated/` |
| `core/` | Cross-platform foundation + per-platform aggregator binaries | `wxc_common/`, `wxc/`, `lxc/`, `mxc_darwin/`, `mxc_engine/`, `mxc-sdk/`, `mxc_pty/`, `mxc_build_common/`, `mxc_version_derive/`, `learning_mode_core/`, `generated/` |
| `backends/` | Backend-specific code (one subfolder per containment backend or backend support component) | `appcontainer/common`, `windows_sandbox/{daemon,guest,common,lifecycle}`, `isolation_session/{bindings,common}`, `learning_mode/windows`, `hyperlight/common`, `nanvix/{common,build_common,binaries,runner}`, `lxc/common`, `bubblewrap/common`, `wslc/common`, `seatbelt/common` |
| `ffi/` | Foreign-function-interface crates (C ABI for language bindings) | `mxc_ffi/` |
| `host/` | Host-side utilities | `wxc_host_prep/`, `wxc_winhttp_proxy_shim/` |
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/Versioning.Checks.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ jobs:
- name: Check SDK wire types are in sync with the Rust wire model (codegen)
run: node scripts/versioning/check-sdk-types-codegen.js

# After the codegen gates, which guarantee the committed schema still
# matches the wire model the ranges are declared in, and before the
# compatibility gate: a wrong `since` is a correctness bug in its own
# right, independent of whether the diff is breaking.
- name: Check availability ranges against the frozen schemas (oracle)
run: node scripts/versioning/check-version-availability.js

# Ahead of corpus validation: a pull request that removes a field also
# migrates the corpus, so validation passes and the removal is what needs
# reporting.
Expand Down
49 changes: 45 additions & 4 deletions docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,14 @@ Full lifecycle API: [`docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md`

### Schema Versioning

MXC config files include an optional `version` field using
MXC config files declare a `version` field using
[Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PATCH). The parser uses
this to detect incompatible configs and provide clear upgrade guidance. If
`version` is absent, the config is assumed compatible with the current version.
this to detect incompatible configs, to decide which fields are legal (see
[availability ranges](#version-availability)), and to provide clear upgrade guidance.

`version` is **required**. It is not merely metadata: it selects the accepted
field surface, so a config without one would silently opt out of every range
rather than defaulting to something safe.

Versions with a pre-release suffix (e.g., `-alpha`) indicate the schema is not
yet stable β€” breaking changes may occur in any release. Once the schema is
Expand All @@ -247,14 +251,51 @@ The parser compares the config's major.minor against its supported version

| Config `version` | Parser supports | Result |
|---|---|---|
| absent | >=0.6, <=0.8 | Accepted (assumed compatible) |
| absent or `""` | >=0.6, <=0.8 | **Rejected** β€” `version_incompatible`, "Missing required field: version" |
| `"0.5.0-alpha"` | >=0.6, <=0.8 | **Rejected** β€” "older than supported" |
| `"0.6.0-alpha"` | >=0.6, <=0.8 | Accepted (0.6 in range) |
| `"0.7.0-alpha"` | >=0.6, <=0.8 | Accepted (0.7 in range) |
| `"0.8.0-alpha"` | >=0.6, <=0.8 | Accepted (0.8 in range) |
| `"0.9.0"` | >=0.6, <=0.8 | **Rejected** β€” "newer than supported" |
| `"1.0.0"` | >=0.6, <=0.8 | **Rejected** β€” "newer than supported" |

#### Version availability

A field may declare the range of schema versions it is valid in. The generated
schema publishes these as `x-mxc-since` / `x-mxc-until` on the property:

```jsonc
"seatbelt": {
"anyOf": [{ "$ref": "#/definitions/Seatbelt" }, { "type": "null" }],
"x-mxc-since": "0.7" // rejected in a config declaring 0.6
}
```

Both bounds are **inclusive** and compare `major.minor` only. Using a field
outside its range is rejected at parse time with `version_incompatible` and
structured `details`:

```json
{
"error": {
"code": "version_incompatible",
"message": "Config field 'seatbelt' was introduced in schema version 0.7 but the config declares '0.6.0-alpha'. Raise the config's 'version' to 0.7 or newer, or remove the field.",
"details": {
"field": "seatbelt",
"declaredVersion": "0.6.0-alpha",
"since": "0.7",
"until": null
}
}
}
```

Annotation is **opt-in**: an unannotated field is valid across the whole
supported range. Annotating one is a deliberate behavioural change β€” it starts
rejecting configs that were previously accepted β€” and is checked against history
by a CI gate. See [Version availability](versioning.md#version-availability) in the
versioning design for how to add one.

#### When to bump

| Change type | Version bump | Example |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ structured `details`. Reference Β§8 has the full list and the `MxcError` mapping
| Id problems | `malformed_id`, `stale_id` |
| State-machine violations | `not_provisioned`, `not_started`, `already_started`, `already_stopped` |
| Config / policy | `policy_validation` |
| Schema version | `version_incompatible` (unsupported version, or a field used outside its availability range) |
| Catch-all | `backend_error` (with structured `details`) |

Process-runtime kill conditions (timeouts, backend-initiated termination) surface as
Expand Down
23 changes: 17 additions & 6 deletions docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,11 +422,20 @@ Phases with no backend-specific or cross-cutting fields declare a Config carryin
change: extend `StateAwareContainmentBackend`, define five new `*Config` interfaces, and
add an arm to `ConfigsForBackend`.

Each Config carries an optional `version?: string`. When omitted, the SDK fills in its
own `SUPPORTED_VERSION`; an explicit value is range-validated against the SDK's
`MIN_VERSION` and `SUPPORTED_VERSION` (same convention as today's
`validatePolicyVersion`). The override exists so consumers can target a specific wire
version when debugging or testing version negotiation.
Each Config carries an optional `version?: string`. It is optional **to the SDK
caller only**: the wire format *requires* `version` (it selects which config fields are
legal β€” see [versioning](../versioning.md#version-availability)), and the SDK always emits
one, filling in `STATE_AWARE_VERSION` when the caller omits it. An explicit value is
range-validated against the SDK's `MIN_VERSION` and `SUPPORTED_VERSION` (same convention
as today's `validatePolicyVersion`). The override exists so consumers can target a
specific wire version when debugging or testing version negotiation.

A request that reaches the executor without a `version` is rejected with
`version_incompatible`. Note that the generated JSON Schema does **not** list `version`
under `required`: the schema is an editor/CI convenience, not the trust boundary (the
parser is), and adding a `required` entry would be a structural restriction that the
dev-schema compatibility gate correctly refuses. Cross-field and presence invariants
live in the parser for exactly this reason.

### 6.2 Method signatures

Expand Down Expand Up @@ -991,6 +1000,7 @@ other state-aware backend, so caller error-handling code is portable across back
| `already_stopped` | `stop` called on an already-stopped sandbox |
| `policy_validation` | Per-stage config or cross-cutting policy contents do not satisfy the backend's expected shape or values |
| `backend_error` | Catch-all for backend-specific failures; `details` carries structured information |
| `version_incompatible` | The config omitted `version`, declared one outside the supported range, or used a field outside its availability range. `details` carries `{ field, declaredVersion, since, until }`, where `field` is the offending field's dotted path or `"version"` for a range failure. See [versioning](../versioning.md#version-availability) |

```typescript
type ErrorCode =
Expand All @@ -1005,7 +1015,8 @@ type ErrorCode =
| 'already_started'
| 'already_stopped'
| 'policy_validation'
| 'backend_error';
| 'backend_error'
| 'version_incompatible';
```

The set is closed at the MXC layer. Backend-specific failures that don't fit one of the
Expand Down
Loading
Loading