Skip to content

Commit fa32478

Browse files
MGudginGudgeCopilot
authored
Phase 3c: formalize the three version axes and the negotiation flow (#582)
This PR rewrites the conceptual sections of docs/versioning.md to match the post-3a/3b reality. It formalizes the three independent, never-conflated version axes and replaces the old version-centric "Version Negotiation" steps with the three-stage, capability-driven runtime flow. Details: - Add a "Three version axes" section: schema/config version (the version field; semver; 0.6.x floor through 0.8.x dev ceiling; major.minor compared), product version (Rust workspace + npm; independent; check-version-sync.js), and host capability (negotiated at runtime, never a string in the config). States the Phase 3a invariant: schema version no longer selects the Windows backend. - Rewrite "Version Negotiation" as three ordered stages: (1) schema-range check at the trust boundary; (2) containment resolve (abstract process/vm intent -> concrete backend per host, independent of version); (3) host-capability negotiate (BaseContainer when is_base_container_usable(), else AppContainer BFS/DACL, logged; the only fallback). Updates the Data Flow diagram to match. - Correct the BaseContainer OS handshake: today MXC uses a fixed SANDBOX_SPEC_VERSION and calls Experimental_CreateProcessInSandbox directly; the EnumerateSandboxSpecVersionInfo spec-version handshake is marked forward-looking / not yet implemented (it had been written as current behavior). - Tighten the Error Contract: schema-range, capability (BackendUnavailable on the ScriptResponse / SDK spawn paths), and policy-unsupported (e.g. deniedPaths) failures are typed and actionable; security policy never fuzzy-falls-back. - Fixups surfaced while editing: bump a stale 0.4.0 example to 0.6.0-alpha; note retired 0.4/0.5 stable schemas are kept as artifacts; correct a promotion-step reference to a non-existent parser symbol. Tests: - Docs-only change; no code or CI gates affected. Draft fact-checked against config_parser.rs, models.rs, fallback_detector.rs, dispatcher.rs, and base_container_runner.rs (SANDBOX_SPEC_VERSION = "0.1.0", Experimental_CreateProcessInSandbox). Generated-with: claude-opus-4.8 Co-authored-by: Gudge <gudge@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3f71414 commit fa32478

1 file changed

Lines changed: 128 additions & 44 deletions

File tree

docs/versioning.md

Lines changed: 128 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ The `version` field in SandboxPolicy must match the MXC config
1212
JSON version: they are the same version, tied 1:1.
1313

1414
When a consumer specifies a SandboxPolicy version (e.g.,
15-
`0.4.0`), MXC creates the corresponding configuration using the
16-
`0.4.0` schema.
15+
`0.6.0-alpha`), MXC creates the corresponding configuration using the
16+
`0.6.0-alpha` schema.
1717

1818
```typescript
1919
// sdk/node/src/types.ts
@@ -43,19 +43,53 @@ Per [semver.org](https://semver.org/):
4343
- **Minor** (x.Y.0) — new features, backward compatible
4444
- **Major** (X.0.0) — breaking changes
4545

46+
## The three version axes
47+
48+
MXC tracks three independent "versions" that are deliberately **never
49+
conflated**. Each answers a different question and changes for different
50+
reasons:
51+
52+
| Axis | What it describes | Where it lives | Who decides it |
53+
|---|---|---|---|
54+
| **Schema (config) version** | The *shape* of the config JSON — which fields exist and what values they accept. | The `version` field in the config / `SandboxPolicy`. | The config author. |
55+
| **Product version** | The MXC *binaries and npm package* that do the work. | Rust workspace version (`src/Cargo.toml`) + `sdk/package.json`. | The release. |
56+
| **Host capability** | What the *running OS* can actually enforce (e.g. whether the BaseContainer sandbox API is usable, velocity keys, Hyper-V). | Negotiated at runtime — **never a string in the config**. | The host, probed at execution time. |
57+
58+
- **Schema version** is semver and is checked at the trust boundary: the parser
59+
accepts the `0.6.x` floor line through the `0.8.x` dev-ceiling line (the
60+
canonical min/max constants are currently `0.6.0-alpha` and `0.8.0-alpha` in
61+
`schemas/schema-version.json`), and the SDK mirrors that range. Only
62+
`major.minor` is compared — patch and pre-release labels are ignored — and both
63+
back-dated and forward-dated versions are rejected.
64+
- **Product version** tracks the shipped artifacts and moves independently of the
65+
schema version; a binary release can fix bugs without changing the config shape.
66+
`scripts/check-version-sync.js` keeps the Rust workspace and npm versions in
67+
step, and `scripts/versioning/check-schema-versions.js` keeps the schema-version
68+
constants in step — but the two axes are not tied to each other.
69+
- **Host capability** is resolved by runtime negotiation, not by a version string.
70+
As of Phase 3a the schema `version` no longer selects the Windows backend:
71+
ProcessContainer resolves to BaseContainer or AppContainer purely by host
72+
capability (see [Version Negotiation](#version-negotiation)). An identical
73+
config runs the same way regardless of which (in-range) schema version it
74+
declares.
75+
4676
## Schema Shipping Model
4777

4878
```
4979
mxc/schemas/
5080
├── stable/
51-
│ ├── mxc-config.schema.0.4.0-alpha.json
52-
│ ├── mxc-config.schema.0.5.0-alpha.json
53-
│ ├── mxc-config.schema.0.6.0-alpha.json
81+
│ ├── mxc-config.schema.0.4.0-alpha.json (retired — below the supported floor)
82+
│ ├── mxc-config.schema.0.5.0-alpha.json (retired — below the supported floor)
83+
│ ├── mxc-config.schema.0.6.0-alpha.json (minimum supported)
5484
│ └── mxc-config.schema.0.7.0-alpha.json (shipped — current stable)
5585
└── dev/
5686
└── mxc-config.schema.0.8.0-dev.json (current — work in progress)
5787
```
5888

89+
Retired stable schema files are **kept as immutable historical artifacts** — the
90+
parser simply stops accepting those versions (the supported floor is
91+
`0.6.0-alpha`). Released schemas are never edited or deleted.
92+
5993
The dev schema file (`mxc-config.schema.X.Y.Z-dev.json`) must define the `experimental`
6094
section structure so that editors can validate experimental configs.
6195

@@ -210,8 +244,8 @@ fn run(&mut self, request: &ExecutionRequest, logger: &mut Logger) -> ScriptResp
210244
from experimental to top-level:
211245

212246
- In `wxc_common::config_parser`, rename the matching entry in
213-
`present_backend_sections` and `owned_backend_section` from
214-
`experimental.<name>` to `<name>`.
247+
`present_backend_sections` (and update `validate_single_backend_section`)
248+
from `experimental.<name>` to `<name>`.
215249

216250
The single-backend-section rule is a cross-field constraint enforced by the
217251
parser (the trust boundary), **not** by the JSON schema — the generated
@@ -232,20 +266,22 @@ User writes SandboxPolicy (policy + environment, versioned)
232266
Config JSON (version: "0.6.0-alpha")
233267
234268
235-
MXC parses → validates schema version
269+
MXC parses → Stage 1: validate schema version (range check)
236270
│ → if --experimental, includes experimental section
237271
238272
239-
MXC calls OS: EnumerateSandboxSpecVersionInfo()
240-
│ → returns supported tech language versions
241-
│ e.g., [1.4.5, 2.0.0]
273+
Stage 2: resolve `containment` intent → concrete backend
274+
275+
276+
Stage 3: probe host capability → select backend tier
277+
│ (ProcessContainer: BaseContainer if usable, else AppContainer)
242278
243279
244-
MXC translates policy → flat buffer
245-
(based on the tech language version the OS supports)
280+
For the BaseContainer tier: translate policy → flat buffer
281+
(fixed SANDBOX_SPEC_VERSION)
246282
247283
248-
MXC calls OS: CreateProcessInSandbox(flatbuffer)
284+
Launch (Experimental_CreateProcessInSandbox(flatbuffer))
249285
250286
251287
Process runs in sandbox
@@ -324,54 +360,102 @@ on the roadmap.
324360

325361
## Version Negotiation
326362

327-
```
328-
1. User sends SandboxPolicy with version "0.6.0-alpha"
363+
Execution resolves a request in three ordered stages. The schema version gates
364+
only the first; it does **not** influence stages 2 or 3 (Phase 3a removed that
365+
coupling).
329366

330-
2. MXC validates: is "0.6.0-alpha" ≤ SUPPORTED_VERSION?
331-
Yes → continue
332-
No → error: "upgrade wxc-exec"
367+
```
368+
Stage 1 — Schema-range check (the trust boundary, `config_parser`)
369+
Is config.version within [floor, dev-ceiling]? (major.minor; pre-release
370+
labels ignored)
371+
below floor → error: "older than supported" (update your config)
372+
above ceiling → error: "newer than supported" (upgrade wxc-exec)
373+
in range / absent → continue
374+
375+
Stage 2 — Containment resolve (independent of schema version)
376+
Map the `containment` intent to a concrete backend:
377+
omitted / "process" → OS-native process sandbox
378+
(Windows: ProcessContainer, Linux: Bubblewrap,
379+
macOS: Seatbelt)
380+
"vm" → host VM-class backend
381+
explicit backend → used verbatim
382+
383+
Stage 3 — Host-capability negotiate (runtime probe, no version input)
384+
For ProcessContainer on Windows:
385+
BaseContainer usable on this host? (is_base_container_usable())
386+
yes → BaseContainer (native OS sandbox API)
387+
no → AppContainer fallback tier (BFS when compiled in with the
388+
`tier2_bfs` feature and `bfscfg.exe` is present, else DACL)
389+
The chosen tier and any fallback are logged (warnings + "selected isolation
390+
tier: …"). This capability fallback is the ONLY fallback.
391+
```
333392

334-
3. MXC calls: EnumerateSandboxSpecVersionInfo(HIGHEST_MAJOR)
335-
OS returns: [
336-
{ version: "1.4.5", isAvailable: true },
337-
{ version: "2.0.0", isAvailable: true }
338-
]
393+
For the BaseContainer tier, Stage 3 translates the policy into a FlatBuffer and
394+
invokes the OS sandbox API. Today MXC builds the FlatBuffer at a fixed spec
395+
version (`SANDBOX_SPEC_VERSION`, `base_container_runner.rs`) and calls
396+
`Experimental_CreateProcessInSandbox` directly:
339397

340-
4. MXC selects the best tech language version for the
341-
features in the policy
398+
```
399+
translate policy → FlatBuffer (fixed SANDBOX_SPEC_VERSION)
400+
→ Experimental_CreateProcessInSandbox(flatbuffer) → success or typed error
401+
```
342402

343-
5. MXC translates policy → flat buffer targeting that
344-
tech language version
403+
> **Forward-looking:** the design anticipates a spec-version *handshake* — the OS
404+
> advertising the spec versions it supports (`EnumerateSandboxSpecVersionInfo`)
405+
> and MXC selecting the best one for the policy's features before translating —
406+
> so that a single binary can target multiple OS sandbox revisions. That
407+
> enumerate/select step is **not implemented yet**; the current code uses the
408+
> fixed spec version above.
345409
346-
6. MXC calls: CreateProcessInSandbox(flatbuffer)
347-
OS returns: success or error with disposition
348-
```
410+
**Backend selection is capability-driven, not version-driven** (Stage 3 takes no
411+
version input), and **security policy never fuzzy-falls-back**: if the selected
412+
backend cannot honor the requested filesystem/network policy, execution fails
413+
with a typed, actionable error rather than silently weakening enforcement (see
414+
[Error Contract](#error-contract)).
349415

350416
## OS APIs
351417

418+
The BaseContainer tier calls the OS sandbox API to launch the child:
419+
352420
```c
353-
// Query what the OS supports
421+
// Execute with the translated policy (current).
422+
HRESULT Experimental_CreateProcessInSandbox(
423+
BYTE* flatbuffer,
424+
UINT32 flatbufferSize,
425+
PROCESS_INFORMATION* processInfo
426+
);
427+
428+
// Forward-looking (not yet implemented): query the spec versions the OS
429+
// supports so a single binary can target multiple sandbox revisions.
354430
HRESULT EnumerateSandboxSpecVersionInfo(
355431
UINT32 highestMajor,
356432
SANDBOX_VERSION_INFO** versions,
357433
UINT32* count
358434
);
359-
360-
// Execute with the translated policy
361-
HRESULT CreateProcessInSandbox(
362-
BYTE* flatbuffer,
363-
UINT32 flatbufferSize,
364-
PROCESS_INFORMATION* processInfo
365-
);
366435
```
367436

368437
## Error Contract
369438

370-
MXC ↔ OS needs a defined error contract:
371-
- Which feature failed
372-
- Whether it's a version mismatch or runtime unavailability (e.g., Hyper-V off)
373-
- What the user should do (upgrade OS, enable feature, etc.)
374-
- Security policy is deterministic — no relaxation, no fuzzy fallback
439+
Negotiation failures are **typed and actionable** — never a silent fallback:
440+
441+
- **Schema-range failures** (Stage 1) carry a clear "older than supported" /
442+
"newer than supported" message telling the caller whether to update the config
443+
or upgrade `wxc-exec`.
444+
- **Capability failures** (Stage 3) surface on the runner's `ScriptResponse`
445+
(and the SDK `spawn` path's `MxcError`) as a `BackendUnavailable` failure
446+
phase when the requested backend's API is absent (e.g. the BaseContainer OS
447+
sandbox API is not present on this build), with a hint pointing at the host
448+
requirement — not a downgrade to a weaker backend behind the caller's back.
449+
(BaseContainer-vs-AppContainer is the one exception, and it is an explicit,
450+
logged capability tier, not a security relaxation.)
451+
- **Policy-unsupported failures** (a backend that cannot honor a specific policy
452+
field, e.g. `deniedPaths`) fail with a specific message naming the
453+
unsupported field. Security policy is deterministic — no relaxation, no fuzzy
454+
fallback.
455+
456+
The MXC ↔ OS contract therefore reports: which feature failed, whether it was a
457+
version mismatch or a runtime/capability unavailability (e.g. Hyper-V off), and
458+
what the user should do (upgrade OS, enable feature, change the config).
375459

376460
## Experimental Features — Clarifications
377461

0 commit comments

Comments
 (0)