Workload IR
The Workload IR is the single contract between language SDKs and the host toolchain. Everything above it (Python decorators, TypeScript higher-order calls) generates IR; everything below it (compile, up, the substrate) consumes IR. Cross-language byte-identity is enforced at the IR layer.
ADR-0002 makes this commitment binding: SDKs may not emit Nix or mvm artifacts directly, only IR.
Schema source of truth
The canonical Rust types live in crates/mvmforge-ir/src/workload.rs. The JSON Schema at schema/workload-ir-v0.json is generated from those types via schemars and committed alongside them. Per-language SDK types are then generated from that schema.
You should never hand-author IR types or serialized IR. The toolchain maintains drift checks at every layer.
v0 field set (schema version 0.1)
The IR document is canonical JSON (RFC 8785). Top-level shape:
{ "schema_version": "0.1", "id": "<workload-id>", "apps": [...], "volumes": [], "extensions": {}}Workload
| Field | Type | Notes |
|---|---|---|
schema_version | "0.1" | Host rejects unsupported MAJOR; rejects MINOR greater than its known minor. |
id | string [a-z0-9-]{1,64} | Workload name. Used as mkGuest’s name and hostname. |
apps | App[] | v0: exactly one entry. |
volumes | Volume[] | Optional; default []. |
extensions | { [substrate]: object } | Reserved partition for substrate-specific fields. v0 has no registered extensions. |
App
| Field | Type | Notes |
|---|---|---|
name | string | Unique within the workload. |
source | Source | How user code enters the VM. Required. |
image | Image | Runtime environment. |
entrypoint | Entrypoint | command, working_dir, env. |
env | { [k]: EnvValue } | Optional. App-level env vars; merged under entrypoint env. |
mounts | Mount[] | Optional. Volume / host-path / tmpfs. |
network | Network | null | Optional. mode: "none" | "bridge" plus ports. |
resources | Resources | cpu_cores, memory_mb, rootfs_size_mb. |
Source
Tagged union, exactly one variant per app:
{ "kind": "local_path", "path": "src", "include": ["**"], "exclude": ["target/**"] }v0 implements only local_path. The reserved variants nix_derivation and oci_image are accepted by serde but rejected by host validation with E_SOURCE_KIND_DEFERRED.
Image
{ "kind": "nix_packages", "packages": ["python312", "curl"] }packages is a list of nixpkgs attribute names. v0 implements only nix_packages.
Entrypoint
{ "command": ["python", "-m", "hello"], "working_dir": "/app", "env": {}}command is parsed argv; no shell interpolation. working_dir defaults to /app. The bundled source is symlinked to working_dir at boot (see Source bundling).
EnvValue
{ "kind": "literal", "value": "production" }or
{ "kind": "secret_ref", "ref": { "name": "api-token", "mount": { "kind": "env", "var": "TOKEN" } } }v0 host rejects any secret_ref with E_SECRETS_NOT_IMPLEMENTED. Secrets resolution awaits a dedicated ADR.
Other types
Mount, Network, PortForward, Resources, Volume, SecretRef, SecretMount follow the same tagged-union pattern. Full normative reference lives in specs/plans/0002-ir-and-codegen-tracer.md Appendix A.
Validation
mvmforge validate <manifest.json> runs every v0 rule and accumulates errors (no fail-fast):
- Schema version gating (
E_UNSUPPORTED_MAJOR,E_MINOR_TOO_HIGH,E_MALFORMED_VERSION). - Closed-world field check (
E_UNKNOWN_FIELD). - App count (
E_EMPTY_APPS,E_MULTI_APP_DEFERRED). - Reserved source kinds (
E_SOURCE_KIND_DEFERRED). - Secret-ref presence (
E_SECRETS_NOT_IMPLEMENTED). - Persistent volumes (
E_PERSIST_DEFERRED). - Network sanity (
E_NETWORK_PORTS_WITH_NONE).
The full registry lives at Error codes.
Versioning
schema_version is a MAJOR.MINOR string. Per ADR-0002:
MINORbump: additive — new optional fields, new variants in open sum types.MAJORbump: breaking — removing fields, renaming, changing meaning. Requires a superseding ADR.- The host rejects
MAJORit doesn’t know. - Within a known
MAJOR, the host rejectsMINORhigher than its own known minor (fail-closed). - Unknown fields within a supported
MAJOR.MINORare rejected (closed-world).
Why “canonical”
Two semantically equivalent IR documents must produce byte-identical output after mvmforge canonicalize. This is the test the golden corpus uses to enforce cross-SDK conformance: Python and TypeScript SDKs declaring the same workload must produce the exact same bytes.
See Canonicalization for the algorithm.