SDK — Python and TypeScript
The mvm SDK lets you declare a microVM workload from a single
decorated function in Python or TypeScript. Two routes from the same
source file produce the same Workload IR:
- In-process:
python app.pyornode app.tsimportsmvm, the decorator records the declaration, andmvm.emit_json()/mvm.emitJson()writes the canonical IR to stdout. - Static compile:
mvmctl compile app.pywalks the AST without importing the script. Same IR; the host never runs user code.
Declaration
Section titled “Declaration”import mvm
@mvm.app( name="hello-app", source=mvm.local_path("."), image=mvm.python_image(python="3.12"), resources=mvm.resources(cpu_cores=1, memory_mb=256, rootfs_size_mb=512), entrypoint=mvm.entrypoint_function( module="app", function="greet", primary=True, ), env={ "MODEL_PATH": mvm.literal("/data/model.pt"), "API_KEY": mvm.secret("api-key"), }, before_start="export HELLO_BANNER='hi'", after_start=mvm.hook(["curl", "-fsS", "http://127.0.0.1:8080/health"]),)def greet(name: str) -> str: return f"hello {name}"Build, run, invoke:
mvmctl compile examples/python/hello-app/app.py --out /tmp/hello-appmvmctl build /tmp/hello-appmvmctl up hello-appmvmctl invoke hello-app --input name='ari'import * as mvm from "mvm-sdk";
export const greet = mvm.app({ name: "hello-app", source: mvm.localPath("."), image: mvm.python_image({ python: "3.12" }), resources: mvm.resources({ cpu: 1, memory_mb: 256 }), entrypoint: mvm.entrypoint_function({ module: "app", function: "greet", primary: true, }), before_start: "export HELLO_BANNER='hi'",})((name: string): string => `hello ${name}`);Build, run, invoke:
mvmctl compile examples/node/hello-app/app.ts --out /tmp/hello-appmvmctl build /tmp/hello-appmvmctl up hello-appmvmctl invoke hello-app --input name='ari'Helper allowlist
Section titled “Helper allowlist”Both SDKs ship a closed set of helpers the static parser also recognizes. Anything else in decorator kwarg position is rejected:
| Helper | Returns | Notes |
|---|---|---|
python_image(python=) | Image::NixPackages | python="3.12" → python312 nix attribute |
node_image(node=) | Image::NixPackages | node="22" → nodejs_22 nix attribute |
nix_packages([...]) | Image::NixPackages | direct passthrough |
resources(...) | Resources | cpu/memory_mb/rootfs_size_mb |
network(mode=, ports=) | Network | none | bridge | host |
secret(name, var=) | EnvValue::SecretRef | host-mediated secret ref; guest sees opaque token only |
literal(value) | EnvValue::Literal | parity with secret(...) |
hook(cmd) | HookCmd | str → Shell; list → Argv |
Lifecycle hooks
Section titled “Lifecycle hooks”Four phases. Each accepts a string (shell line), a list of strings
(argv), a single mvm.hook(...), or a list of any of those.
| Phase | Runs |
|---|---|
before_build | Inside the builder VM, after deps install and before the rootfs snapshot. |
before_start | At every microVM boot, before the entrypoint dispatch. |
after_start | Readiness probe — polled to exit-0 before the agent accepts mvmctl invoke. |
before_stop | At shutdown, best-effort. |
Addons (when attached via addons=[...]) contribute their own hooks;
the compiler merges them into the rootfs before the consuming app’s
hooks, in attachment order. The result lands in launch.json for the
Nix factory to bake into /etc/mvm/hooks/<phase>.sh.
Build artifacts
Section titled “Build artifacts”Use mvmctl compile to produce local build artifacts that can be reviewed,
signed, built, and launched through the normal mvm workflow. Treat packaging
and rollout as a separate admission step, not as part of declaration parsing.
Secret semantics
Section titled “Secret semantics”mvm.secret(...) names a managed secret reference, not a raw secret
value.
- The guest sees a normal env var name and an opaque token value.
- The raw secret stays on the host side.
- Request-time release is supported only on host-mediated outbound
surfaces such as
mvm.web_fetchandmvm.web_search. - Guest HTTPS CONNECT egress is not a managed-secret substitution path.
TypeScript runner
Section titled “TypeScript runner”mvmctl compile app.ts walks the AST statically for mvm.app({...})
calls and does not run user code. When a .ts script uses the
record-mode Sandbox API instead (no mvm.app decorator), mvmctl compile falls back to auto-run: it spawns the script on the host
with MVM_SDK_MODE=record set, and the SDK’s atexit hook writes a
recording JSON that the CLI then lowers into a Workload.
That auto-run path needs a TypeScript-aware runner — plain node
cannot execute .ts in mvm’s supported Node range. Three runners
are supported, in priority order: tsx, bun, deno.
Install one
Section titled “Install one”| OS | Recipe |
|---|---|
| macOS (Homebrew) | brew install tsx or brew install oven-sh/bun/bun or brew install deno |
| Any (npm) | npm install -g tsx |
| Any (pnpm) | pnpm add -g tsx |
| Any (yarn) | yarn global add tsx |
| Any (bun) | curl -fsSL https://bun.sh/install | bash |
| Any (deno) | curl -fsSL https://deno.land/install.sh | sh |
Prefer the project-local install if you want a reproducible lockfile- pinned runner:
npm install --save-dev tsxThat puts the binary at ./node_modules/.bin/tsx, and mvmctl compile picks it up automatically — see the resolution order below.
Resolution order
Section titled “Resolution order”mvmctl compile resolves the runner in this order, picking the
first hit:
MVM_TSX=<path>env override — explicit pin (any binary)../node_modules/.bin/tsx— cwd-relative, lockfile-pinned../node_modules/.bin/bun../node_modules/.bin/deno.tsxonPATH.bunonPATH.denoonPATH.
If nothing resolves, the command fails with the install hint
shown above. mvmctl doctor surfaces the same hint in its
Tools section — run it after a fresh checkout to confirm
your host is wired up.