Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
fabdfd0
[LXC] Implement StatefulSandboxBackend for LXC (state-aware lifecycle…
dhoehna Jul 10, 2026
0853f9c
[LXC] Address PR #633 review: collision-free container names + narrow…
dhoehna Jul 13, 2026
9764b1a
Add LXC state-aware provision sample config (tests/configs/lxc_state_…
dhoehna Jul 15, 2026
af38cc1
Merge microsoft/mxc main (47a172c) into lxc-state-aware-lifecycle
dhoehna Jul 15, 2026
5a138bc
ci: re-trigger PR build (empty commit)
dhoehna Jul 15, 2026
f84cf2b
Address PR #633 review: LXC state-aware lifecycle fixes
dhoehna Jul 16, 2026
dd29002
Merge upstream/main (d271ac4) into lxc-state-aware-lifecycle
dhoehna Jul 17, 2026
91ebaf7
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-s…
dhoehna Jul 20, 2026
7ad03df
Merge upstream/main into user/dahoehna/lxc-state-aware-lifecycle
dhoehna Jul 27, 2026
287b918
[LXC] Address PR #633 review: egress direction, fail-open start, SDK …
dhoehna Jul 30, 2026
95a197d
[LXC] Share state-aware telemetry orchestration between the wxc and l…
dhoehna Jul 30, 2026
783c197
[LXC] Add the missing state-aware lifecycle test and run it from the …
dhoehna Jul 30, 2026
c453787
[LXC] Do not treat the default network policy as an explicit restriction
dhoehna Jul 30, 2026
c9001a3
Merge upstream/main into lxc-state-aware-lifecycle
dhoehna Jul 30, 2026
d287e49
Remove accidentally committed SDK build output and node_modules
dhoehna Jul 30, 2026
a39e1cc
Restore LF line endings in wxc/src/main.rs
dhoehna Jul 30, 2026
15daa8f
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-s…
dhoehna Aug 4, 2026
1832e43
LXC: clear only MXC-added bind mounts, not baseline entries
dhoehna Aug 4, 2026
cdb8344
LXC: make firewall chain name collision-free via a name hash
dhoehna Aug 4, 2026
5c03c13
LXC: remove FORWARD hook on teardown regardless of veth discovery
dhoehna Aug 4, 2026
d8be903
LXC: install the firewall before the container starts (fail closed)
dhoehna Aug 4, 2026
44929f0
LXC: route exec error envelope to stderr so it can't corrupt the stream
dhoehna Aug 4, 2026
d699f9e
LXC test script: make cleanup idempotent so it runs once per run
dhoehna Aug 4, 2026
cd34a4f
docs(sdk): add LXC to the state-aware backend list, note streaming-ex…
dhoehna Aug 4, 2026
e3e657a
LXC: enforce an explicit default network policy (item 6)
dhoehna Aug 4, 2026
9c055f2
LXC: add chain-name collision-freedom spec tests
dhoehna Aug 4, 2026
6d712e3
LXC: fix three evidence-reviewer findings from e3e657a
dhoehna Aug 4, 2026
cb62636
Fix chain-name collision from 32-bit hash truncation (PR 633 review)
dhoehna Aug 4, 2026
adf8e03
docs: correct the 36^11 modulus value in a comment
dhoehna Aug 4, 2026
dbac069
Correct the chain-hash work-factor claim
Aug 4, 2026
bf78432
Correct MAX_CONTAINER_NAME_LEN comment: chain names are not collision…
dhoehna Aug 5, 2026
204bff4
Address review feedback on the state-aware lifecycle
Aug 6, 2026
d7e3bc9
Correct four defects an independent review found in the previous commit
dhoehna Aug 6, 2026
d9ca600
[LXC] Report a timeout that killed a script, and give back chain owne…
dhoehna Aug 7, 2026
2cbaa25
[LXC] Bound the pty drain on silence, not on the caller's whole budget
dhoehna Aug 7, 2026
f8662d7
[LXC] Stop the container before a signal removes its firewall
dhoehna Aug 7, 2026
7813ae9
Scope the stderr error-envelope fallback to LXC
dhoehna Aug 7, 2026
6a154bf
Make a dry-run start answer the question it was asked
dhoehna Aug 7, 2026
e65f65b
Do not unfilter a container that is still running
dhoehna Aug 7, 2026
5449775
Tear down through the manager that owns the chain
dhoehna Aug 7, 2026
5adda93
Require a firewall enforcementMode when the LXC policy restricts
dhoehna Aug 7, 2026
53ac40e
Merge upstream/main (PR #724 dual-stack IPv6) into lxc-state-aware-li…
dhoehna Aug 7, 2026
51c26e4
Merge upstream/main into the state-aware lifecycle branch
dhoehna Aug 12, 2026
2658d07
Drop the IsolationSessionUserConfig import the merge should not have …
dhoehna Aug 12, 2026
c0bc56f
Normalize line endings to LF on the state-aware lifecycle branch
dhoehna Aug 13, 2026
5dd22f4
Merge upstream/main (#810 WSLc per-phase SDK configs) into the state-…
dhoehna Aug 13, 2026
bb5895a
Address Copilot review: fix docs, trap handling, and drop a dead test…
dhoehna Aug 14, 2026
816851b
Add the lxc arm to the state-aware probe and correct the README
dhoehna Aug 14, 2026
3b8fdf5
Merge upstream/main into user/dahoehna/lxc-lifecycle-current
dhoehna Aug 14, 2026
39480f4
Address suppressed review comments: skip probe, cleanup retry, stale …
dhoehna Aug 14, 2026
a481f3d
Fail closed on an unreadable FORWARD, and rewrite lxc config atomically
dhoehna Aug 14, 2026
fd60d0d
Enforce the inbound half of the network policy in state-aware start
dhoehna Aug 14, 2026
4e5933d
Stop the LXC lifecycle paths from failing open on teardown and on races
dhoehna Aug 14, 2026
40cf877
Trust the right stream for LXC errors, and let empty host lists through
dhoehna Aug 14, 2026
9462cb6
Reap an exec's container processes when its timeout fires
dhoehna Aug 14, 2026
2f05fc5
Do not read a failed existence probe as an absent container
dhoehna Aug 14, 2026
7cc27b5
Close three fail-open paths the previous round opened
dhoehna Aug 14, 2026
48657f7
Lock the whole LXC lifecycle, not just start
dhoehna Aug 14, 2026
86a262a
Keep teardown idempotent when there is no LXC root
dhoehna Aug 14, 2026
15a25b2
Take the lifecycle lock in provision too
dhoehna Aug 14, 2026
f83c5ee
Close the remaining fail-open probes and make the mount set atomic
dhoehna Aug 14, 2026
706ce82
Derive each path once so the probe and the reclaim cannot drift
dhoehna Aug 14, 2026
b6e2480
Make each fact have one answer, and stop claiming more than we enforce
dhoehna Aug 14, 2026
b22589f
Say plainly that the exec reaper is hygiene, not containment
dhoehna Aug 14, 2026
c9b0833
Only a missing tool means absent, and only a stopped container is saf…
dhoehna Aug 14, 2026
24b0bfd
Say exactly which network configs a firewall-enforced start refuses
dhoehna Aug 14, 2026
5b12c72
Send containment only on provision, so the lifecycle script can run a…
dhoehna Aug 14, 2026
a2c55c3
Say that allowLocalNetwork is rejected, not silently ignored
dhoehna Aug 14, 2026
2523b98
Merge upstream/main into the state-aware LXC lifecycle branch
dhoehna Aug 14, 2026
0016410
Refuse a Library exec on LXC before the workload runs
dhoehna Aug 14, 2026
c77937b
Pin the state-aware LXC test to a published Alpine release
dhoehna Aug 14, 2026
a551ce2
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-l…
dhoehna Aug 15, 2026
7d3e2e0
[LXC] Enforce the network mode rule at start instead of in the SDK types
dhoehna Aug 16, 2026
c4a87aa
[LXC] Cover the network presence bit end to end on a real container
dhoehna Aug 16, 2026
1470f15
[LXC] Pin filesystem_specified with a case that can actually fail
dhoehna Aug 16, 2026
8fecff7
[LXC] Ask liblxc for the interface list instead of parsing the config
Aug 17, 2026
579d486
[LXC] Assert the default-deny hook is applied, not just reported
Aug 17, 2026
de90b20
[LXC] Drop the lxc.include refusal from the contract
Aug 17, 2026
79ab7ab
[LXC] Require the physdev hook the bridged veth actually uses
dhoehna Aug 17, 2026
bdd8214
[LXC] Drop the stale lxc.include refusal from the repo guidance
dhoehna Aug 17, 2026
b02e731
[LXC] Pin the netdev type named "empty" against the absence signal
dhoehna Aug 17, 2026
825334a
[LXC] Follow the sole interface's index instead of requiring lxc.net.0
dhoehna Aug 17, 2026
1fbc2a9
[LXC] Refuse a network config that moved while it was being read
dhoehna Aug 17, 2026
e140496
[LXC] Recheck the located index, not just the interface summary
dhoehna Aug 17, 2026
d6984f1
[LXC] Enforce the network policy without depending on the interface i…
dhoehna Aug 17, 2026
56fccba
[LXC] Close three teardown gaps an independent review found
dhoehna Aug 17, 2026
ae24c28
[LXC] Ask the container which name its interface has, and re-read bef…
dhoehna Aug 17, 2026
7c452f4
[LXC] Cover every start-phase policy field with a roadmap E2E case
dhoehna Aug 17, 2026
1ced1fa
[LXC] Gate the filesystem phase on the path lists alone
dhoehna Aug 18, 2026
b78cb95
Enforce LXC network policy from the policy, not from enforcementMode
dhoehna Aug 18, 2026
c54733c
Inline the last-line envelope parse at its only call site
dhoehna Aug 18, 2026
b9a6e81
Put the LXC error envelope on stdout like every other backend
dhoehna Aug 18, 2026
c4deacf
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-l…
dhoehna Aug 18, 2026
f9c9352
Drop enforcementMode from LxcNetworkConfig and fix two stale doc comm…
dhoehna Aug 19, 2026
cfff8cd
Enable bridge netfilter on the SDK integration Linux lanes
dhoehna Aug 19, 2026
0ef10b6
Wire experimental.lxc into the generated config schema
dhoehna Aug 19, 2026
669c80e
Merge upstream/main into lxc-experimental-wire
dhoehna Aug 19, 2026
b5d5cd6
Trim LXC runner comments to one-sentence reasons
dhoehna Aug 20, 2026
712ece5
Install the LXC egress firewall before the container starts
dhoehna Aug 20, 2026
e43e48b
Restore CRLF line endings in lxc_runner.rs
dhoehna Aug 20, 2026
e2419a6
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-l…
dhoehna Aug 20, 2026
e9db1c6
[LXC] Halt a reused container when a post-start step fails
dhoehna Aug 20, 2026
a492906
[LXC] Restore the Linux gate on run_watchdog
dhoehna Aug 20, 2026
0289a92
[LXC] Correct what the pty drain comment claims about the abandoned t…
dhoehna Aug 20, 2026
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
14 changes: 14 additions & 0 deletions .azure-pipelines/templates/SDK.Integration.Test.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ jobs:
sudo apt-get install -y -qq lxc lxc-utils dnsmasq-base iptables bubblewrap slirp4netns
displayName: Install LXC, Bubblewrap, and slirp4netns

# LXC enforces network policy from the policy itself, and the default is
# deny-all, so every LXC run installs an egress chain even when the
# request carries no network section. A bridged veth only reaches
# FORWARD while br_netfilter delivers bridged packets to iptables, and
# without it the backend refuses to report success for a policy it
# cannot enforce. Tolerated rather than required, because a hosted pool
# may forbid loading modules; when it does, the LXC tests fail exactly
# as they already would.
- script: |
sudo modprobe br_netfilter || echo "br_netfilter unavailable; LXC policy enforcement is unreachable on this pool"
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1 || true
sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=1 || true
displayName: Enable bridge netfilter

- script: sudo MXC_SKIP_LXC_NETWORK_TESTS=1 MXC_DEBUG=${{ parameters.debug }} npm test
workingDirectory: $(integrationDirectory)
displayName: npm test (sudo, with LXC)
Expand Down
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions .github/workflows/SDK.Integration.Test.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ jobs:
sudo systemctl start lxc-net
sudo systemctl is-active --quiet lxc-net

# LXC enforces network policy from the policy itself, and the default is
# deny-all, so every LXC run installs an egress chain even when the
# request carries no network section. A bridged veth only reaches
# FORWARD while br_netfilter delivers bridged packets to iptables, and
# without it the backend refuses to report success for a policy it cannot
# enforce, so every LXC test fails at the backend probe.
- name: Enable bridge netfilter
if: matrix.os_label == 'linux'
shell: bash
run: |
set -euo pipefail
sudo modprobe br_netfilter
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1
sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=1

- name: Restore execute permission on mxc-exec-mac
if: matrix.os_label == 'macos'
shell: bash
Expand Down
66 changes: 54 additions & 12 deletions docs/lxc-support/lxc-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,49 @@ The `distribution` and `release` fields control which LXC template is used to cr
| `debian` | `bookworm`, `trixie` | Stable, well-tested |
| `fedora` | `39`, `40` | Modern packages |

### State-aware lifecycle configuration

The table above describes the **one-shot** surface, where the two fields sit in
a top-level `lxc` section. The state-aware lifecycle carries the same two
fields in the backend's own sub-object instead, under
`experimental.lxc.provision`, alongside the `provision` phase that consumes
them:

```json
{
"phase": "provision",
"containment": "lxc",
"experimental": {
"lxc": {
"provision": {
"distribution": "alpine",
"release": "3.23"
}
}
}
}
```

`provision` is the only phase that takes LXC-specific configuration. `start`,
`exec`, `stop`, and `deprovision` carry no `experimental.lxc` payload β€” they
route by the `sandboxId` returned from `provision`, and their cross-cutting
`filesystem` and `network` policy comes from the top-level sections.

| Rule | Enforced by | Diagnostic when violated |
|------|-------------|--------------------------|
| Both `distribution` and `release` are required | runtime | `LXC distribution and release are required` |
| `experimental.lxc.provision` must be present on the provision phase | runtime | `experimental.lxc.provision with distribution and release is required` |
| Each field must be a string | schema and parser | ``Invalid configuration at `experimental.lxc.provision.distribution`: invalid type: ... expected a string`` |
| Only one backend section may appear, and it must match `containment` | runtime | `Multiple containment backends configured: ... Only one backend section is allowed; remove the unused section(s)` |

Both fields are optional in the wire model, exactly as they are in the one-shot
`lxc` section, so the schema accepts a provision section that omits them and the
backend is what refuses it. That split is deliberate: the `experimental` block
is intentionally permissive and the schema "is an editor/CI convenience, never
the gate" (`docs/schema-codegen.md`), so requirements that a caller must satisfy
live in the parser and the backend. See `docs/versioning.md` for the
single-backend-section rule and its graduation path.

### Process Environment and Working Directory

The `process.cwd` and `process.env` fields from the standard schema are honored inside the container:
Expand Down Expand Up @@ -111,7 +154,12 @@ Filesystem policies are enforced via bind mounts in the container configuration:

Network policy has two independent halves: outbound (egress) filtering on the host, described first, and inbound (ingress) filtering inside the container, described under [Inbound (ingress) policy](#inbound-ingress-policy).

Both halves require `enforcementMode` to be `firewall` or `both`. Under the default `capabilities` mode, MXC installs no iptables rules at all, so `defaultPolicy`, `allowedHosts`, and `blockedHosts` are parsed but never take effect.
The egress half is installed whenever the policy requires firewall enforcement:
`defaultPolicy` is `"block"` (including its default when omitted),
`allowedHosts` or `blockedHosts` is non-empty, or `proxy` is enabled. The
ingress half is installed for every LXC run, regardless of that predicate. LXC
accepts `enforcementMode` but ignores its value; it does not disable policy
enforcement.

Outbound policies are enforced with parallel `iptables` and `ip6tables` chains scoped to the container's virtual ethernet (veth) interface:

Expand Down Expand Up @@ -220,13 +268,14 @@ Egress firewall state is torn down automatically with best-effort removal of the

### Inbound (ingress) policy

Inbound filtering is a separate chain from the egress chains above, and it lives **inside the container's own network namespace** rather than on the host. Every command is issued through `nsenter -t <init-pid> -n`, so the container's init PID is mandatory. When a firewall enforcement mode is requested and MXC cannot discover that PID, the run is aborted rather than started with inbound enforcement silently disabled. This is LXC-specific, and the Bubblewrap comparison is policy-dependent rather than absolute: Bubblewrap gives the sandbox its own network namespace via `--unshare-net` when the default policy is `block` with no `allowedHosts`, no `blockedHosts`, and no proxy, and shares the host's namespace otherwise. It installs no inbound chain in either case β€” under `--unshare-net` because nothing outside the sandbox can reach in, and when the namespace is shared because an inbound chain there would be host-wide.
Inbound filtering is a separate chain from the egress chains above, and it lives **inside the container's own network namespace** rather than on the host. Every command is issued through `nsenter -t <init-pid> -n`, so the container's init PID is mandatory for every run. If MXC cannot discover that PID, the run is aborted rather than started with inbound enforcement silently disabled. This is LXC-specific, and the Bubblewrap comparison is policy-dependent rather than absolute: Bubblewrap gives the sandbox its own network namespace via `--unshare-net` when the default policy is `block` with no `allowedHosts`, no `blockedHosts`, and no proxy, and shares the host's namespace otherwise. It installs no inbound chain in either case β€” under `--unshare-net` because nothing outside the sandbox can reach in, and when the namespace is shared because an inbound chain there would be host-wide.

Every `iptables`/`ip6tables` subprocess is spawned with `LC_ALL=C` and `LANG=C`. Teardown decides whether a non-zero exit means "already absent" by matching iptables' own diagnostic text, and that text is localized, so an unpinned locale would turn a benign already-absent result on a non-English host into a fatal error and abort every fresh install.

The rows below describe the `firewall` and `both` enforcement modes. `networkEnforcementMode` defaults to `capabilities`, and under that mode the ingress path installs nothing at all β€” the same gate that skips the egress chains skips this one, so inbound is unfiltered and `allowLocalNetwork: true` is accepted rather than refused. Inbound default-deny is a property of the firewall enforcement modes, not of every LXC run.
The rows below apply to every LXC run. Inbound default-deny is unconditional;
it does not depend on the egress policy or `enforcementMode`.

| Policy (`networkEnforcementMode`: `firewall` or `both`) | Implementation |
| Network policy | Implementation |
|--------|---------------|
| `allowLocalNetwork: false` (default) | Container `INPUT` chain drops new inbound connections |
| `allowLocalNetwork: true` | **Not yet implemented.** Firewall setup fails with an explicit not-yet-implemented error rather than falling back to an unenforced accept |
Expand Down Expand Up @@ -265,15 +314,8 @@ unreachable and the firewall rule would never match. `{ "builtinTestServer":
true }` is rejected for the same reason, as is a `url` whose host is a loopback
literal.

Two further constraints are enforced at parse time, both rejections rather than
silent corrections:
One further constraint is enforced at parse time:

- **`enforcementMode` must be `firewall` or `both`.** Under the default
`capabilities` mode no iptables rules are installed, so the proxy env vars
would be injected while direct egress stayed open β€” a config that reads as
deny-all-except-proxy and enforces neither half. MXC refuses it rather than
auto-promoting the mode, so a stated enforcement level is never silently
rewritten.
- **The `url` must not carry credentials.** LXC passes the proxy URL to
`lxc-attach` as a `--set-var` argument, and process arguments are
world-readable through `/proc/<pid>/cmdline`, so inline `user:pass@` would be
Expand Down
52 changes: 51 additions & 1 deletion docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,27 @@ wire format and have different roles:
| Field | Where it appears | Source | Purpose |
|---|---|---|---|
| `sandboxId` | State-aware wire envelope (Β§7); SDK return value from `provisionSandbox` | System-generated by the backend | Opaque routing identifier; must be passed to subsequent state-aware calls |
| `containerId` | One-shot wire envelope (per `docs/schema.md`) | Caller-supplied (or auto-generated random hex) | Human-readable label, used as e.g. AppContainer profile name |
| `containerId` | One-shot wire envelope (per `docs/schema.md`); **LXC** state-aware `provision` | Caller-supplied (or auto-generated random hex) | Human-readable label, used as e.g. AppContainer profile name or LXC container name |

State-aware non-provision calls carry `sandboxId` on the request; provision returns it
on the response. A state-aware request **may** also carry `containerId` β€” the parser
preserves it into the request the backend receives β€” but it is inert for backends that
do not use it as a label, and it is never a routing key on the state-aware path.
One-shot calls carry `containerId` (when present); they do not carry `sandboxId`.

**LXC exception.** LXC is the one backend whose state-aware `provision` also accepts
`containerId`, because its durable state *is* the named container and `lxc-ls` /
`lxc-attach` address it by name. Consequences callers must understand:

- The returned `sandboxId` is `lxc:<containerName>`, so the name is not opaque.
- Provision is **adopt-or-create**: if a container with that name already exists it is
reused rather than created, and `provisionMetadata.created` reports which happened.
- Deprovision **destroys the container unconditionally**, including one it adopted
rather than created. MXC retains no caller-side state between phases, so deprovision
cannot distinguish an adopted container from a created one. Callers that pass a
`containerId` for a pre-existing container therefore hand MXC ownership of it.
- Omit `containerId` to get a generated name and avoid adopting anything.

## 6. TypeScript SDK

The SDK adds five new functions, exported from `@microsoft/mxc-sdk` alongside the existing
Expand Down Expand Up @@ -1688,6 +1701,43 @@ unconditionally by the in-guest agent).
| `network` | rejected | rejected | rejected | rejected | rejected |
| `ui` | rejected | rejected | rejected | rejected | rejected |

For LXC, filesystem path lists and `network` are applied at **start** β€” the container is
created empty at provision and its mounts and iptables chain are installed just before
it runs β€” and rejected at every other phase. Only a *non-empty* path list counts as a
filesystem policy, so a `filesystem` block whose lists are all empty is accepted at any
phase. `ui` is not consulted anywhere in the LXC backend; `LxcStartConfig` does not
expose it, so SDK callers cannot pass it, but a raw-JSON caller that sends it is not
rejected.

| Field | provision | start | exec | stop | deprovision |
|---|---|---|---|---|---|
| `filesystem` (non-empty path lists) | rejected | applied | rejected | rejected | rejected |
| `network` | rejected | applied | rejected | rejected | rejected |
| `ui` | ignored | ignored | ignored | ignored | ignored |

> **LXC `network` constraints.** `network.proxy` and `allowLocalNetwork` are
> rejected at start. LXC enforces network policy through iptables whenever
> `defaultPolicy` is `"block"`, either host list is non-empty, or proxy is
> enabled. The value of `enforcementMode` is accepted but ignored by LXC; it
> does not disable enforcement. The veth is not discovered after start: the name
> is derived from the container name and pinned by a container-global
> `lxc.hook.start-host` installed before start, so the chain is scoped before the
> interface exists. The hook resolves the container's peer interface from
> `$LXC_PID` and renames it to that name; if it cannot find one it exits
> nonzero, which aborts the start rather than leaving the container unfiltered.
> The interface set comes from liblxc rather than from the container's own
> config file, so an `lxc.include` that declares interfaces elsewhere is
> resolved rather than guessed at, and no interface index is read at all β€” a
> container numbering its only interface `lxc.net.3` is enforced exactly as one
> using `lxc.net.0`, as is one at any index liblxc accepts. Start fails instead
> when that pin cannot be trusted to cover the container's traffic β€” when the
> container declares anything other than exactly one interface, or does not
> declare that interface's type as `veth`. An undeclared type is refused
> alongside a wrong one, because absence is not evidence of a veth, and a
> `macvlan` or `phys` interface would take a hook pinned to a veth name that
> never appears while its traffic ran unfiltered. `removeRulesOnExit` is not
> part of the LXC surface (see `LxcNetworkConfig`).

- **Compile-time enforcement at the SDK.** Each per-(backend, phase) Config (Β§6.1)
declares only the cross-cutting fields the matrix marks as `applied` for that phase
*and* that the runtime currently honors. TypeScript rejects callers passing fields
Expand Down
48 changes: 48 additions & 0 deletions schemas/dev/mxc-config.schema.0.8.0-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,17 @@
],
"description": "IsolationSession backend config (Windows)."
},
"lxc": {
"anyOf": [
{
"$ref": "#/definitions/LxcExperimental"
},
{
"type": "null"
}
],
"description": "LXC backend config (Linux)."
},
"seatbelt": {
"anyOf": [
{
Expand Down Expand Up @@ -401,6 +412,43 @@
},
"type": "object"
},
"LxcExperimental": {
"description": "LXC backend config under the experimental surface. Carries only the per-phase state-aware nesting for the phases that take config (`provision`); the one-shot LXC surface is the stable top-level `lxc` section, so this type is named apart from it rather than shared with it. `start`, `exec`, `stop`, and `deprovision` take no per-phase config payload.",
"properties": {
"provision": {
"anyOf": [
{
"$ref": "#/definitions/LxcProvisionPhase"
},
{
"type": "null"
}
],
"description": "State-aware provision-phase configuration."
}
},
"type": "object"
},
"LxcProvisionPhase": {
"description": "Provision-phase LXC configuration (state-aware lifecycle), nested under `experimental.lxc.provision`. Names the container image to create.\n\nFilesystem mounts and network policy derive from the top-level `filesystem` and `network` sections, not from here. It is its own type rather than a shared one because a shared type would advertise its fields on every phase in the generated schema.",
"properties": {
"distribution": {
"description": "Distribution image (e.g. `alpine`).",
"type": [
"string",
"null"
]
},
"release": {
"description": "Distribution release (e.g. `3.23`).",
"type": [
"string",
"null"
]
}
},
"type": "object"
},
"Network": {
"additionalProperties": false,
"description": "Network access policy.",
Expand Down
8 changes: 4 additions & 4 deletions sdk/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ capability names are reserved and must not be added directly to

For long-lived sandboxes where you provision once, exec many times, and tear down at the end (e.g. agentic loops), use the state-aware lifecycle.

> **Backend support:** the state-aware lifecycle is currently implemented for `isolation_session`, `windows_sandbox`, and `wslc` (all Windows-only; all still experimental, so every call must pass `{ experimental: true }`). The one-shot spawn APIs (`spawnSandbox` / `spawnSandboxFromConfig`) are the supported path for every other backend.
> **Backend support:** the state-aware lifecycle is currently implemented for `isolation_session`, `windows_sandbox`, and `wslc` (all Windows-only; all still experimental, so every call must pass `{ experimental: true }`) and `lxc` (Linux-only; not experimental). The one-shot spawn APIs (`spawnSandbox` / `spawnSandboxFromConfig`) are the supported path for every other backend.

```typescript
import {
Expand Down Expand Up @@ -404,10 +404,10 @@ spawnSandboxFromConfig(config, options?, workingDirectory?, env?) β†’ IPty | Chi
spawnSandbox(script, policy, options?, workingDirectory?, containerName?, env?) β†’ IPty
spawnSandboxAsync(script, policy, ...) β†’ Promise<{ stdout, stderr, exitCode }>

// State-aware lifecycle (currently `isolation_session`, `windows_sandbox`, and `wslc` β€” all Windows-only)
// State-aware lifecycle (`isolation_session`, `windows_sandbox`, and `wslc` β€” Windows-only, experimental; `lxc` β€” Linux-only)
// `config` on provisionSandbox is required for backends whose provision config
// has a required member (isolation_session: the network acknowledgment) and
// optional otherwise (windows_sandbox, wslc).
// has a required member (isolation_session: the network acknowledgment; lxc:
// distribution and release) and optional otherwise (windows_sandbox, wslc).
provisionSandbox(containment, config, options?) β†’ Promise<ProvisionResult>
startSandbox(sandboxId, config?, options?) β†’ Promise<StartResult>
execInSandbox(sandboxId, config, options?) β†’ IPty // streaming
Expand Down
Loading
Loading