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
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ The Rust workspace (`src/`) implements multiple sandboxing backends behind the `

### Config flow

1. User provides JSON config (file or base64) → `config_parser.rs` deserializes into intermediate `Raw*` structs → validates and maps to `ExecutionRequest` (the internal execution model in `models.rs`)
1. User provides JSON config (file or base64) → `config_parser.rs` deserializes into the typed wire model (`wxc_common::wire`) → validates and maps to `ExecutionRequest` (the internal execution model in `models.rs`)
2. `ExecutionRequest` includes the containment backend selection, process config, filesystem/network policies, and optional experimental features
3. The appropriate `ScriptRunner` implementation executes the process and returns `ScriptResponse`

Expand Down Expand Up @@ -163,7 +163,7 @@ State-aware lifecycle:
New features go under the `experimental` JSON section and are only active when `--experimental` is passed. See `docs/authoring-a-new-feature.md` for the full checklist. The pattern:

1. Add the field to the Rust wire model (`src/core/wxc_common/src/wire.rs`) under the `Experimental` section, then regenerate the dev schema (`cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- schemas/dev/mxc-config.schema.<dev>.json`) — do not hand-edit the generated schema
2. Add Rust structs to `models.rs` (`ExperimentalConfig`) and `config_parser.rs` (`RawExperimental`)
2. Add the matching field to the wire model's `Experimental` struct (`src/core/wxc_common/src/wire.rs`) and the domain `ExperimentalConfig` in `models.rs`, then map wire→domain in `config_parser.rs` (use `From` impls beside the domain type for trivial enum/struct conversions)
3. Guard execution behind `if request.experimental_enabled` in the runner
4. Never modify files in `schemas/stable/` — those are immutable release artifacts

Expand Down Expand Up @@ -191,7 +191,7 @@ The workspace is organized into five top-level directories under `src/`:

### Config parser pattern

The parser uses two layers of structs: `Raw*` structs (matching JSON with `#[serde(rename)]`) that deserialize permissively, then map to validated domain structs in `models.rs`. This keeps serde attributes separate from the internal model.
The parser deserializes JSON directly into the typed wire model (`wxc_common::wire`), the single source of truth for the config shape (it also generates the JSON schema). `config_parser.rs` then maps the wire types to the validated domain structs in `models.rs`. The stable surface uses `deny_unknown_fields` (closed); the `experimental` block is permissive.

### TypeScript conventions

Expand Down
181 changes: 67 additions & 114 deletions docs/authoring-a-new-feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,67 +112,52 @@ Adding a feature may touch these files:

| File | What to change |
|------|----------------|
| `schemas/dev/mxc-config.schema.0.8.0-dev.json` | Add `gpuIsolation` as a feature under `experimental` |
| `src/core/wxc_common/src/wire.rs` | Add a `gpuIsolation` field + `GpuIsolation` struct to the wire `Experimental` model (the schema is generated from this) |
| `schemas/dev/mxc-config.schema.0.8.0-dev.json` | **Generated** — regenerate with `mxc_schema_gen` after editing the wire model; do not hand-edit |
| `src/core/wxc_common/src/models.rs` | Add `GpuIsolationConfig` struct, add field to `ExperimentalConfig` |
| `src/core/wxc_common/src/config_parser.rs` | Add `gpuIsolation` field to `RawExperimental` |
| `src/core/wxc_common/src/config_parser.rs` | Map the new wire field to the domain struct in `convert_wire_config` |
| Runner (`appcontainer.rs` or `lxc_runner.rs`) | Feature logic, guarded behind `experimental_enabled` |
| `tests/configs/` | Test config exercising your feature |

## Step 1: Update the schema
## Step 1: Add the field to the wire model (the schema source of truth)

In `schemas/dev/mxc-config.schema.0.8.0-dev.json`, the `experimental` section already
exists with `compartments` as a feature. Add `gpuIsolation` as a new
feature with its own typed schema:
The JSON schema is **generated** from the Rust wire model
(`src/core/wxc_common/src/wire.rs`); you never hand-edit
`schemas/dev/mxc-config.schema.0.8.0-dev.json`. Add your feature as a typed field
on the wire `Experimental` struct (which is intentionally permissive — no
`deny_unknown_fields` — so in-flux experimental shapes stay forward-compatible):

```json
"experimental": {
"type": "object",
"description": "Experimental features. Only active when --experimental is passed.",
"properties": {
"compartments": {
"type": "object",
"description": "Network compartment isolation (experimental).",
"properties": {
"namespace": {
"type": "string",
"description": "Compartment namespace identifier."
},
"isolationLevel": {
"type": "integer",
"minimum": 1,
"description": "Isolation level (1 = shared network, 2 = separate stack, 3 = full isolation)."
}
}
},
"gpuIsolation": {
"type": "object",
"description": "GPU device isolation (experimental).",
"properties": {
"deviceIndex": {
"type": "integer",
"minimum": 0,
"description": "GPU device index to assign to the container."
},
"memoryLimitMb": {
"type": "integer",
"minimum": 0,
"description": "GPU memory limit in megabytes. 0 = no limit."
},
"allowCuda": {
"type": "boolean",
"default": false,
"description": "Allow CUDA runtime access inside the container."
}
},
"required": ["deviceIndex", "memoryLimitMb"]
}
}
```rust
// in wire.rs
pub struct Experimental {
pub compartments: Option<Compartments>,
pub gpu_isolation: Option<GpuIsolation>, // ← add this
// ...
}

/// GPU device isolation (experimental).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-gen", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct GpuIsolation {
/// GPU device index to assign to the container.
pub device_index: Option<u32>,
/// GPU memory limit in megabytes. 0 = no limit.
pub memory_limit_mb: Option<u32>,
/// Allow CUDA runtime access inside the container.
pub allow_cuda: Option<bool>,
}
```

Each experimental feature is its own typed property under `experimental` —
the same pattern as stable features (`filesystem`, `network`) under the
top-level config. This gives editors full autocomplete and validation.
The `///` doc comments become schema `description`s and `#[schemars(...)]`
attributes become constraints. Then regenerate the committed schema:

```
cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- schemas/dev/mxc-config.schema.0.8.0-dev.json
```

The `check-schema-codegen.js` CI gate fails if the committed schema drifts from
the wire model, so the regenerate step is mandatory.

## Step 2: Add the model struct

Expand All @@ -198,74 +183,39 @@ pub struct ExperimentalConfig {
}
```

## Step 3: Parse the experimental section

In `src/core/wxc_common/src/config_parser.rs`, the `RawExperimental` struct already
exists with `compartments`. Add `gpu_isolation`:

```rust
#[derive(Deserialize, Default)]
struct RawExperimental {
compartments: Option<RawCompartments>, // existing
#[serde(rename = "gpuIsolation")]
gpu_isolation: Option<RawGpuIsolation>, // ← add this
}

#[derive(Deserialize)]
struct RawGpuIsolation {
#[serde(rename = "deviceIndex")]
device_index: u32,
#[serde(rename = "memoryLimitMb")]
memory_limit_mb: u32,
#[serde(rename = "allowCuda")]
allow_cuda: Option<bool>,
}
```
## Step 3: Map the wire field to the domain model

In `convert_raw_config()`, map it directly — no name matching needed. Each
feature should own its parsing via a constructor:
The parser deserializes JSON directly into the wire model (`wire::MxcConfig`),
so your `wire::GpuIsolation` from Step 1 is the parse target. In
`config_parser.rs`, map it to the domain struct inside `convert_wire_config`
where the `experimental` block is converted:

```rust
let mut experimental = ExperimentalConfig::default();

if let Some(raw_exp) = raw.experimental {
if let Some(c) = raw_exp.compartments {
experimental.compartments = Some(CompartmentsConfig::from_raw(c)?);
}
if let Some(g) = raw_exp.gpu_isolation {
experimental.gpu_isolation = Some(GpuIsolationConfig::from_raw(g)?);
let experimental = if let Some(raw_exp) = cfg.experimental {
// ... existing feature mappings ...
let gpu_isolation = raw_exp.gpu_isolation.map(|g| GpuIsolationConfig {
device_index: g.device_index.unwrap_or(0),
memory_limit_mb: g.memory_limit_mb.unwrap_or(0),
allow_cuda: g.allow_cuda.unwrap_or(false),
});
ExperimentalConfig {
// ... existing fields ...
gpu_isolation,
}
}
} else {
ExperimentalConfig::default()
};
```

Each feature implements its own `from_raw()` constructor to keep
`convert_raw_config()` clean:

```rust
impl GpuIsolationConfig {
fn from_raw(raw: RawGpuIsolation) -> Result<Self, String> {
Ok(Self {
device_index: raw.device_index,
memory_limit_mb: raw.memory_limit_mb,
allow_cuda: raw.allow_cuda.unwrap_or(false),
})
}
}
```
Prefer destructuring the wire struct (`let wire::GpuIsolation { device_index, .. }`)
in any standalone mapping helper so that adding a wire field without mapping it
becomes a compile error rather than a silent runtime drop.

Add tests to verify:
- `gpuIsolation` config parses correctly
- `gpuIsolation` config parses correctly and maps to `ExecutionRequest.experimental`
- Missing optional fields use defaults
- Unknown fields under `experimental` are ignored (forward compatibility)

Also ensure that `convert_raw_config()` populates `ExecutionRequest.experimental`:

```rust
Ok(ExecutionRequest {
// ... existing fields ...
experimental, // ← include the parsed experimental config
})
```
- Unknown fields under `experimental` are tolerated (forward compatibility — the
experimental surface is intentionally permissive)

## Step 4: Implement the feature in the runner

Expand Down Expand Up @@ -374,10 +324,13 @@ The SDK passes `--experimental` to the underlying binary when this is set.

When your experimental feature is ready to ship:

1. Move the property from `experimental` to a top-level property in the schema
(e.g., `experimental.gpuIsolation` → `gpuIsolation`)
1. Move the field from the wire `Experimental` struct to the top-level
`MxcConfig` (e.g., `experimental.gpuIsolation` → top-level `gpuIsolation`),
then regenerate the schema with `mxc_schema_gen`
2. Move the struct from `ExperimentalConfig` to `ExecutionRequest`
3. Move the field from `RawExperimental` to `RawConfig`
3. Map the now-top-level wire field in `convert_wire_config` (and add
`deny_unknown_fields` to the wire struct so the promoted, stable surface is
closed)
4. Remove the `if request.experimental_enabled` guard
5. Bump the minor version
6. Add a parser error for configs still referencing the feature under
Expand Down
13 changes: 8 additions & 5 deletions docs/bwrap-support/bubblewrap-backend-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@ A backend-specific config can be added later under `ExperimentalConfig` if neede

### 3. Config Parser Changes

**File:** `src/core/wxc_common/src/config_parser.rs`
**File:** `src/core/wxc_common/src/wire.rs` and `config_parser.rs`

- Add `RawBubblewrap` struct (placeholder — empty for now, reserved for future backend-specific fields)
- Add `bubblewrap` field to `RawExperimental` (optional, for future use)
- Add `"bubblewrap"` arm to containment match in `convert_raw_config_inner()`
- Add a `Bubblewrap` variant to the wire `Containment` enum (or rely on the
abstract `process` intent resolving to `Bubblewrap` on Linux)
- Add any backend-specific fields to the wire model (under `experimental` while
experimental), then regenerate the schema with `mxc_schema_gen`
- Map the new `containment` value in `map_wire_containment`
- Optionally: make `"process"` resolve to `Bubblewrap` on Linux when LXC is unavailable
(or add a `"process"` → bwrap fallback chain)

Expand Down Expand Up @@ -337,7 +339,8 @@ policy gap is a design decision, not an implementation challenge.
- `src/core/lxc/Cargo.toml` — add `bwrap_common` dependency
- `src/core/lxc/src/main.rs` — add dispatch arm for `ContainmentBackend::Bubblewrap`
- `src/core/wxc_common/src/models.rs` — add `Bubblewrap` variant, `BubblewrapConfig` struct, wire into `ExperimentalConfig` and `ExecutionRequest`
- `src/core/wxc_common/src/config_parser.rs` — add `RawBubblewrap`, parsing, containment match arm
- `src/core/wxc_common/src/wire.rs` — add the `Bubblewrap` containment variant (and any backend fields), then regenerate the schema
- `src/core/wxc_common/src/config_parser.rs` — map the new containment value in `map_wire_containment`

### Schema (modify)
- `schemas/dev/mxc-config.schema.0.6.0-dev.json` — add `"bubblewrap"` to enum, add config block
Expand Down
5 changes: 3 additions & 2 deletions docs/nanvix-microvm/nanvix-integration-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ mxc/src/
│ └── src/
│ ├── lib.rs # Add: pub mod nanvix_runner (1 line)
│ ├── models.rs # Add: NanVixConfig struct, ContainmentBackend::MicroVm
│ ├── config_parser.rs # Add: RawNanVix struct, "microvm" parsing
│ ├── wire.rs # Add: MicroVm containment variant (schema source); regenerate schema
│ ├── config_parser.rs # Add: map_wire_containment "microvm" arm
│ ├── error.rs # Add: WxcError::NanVix variant
│ ├── nanvix_runner.rs # NEW — NanVixScriptRunner implementation
│ ├── appcontainer.rs # UNCHANGED
Expand Down Expand Up @@ -239,7 +240,7 @@ Setup scripts (PowerShell & Bash) will download matching pre-release binaries an

**What changed:**
- `models.rs` — Added `MicroVm` variant to `ContainmentBackend`, added `NanVixConfig` struct, added `nanvix_config` field to `ExecutionRequest`
- `config_parser.rs` — Added `RawNanVix` serde struct, `"microvm"` containment parsing, NanVix config section parsing
- `config_parser.rs` — Added `"microvm"` containment parsing and NanVix config section parsing (originally via `Raw*` structs; the parser has since been rewired onto the `wire::MxcConfig` model — new work maps the wire types in `convert_wire_config`)
- `error.rs` — Added `WxcError::NanVix(String)` variant
- `nanvix_runner.rs` — **NEW** — `NanVixScriptRunner` implementing `ScriptRunner` trait
- `lib.rs` — Added `pub mod nanvix_runner`
Expand Down
13 changes: 6 additions & 7 deletions docs/schema-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,9 @@ CI run, and the corpus gate pins the accept-side behavior.
## Roadmap

- The wire model generates the committed dev schema, guarded by the codegen and
corpus CI gates. The parser still deserializes via a separate set of
permissive `Raw*` structs.
- Next: rewire the parser to deserialize directly into the wire model and delete
the `Raw*` structs, so the schema source and the trust boundary share one
definition of the wire shape and cannot drift.
- After that: generate the SDK TypeScript types from the same schema and retire
the hand-maintained `*-strict.json` stable view.
corpus CI gates.
- The parser deserializes directly into the wire model and the `Raw*` structs
are gone, so the schema source and the trust boundary share one definition of
the wire shape and cannot drift.
- Next: generate the SDK TypeScript types from the same schema and retire the
hand-maintained `*-strict.json` stable view.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ on the response, and neither shape carries `containerId`.
|---|---|---|
| TypeScript SDK (reference §6) | Five new functions: `provisionSandbox`, `startSandbox`, `execInSandbox` / `execInSandboxAsync`, `stopSandbox`, `deprovisionSandbox`. Branded `SandboxId<C>` type tagging ids by backend (`containment` named once at provision, inferred from the id thereafter). Per-(backend, phase) typed `*Config` interfaces (e.g. `IsolationSessionProvisionConfig`) that absorb cross-cutting fields directly — no separate policy parameter. Per-phase typed `*Result` types per backend. `AbortSignal` cancellation via the existing `SandboxSpawnOptions`. Typed `MxcError` class carrying a closed-enum `code`. | `spawnSandbox` family preserved. `ContainmentBackend` extension reused. The wire-format-aligned `Process` / `Filesystem` / `Network` / `UiConfig` interfaces from `sdk/src/types.ts` are reused as field types inside state-aware Configs. `SandboxSpawnOptions` reused as the third-arg options bag (gains `signal?: AbortSignal`). `*Config` naming convention reused. |
| JSON wire format (reference §7) | Top-level `phase` discriminator. Top-level `sandboxId`. `containment` carried on provision only; non-provision phases route via the `sandboxId` prefix. Per-phase nesting under `experimental.<backend>.<phase>`. Named envelope types as a TypeScript discriminated union. | One-shot configs (no `phase`) work unchanged. Cross-cutting `filesystem` / `network` / `ui` at top level for state-aware too — backends declare per-phase honor. |
| Rust executor (reference §9) | Dispatch arm for state-aware. New `StatefulSandboxBackend` trait. Rust mirror of the wire envelope (private `Raw*` parser pattern). | `ScriptRunner` trait. Existing one-shot dispatch path. Existing backends unchanged. |
| Rust executor (reference §9) | Dispatch arm for state-aware. New `StatefulSandboxBackend` trait. Rust mirror of the wire envelope (the `wire::MxcConfig` parse target). | `ScriptRunner` trait. Existing one-shot dispatch path. Existing backends unchanged. |
| Error model (reference §8) | Closed enum of 12 codes. `MxcError` class with `code: ErrorCode`. `details` open object. | Existing one-shot error paths preserved. |
| Plug-in surface (reference §11) | Implement `StatefulSandboxBackend`. Define typed per-(backend, phase) `*Config` interfaces. Declare the trait's `ID_PREFIX` and `BACKEND_KEY` consts. Document the cross-cutting honor matrix. | Ephemeral-only backends require no changes. |

Expand Down Expand Up @@ -141,8 +141,8 @@ state-aware Config's `filesystem` field — no change to the helpers.
## Wire contract

The wire envelope is a TypeScript discriminated union over `phase`, JSON-serialised.
The Rust executor parses the same shape via private `Raw*` intermediate structs
(reference §9.1). The only `Record<string, unknown>` in the contract is
The Rust executor parses the same shape into the typed wire model
(`wire::MxcConfig`, reference §9.1). The only `Record<string, unknown>` in the contract is
`ErrorEnvelope.details` — the escape hatch for backend-specific structured failure
information.

Expand Down Expand Up @@ -407,8 +407,8 @@ Reference §11 has the full guide. Operational checklist:
`BACKEND_KEY` (the wire-format `containment` value, used for provision-phase
routing and `experimental.<BACKEND_KEY>.<phase>` deserialisation). Add a dispatch
arm for the new variant.
5. Add `Raw*` intermediate structs in `config_parser.rs` for the backend's wire-format
block.
5. Add typed fields to the `experimental` block of the wire model (`wire.rs`) for the
backend's wire-format block, then regenerate the schema.
6. Document policy-honor matrix, idempotence, concurrency, and error mapping in
`docs/<backend-or-feature>/<plan-name>.md` (e.g.,
`docs/isolation-session/state-aware-plan.md`).
Expand Down
Loading
Loading