Python SDK
The Python SDK at sdks/python/ exposes three API surfaces from the same mvmforge package:
- Authoring — decorators and factories that register a workload to be emitted as canonical IR.
- Runtime — host-side
f.remote(...)andsession(...)for calling function-entrypoint workloads from a Python process. Dev-only by design (per ADR-0010 §2). - Sandbox — typed lifecycle handles (
Sandbox,Process,FileEntry, …) over the local mvm sandbox primitives.
All three use keyword-argument decorator style per ADR-0003 §3.
Install
Available on PyPI: mvm.
pip install mvmOr, in a pyproject.toml:
dependencies = ["mvm>=0.1.2"]Optional extras:
pip install 'mvm[schema]' # pydantic-based mv.derive_schema(...)pip install msgpack # only if a workload declares format="msgpack"For local development against this repo (cloned), use cd sdks/python && uv sync instead.
Authoring
Two workload shapes share the same workload(...) + @app(...) registration:
- Command entrypoint — service exec’d at boot.
- Function entrypoint — wrapper reads stdin, dispatches
module:function, writes return on stdout. Requiresdependencies=.
Command entrypoint
import mvm as mv
mv.workload(id="my-service")
@mv.app( name="my-service", source=mv.local_path("src"), image=mv.nix_packages(["python312"]), entrypoint=mv.entrypoint(command=["python", "-m", "my_service"]), resources=mv.resources(cpu_cores=2, memory_mb=512, rootfs_size_mb=1024),)def my_service(): """The decorated function body is ignored at emit time."""Function entrypoint (short form)
import mvm as mv
@mv.func( name="adder", image=mv.nix_packages(["python312"]), resources=mv.resources(cpu_cores=1, memory_mb=256, rootfs_size_mb=512),)def add(a: int, b: int) -> int: return a + b
add(2, 3) # → 5 (local in-process call)add.remote(2, 3) # → 5 (dispatched in the VM via mvmctl invoke)mv.func(...) registers the workload + app + function entrypoint in one call (mirrors the TypeScript SDK’s mv.func({...}, fn)). Defaults: module=fn.__module__, function=fn.__name__, language="python", dependencies=no_deps(), source=local_path("."). The decorated function becomes a RemoteFunction. If fn.__module__ is "__main__", you must pass module= explicitly so the emitted IR is deterministic across python entry.py and python -m pkg.entry invocation styles.
The decorated function’s typed parameters and return type populate args_schema / return_schema automatically — no mv.derive_schema(...) needed for the common case. See Payload schemas for the supported type table and when to reach for pydantic.
Function entrypoint (long form)
import mvm as mv
mv.workload(id="adder")
@mv.app( name="adder", source=mv.local_path("."), image=mv.nix_packages(["python312"]), entrypoint=mv.entrypoint_function(module="adder", function="add"), resources=mv.resources(cpu_cores=1, memory_mb=256, rootfs_size_mb=512), dependencies=mv.no_deps(),)def add(a: int, b: int) -> int: return a + bSame end-state as mv.func(...); useful for multi-app workloads or when you need a custom source= / workload id.
Multi-function apps (ADR-0014 Phase 2)
A workload can bundle several callable functions sharing one image,
one resources block, and one source tree. Repeat @mv.func(name="X", ...)
against the same workload name; subsequent decorations contribute
additional entrypoints rather than registering a new app.
@mvm.func(name="math-svc", module="math", primary=True)async def add(a: int, b: int) -> int: return a + b
@mvm.func(name="math-svc", module="math")async def mul(a: int, b: int) -> int: return a * bThe first decoration carries the app-level config (image,
resources, source, dependencies, network, depends_on,
mounts, env). Subsequent decorations against the same name=
must omit those — the SDK rejects them with a clear hint pointing
at “app-level config goes on the FIRST decoration.”
primary=True marks the workload’s default function — the one
mvmctl invoke math-svc (no --fn selector) dispatches to. The
SDK auto-marks the first decoration primary; you can flip it
explicitly via primary=True/False. The validator rejects
multi-function apps with no primary (E_NO_PRIMARY_ENTRYPOINT),
multiple primaries (E_MULTIPLE_PRIMARY_ENTRYPOINTS), or
duplicate (module, function) pairs (E_DUPLICATE_ENTRYPOINT_FUNCTION).
For long-form authoring, mv.app(name=..., entrypoints=[...])
takes a plural entrypoints= list directly — useful when you want
to declare a multi-function app explicitly.
Top-level functions
| Function | Purpose |
|---|---|
workload(*, id) | Declares the workload identity. Call once per emit. id matches [a-z][a-z0-9-]{0,62}. |
app(...) | Decorator factory; registers an app. |
func(*, name, image, resources, module=None, function=None, language="python", format="json", source=None, working_dir="/app", env=None, network=None, mounts=None, dependencies=None, args_schema=None, return_schema=None, extra_imports=None) | Workload + app + function-entrypoint shortcut. Defaults dependencies=no_deps(), source=local_path("."), infers module/function from the decorated callable. |
local_path(path, *, include=None, exclude=None) | Source tree, relative to the manifest. Glob syntax per Source bundling. |
nix_packages(packages) | Image declaration. List of nixpkgs attribute names. |
entrypoint(*, command, working_dir="/app", env=None) | Command-style entrypoint. |
entrypoint_function(*, module, function, language="python", format="json", working_dir="/app", env=None, args_schema=None, return_schema=None, extra_imports=None) | Function-style entrypoint. |
resources(*, cpu_cores, memory_mb, rootfs_size_mb) | Resource sizing. |
python_deps(*, lockfile, tool="uv") / node_deps(*, lockfile, tool="pnpm") / no_deps() | Dependency declarations (required for function entrypoints, per plan-0008). |
network(*, mode="none", ports=None, egress=None, peers=None, dns=None) | Network posture (plan-0004 §Phase 5). host mode rejected for function workloads. |
egress(allowlist) / host_port(host, port) / dns_none() / dns_system() / dns_resolver(host, port=53) | Granular network grants. |
derive_schema(fn, *, return_only=False) | Derive JSON Schema from type hints (requires mvm[schema]). |
emit_json() / reset() | Serialize / reset module state (used by tests + the emit subprocess). |
language field
Per ADR-0010 §4, entrypoint_function(language=...) defaults to "python" — the SDK records its own language. Override explicitly to author a workload in a different language. Current allowlist: python, node, wasm. The host validator rejects values outside the allowlist with E_UNSUPPORTED_LANGUAGE (see error codes).
For language="wasm": module is interpreted as the relative path of a .wasm (WASI Preview 1) file inside the bundled source tree; function is captured for documentation but unused at dispatch time (wasmtime run invokes the module’s _start export). This unlocks any compile-to-WASM toolchain (Rust + wasm32-wasi, TinyGo, Zig, AssemblyScript, .NET NativeAOT-LLVM, etc.) without a per-language factory in mvmforge — the user’s .wasm IS the wrapper and satisfies the wire contract via WASI host functions.
Runtime
When entrypoint_function(...) is registered, the decorator returns a RemoteFunction wrapper.
RemoteFunction
| Attribute | Notes |
|---|---|
add(2, 3) | Calls the local function — for in-process testing. |
add.remote(2, 3) | Encodes args, shells out to mvmctl invoke, decodes the return. Synchronous. |
add.local | The undecorated local function. |
add.workload_id | Workload id at registration time. |
add.format | Wire format ("json" or "msgpack"). |
Transport contract:
- stdin to
mvmctl invoke: encoded[args, kwargs]per declared format. - stdout from
mvmctl invoke: encoded return value. - non-zero exit + structured stderr envelope →
RemoteError. - non-zero exit without an envelope →
MvmTransportError.
mvmforge.session(workload_id)
Context manager holding a single VM warm across calls (plan-0003):
with mvmforge.session("adder"): add.remote(2, 3) # boots the VM add.remote(4, 5) # reuses itSession id propagates via contextvars; works inside asyncio tasks. Torn down on exit even if the body raised.
Errors
| Class | When |
|---|---|
RemoteError(kind, error_id, message) | User code in the VM raised. error_id is stable. |
MvmTransportError | Transport failure (couldn’t reach substrate, unparseable response, cap exceeded). |
MsgpackUnavailable | format="msgpack" declared but msgpack package not installed. |
SecretInArgWarning / SecretInArgError | Heuristic flagged a secret-shaped kwarg name. Strict mode via MVMFORGE_STRICT_SECRETS=1. |
EmittingContextError | Layer-3 call (.remote() / session()) fired during mvmforge emit (MVMFORGE_EMITTING=1 is set). Per ADR-0010 §2 — prevents build-time recursion. |
Transport caps + timeouts
Tunable via env vars (see Environment variables):
| Env var | Default | Affects |
|---|---|---|
MVMFORGE_INVOKE_TIMEOUT_SEC | 60 | Single mvmctl invoke wall-clock budget. |
MVMFORGE_SESSION_START_TIMEOUT_SEC | 60 | mvmctl session start. |
MVMFORGE_SESSION_STOP_TIMEOUT_SEC | 30 | mvmctl session stop. |
MVMFORGE_MAX_OUTPUT_BYTES | 16 MiB | Hard cap on mvmctl stdout/stderr (each independently). |
MVMFORGE_STRICT_SECRETS | unset | 1 = SecretInArgError instead of warning. |
MVMFORGE_EMITTING | (set by host) | 1 blocks Layer-3 calls. |
MVMFORGE_MVM_BIN | PATH search | The mvmctl binary. |
Result decoding adds defense-in-depth: max nesting depth 64, non-finite numbers rejected, duplicate JSON keys rejected.
Dev / production posture
The runtime SDK is a dev iteration tool, structurally absent from the production runtime call path (per ADR-0010 §2). Production microVMs are observed via mvmctl logs and output streams — no host-side .remote() calls. Two safety gates enforce this:
MVMFORGE_EMITTING=1(build-time guard, set by host).- The wrapper’s runtime
mode = "prod" | "dev"(gates dev-only execution surfaces inside the VM; default"prod").
Sandbox
Typed lifecycle surface for local mvm sandboxes (plan-0010 phase A). Use for ad-hoc dev iteration: spin up a sandbox, exec processes, manage filesystem and snapshots.
import mvm as mv
with mv.Sandbox.create(name="sandbox-1") as sb: info = sb.info() proc = sb.spawn(["python", "-c", "print('hello')"]) proc.await_exit(timeout=5) sb.write_file("/tmp/hello", b"hi") entries = sb.list_dir("/tmp") snap = sb.snapshot()Surfaces:
| Symbol | Purpose |
|---|---|
Sandbox | Lifecycle: create, list, get, info, destroy. |
Process / ProcessInfo | Spawned process handles + status. |
FileEntry / FileStat | Filesystem listings + metadata. |
SnapshotEntry | Snapshot lifecycle records. |
SandboxInfo / SandboxMetrics | Sandbox state introspection. |
SandboxError / SandboxNotFound | Substrate-side failure types. |
The sandbox surface is dev-only (same posture as the runtime SDK): production deployments don’t drive sandboxes from host SDK code.
The emit subprocess contract
Per ADR-0002:
python -m mvm <entry.py> with MVMFORGE_IR_OUT=<output-path> MVMFORGE_EMITTING=1 (host sets this; SDK reads it for guard)__main__ loads <entry.py> (decorators register), calls emit_json(), writes canonical JSON to MVMFORGE_IR_OUT. Exit codes 0 / 1 / 2 (success / emit failure / I/O or CLI error). No stdout on success.
MVMFORGE_IR_OUT=/tmp/ir.json uv run python -m mvm app.pycat /tmp/ir.json | jqInternal layout
sdks/python/ pyproject.toml uv.lock mvmforge/ __init__.py # public re-exports __main__.py # subprocess emitter _dsl.py # authoring DSL _remote.py # RemoteFunction + transport + EmittingContextError _session.py # session() context manager _sandbox.py # Sandbox surface _subprocess.py # capped + timed mvmctl wrapper _ir/workload.py # generated lower layer (datamodel-codegen) tests/ test_emit*.py test_remote.py test_session.py test_emitting_guard.py test_sandbox*.py test_schema_derive.pymvmforge (everything in __init__.py’s __all__) is the stable public surface. _ir/ and the _dsl / _remote / _session / _sandbox modules are internal — pin exact mvmforge versions if you import from them. Layer isolation is enforced via import-linter per ADR-0003 §5.
Regenerating the lower layer
just sdk-python-gen # writes sdks/python/mvmforge/_ir/workload.pyjust sdk-python-check # asserts the committed file matches fresh regenerationjust sdk-python-check is wired into just ci and fails on drift.
Tests
just sdk-python-test128 tests cover the DSL surface, the subprocess contract, the runtime transport (against a fake mvmctl), the emit-context guard, the secret-name heuristic, and the sandbox surface (including a cross-language argv-corpus parity check shared with the TypeScript SDK).
Cross-SDK conformance
The Python SDK is one of two active SDKs. The golden corpus at tests/corpus/ enforces byte-identical canonical IR across SDKs for semantically equivalent source. just corpus-check runs every SDK against every corpus entry; cross-language pairs (Python authoring + TS authoring of the same workload) must produce identical bytes.
When you change DSL behavior in one SDK, change it in the other in the same PR, and add a corpus entry that exercises the new behavior.