Skip to content

Enforce per-field schema version availability at parse time - #783

Open
Gudge (MGudgin) wants to merge 1 commit into
user/gudge/versioning_phase7d_dev_schema_gatefrom
user/gudge/versioning_phase8a_version_windows
Open

Enforce per-field schema version availability at parse time#783
Gudge (MGudgin) wants to merge 1 commit into
user/gudge/versioning_phase7d_dev_schema_gatefrom
user/gudge/versioning_phase8a_version_windows

Conversation

@MGudgin

@MGudgin Gudge (MGudgin) commented Aug 8, 2026

Copy link
Copy Markdown
Member

This PR adds per-field schema version availability: a wire field can declare the range of config schema versions it is valid in, and the parser rejects any use outside that range. It is what makes shape-only support for older schema versions real — until now such an annotation would have been documentation that nothing honoured.

Replaces #775 (closed), rebased onto the current main tip with three compile breaks fixed.

Details

  • New mxc_version_derive proc-macro crate. #[derive(VersionAvailability)] lifts #[mxc_version(since = "0.8")] / until off wire.rs into metadata normal builds carry, so one declaration feeds both the parser and schema generation (published as x-mxc-since / x-mxc-until). A derive rather than #[schemars(extend(...))], which sits behind the schema-gen feature and so can never be consulted by the parser.
  • The mechanism would fail open if a derived JSON name ever disagreed with what serde accepts — the range would simply never fire — so that case is guarded twice: the macro compile-errors on every serde construct it cannot model exactly (flatten, split rename/rename_all, unknown rename_all rules, data-carrying variants), and a conformance test cross-checks all 32 wire types against the names schemars derives independently.
  • The gate runs immediately after deserialisation in every entry point, because convert_wire_config moves fields out of the config. State-aware requests are gated on the original document, not the experimental-masked copy.
  • version is now required — it selects the legal field surface, so an absent one would silently opt out of every range.
  • New version_incompatible code across all five surfaces (Rust MxcErrorCode, engine ErrorCode, TS, C#, MXC_STATUS_VERSION_INCOMPATIBLE = 13) carrying details: { field, declaredVersion, since, until }. The supported-range error migrates onto it. This changes an existing error's observable shape — a consumer string-matching the old range message is affected.
  • Only three fields are annotated (seatbelt since 0.7, processContainer.captureDenials / learningMode since 0.8). What is deliberately not annotated matters more: schema-first-appearance is only a lower bound on accepted surface, so experimental (an open block before 0.8) and the state-aware discriminators carry no range — annotating them from schema data would reject configs that have always worked. The new check-version-availability.js oracle gate is fail-closed on exactly those surfaces.
  • Rebase fixes: main's feat(sdk): add captureDenials to the typed SandboxPolicy #748 added capture_denials to the typed SandboxPolicy, which breaks SandboxPolicy literals that git auto-merges cleanly, so they surface only at compile time. src/core/mxc-sdk/tests/sandbox.rs and two #[cfg(target_os = "macos")] tests in mxc_engine/src/policy.rs did not compile on Enforce per-field schema version availability at parse time #775's head; the macOS pair is invisible to a Windows cargo check.

Tests

  • cargo test --workspace2031 passed, 0 failed. cargo fmt --all -- --check, cargo check --workspace --all-targets and cargo clippy --workspace --all-targets -- -D warnings all clean.
  • cargo check --target aarch64-apple-darwin -p mxc_engine -p wxc_common -p mxc-sdk --all-targets clean — this is what caught the two macOS test breaks that a green Windows run had hidden.
  • Feature-gated builds clean: wxc_common {schema-gen, microvm}, mxc_ffi {dotnetsdk}, mxc_engine {isolation_session}, wxc {isolation_session, microvm, tier2_bfs, wslc, hyperlight}.
  • Versioning gates: 125 pass / 0 fail. check-version-availability (3 declared ranges agree with the 0.6 / 0.7 / 0.8 schemas), validate-configs (201 configs), check-schema-versions, check-schema-codegen, check-sdk-types-codegen, and check-dev-schema-compat against phase 7d all green.
  • Node SDK build + 223 pass / 6 skipped; C# SDK 35 pass; ErrorCode parity 17 codes; C# bindings codegen OK; version sync OK.
  • Non-regression: the Bring network wire schema to full GA spec (wire.rs + config fixtures only) #676 replay still yields exactly 6 findings naming allowLocalNetwork, allowedHosts, blockedHosts, defaultPolicy, enforcementMode, proxy. SUPPORTED_VERSION unchanged at >=0.6, <=0.8.
  • Not executed on this host (Windows): the macOS Seatbelt runtime paths, the Windows Sandbox and IsolationSession PowerShell lifecycle suites, and the host-gated MicroVM / Hyperlight E2E configs. The macOS code cross-compiles clean but has not been run.

Stacked on #732 (phase 7d). Replaces #775.

Co-authored-by: Branden Bonaby bbonaby@microsoft.com
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

This PR adds per-field schema version availability: a wire field can declare the
range of config schema versions it is valid in, and the parser rejects any use
outside that range. It is what makes shape-only support for older schema
versions real — until now such an annotation would have been documentation that
nothing honoured.

Details

* New `mxc_version_derive` proc-macro crate. `#[derive(VersionAvailability)]`
  lifts `#[mxc_version(since = "0.8")]` / `until` off `wire.rs` into metadata
  normal builds carry, so one declaration feeds both the parser and schema
  generation (published as `x-mxc-since` / `x-mxc-until`). A derive rather than
  `#[schemars(extend(...))]`, which sits behind the `schema-gen` feature and so
  can never be consulted by the parser.
* The mechanism fails **open** if a derived JSON name ever disagrees with what
  serde accepts — the range simply never fires — so that case is guarded twice:
  the macro compile-errors on every serde construct it cannot model exactly
  (`flatten`, split `rename`/`rename_all`, unknown `rename_all` rules,
  data-carrying variants), and a conformance test cross-checks all 32 wire types
  against the names `schemars` independently derives.
* The gate runs immediately after deserialisation in every entry point, because
  `convert_wire_config` moves fields out of the config; state-aware requests are
  gated on the original document, not the experimental-masked copy.
* `version` is now **required** — it selects the legal field surface, so an
  absent one would silently opt out of every range.
* New `version_incompatible` code across all five surfaces (Rust `MxcErrorCode`,
  engine `ErrorCode`, TS, C#, `MXC_STATUS_VERSION_INCOMPATIBLE = 13`) carrying
  `details: { field, declaredVersion, since, until }`. The supported-range error
  migrates onto it. NOTE: this changes an existing error's observable shape — a
  consumer string-matching the old range message is affected.
* Three annotations, each checked against real corpus usage first: `seatbelt`
  since 0.7, `processContainer.captureDenials` / `learningMode` since 0.8. The
  central subtlety is what is deliberately **not** annotated: schema-first-
  appearance is only a lower bound on accepted surface. `experimental` was an
  open block before 0.8, and state-aware requests declare 0.6 while carrying
  `phase` / `sandboxId` / `correlationVector` — annotating those from schema
  data would reject configs that have always worked. (Measured: 66 properties
  are unannotated yet absent from the 0.6 schema; 8 are covered transitively by
  an annotated ancestor, and the rest legitimately carry no range.)
* New `check-version-availability.js` oracle gate derives each field's true
  first appearance from the frozen 0.6/0.7 and dev schemas and fails on
  disagreement. It is fail-closed on the surfaces above, which also catches a
  range that would leak onto the permissive `experimental` surface via a shared
  type.
* Corpus and callers migrated: 61 configs versioned (state-aware to 0.6.0-alpha,
  matching what the SDK emits; one-shot to 0.8.0-alpha), ~200 Rust test
  literals, the PowerShell lifecycle helpers (stamped centrally), and the SDK
  builders, which no longer synthesise a top-level `seatbelt` marker below 0.7.

Tests

* On the final tip: `cargo fmt --all -- --check`,
  `cargo check --workspace --all-targets`,
  `cargo clippy --workspace --all-targets -- -D warnings`, and the per-package
  test suites (`wxc_common` incl. the corpus test, `mxc_schema_gen`,
  `mxc_version_derive`, `mxc_engine`, `mxc-sdk`, `mxc_ffi`, `wxc`,
  `wxc_e2e_tests::e2e_state_aware`) all clean.
* Feature-gated builds covering every flag this diff can reach, all clean:
  `wxc_common` {schema-gen, microvm}, `mxc_ffi` {dotnetsdk}, `mxc_engine`
  {isolation_session}, `wxc` {isolation_session, microvm, tier2_bfs, wslc,
  hyperlight}.
* `wxc_common` 594 unit tests plus a new corpus test asserting all 195 configs
  declare a version and still parse, with the out-of-range fixture pinned as a
  negative case by exact code and bounds. Versioning gate tests 71 → 94.
* Node SDK build + 223 tests; C# SDK 35 tests; ErrorCode parity 17 codes;
  bindings codegen OK. All 11 CI gates pass.
* Non-regression: the PR #676 replay still yields exactly 6 findings naming
  `allowLocalNetwork`, `allowedHosts`, `blockedHosts`, `defaultPolicy`,
  `enforcementMode`, `proxy`; detector baselines hold (dev vs dev = 0,
  0.6→0.7 = 6, 0.7→dev = 12); the dev-schema gate passes; `SUPPORTED_VERSION`
  unchanged at `>=0.6, <=0.8`.
* Converged through a 2-round adversarial review (14 findings; 12 fixed, 1
  pushback accepted, 1 pre-existing). Two blockers were genuine test failures an
  earlier verification pass had masked with a faulty grep.
* **Not executed on this host** (Windows): the macOS Seatbelt paths, the Windows
  Sandbox and IsolationSession PowerShell lifecycle suites, and the host-gated
  MicroVM / Hyperlight E2E configs. The macOS code does **cross-compile** —
  `cargo check --target aarch64-apple-darwin --all-targets` is clean for
  `mxc_engine`, `wxc_common` and `mxc-sdk`, including the new
  `cfg(target_os = "macos")` regression tests — but it has not been run.
  (`mxc_darwin` cannot be cross-checked at all: pre-existing issue #735.)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 21bb36ae-131a-4ab6-b062-a830ba488428
Generated-with: claude-opus-5
@MGudgin
Gudge (MGudgin) requested a review from a team as a code owner August 8, 2026 18:39
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant