Skip to content

TypeScript SDK

The TypeScript SDK at sdks/typescript/ exposes three API surfaces from the same mvmforge package:

  • Authoring — higher-order function calls and factories that register a workload to be emitted as canonical IR.
  • Runtime — host-side f.remote(...) and session(...) for calling function-entrypoint workloads. Dev-only by design (per ADR-0010 §2).
  • Sandbox — typed lifecycle handles (Sandbox, Process, …) over the local mvm sandbox primitives.

Uses keyword-style higher-order calls per ADR-0003 §3 — the TS analog of Python’s keyword-argument decorator.

Install

Available on npm: mvm-sdk.

Terminal window
npm install mvm-sdk
# or: pnpm add mvm-sdk | yarn add mvm-sdk

Optional peer deps (only as needed):

Terminal window
npm install zod zod-to-json-schema # for mv.zodSchema(...)
npm install @msgpack/msgpack # for format: "msgpack"

The SDK runs on Node 22.6+ via --experimental-strip-types — no compile step. Node 22.18+ enables type stripping by default. For local development against this repo (cloned), use cd sdks/typescript && pnpm install instead.


Authoring

Two workload shapes share the same workload({...}) + app({...}) registration. For function entrypoints, func({...}, fn) is the ergonomic shortcut.

Command entrypoint

import * as mv from "mvm-sdk";
mv.workload({ id: "my-service" });
mv.app({
name: "my-service",
source: mv.localPath("src"),
image: mv.nixPackages(["python312", "curl"]),
entrypoint: mv.entrypoint({ command: ["python", "-m", "my_service"] }),
resources: mv.resources({ cpuCores: 2, memoryMb: 512, rootfsSizeMb: 1024 }),
});

Function entrypoint (short form)

import * as mv from "mvm-sdk";
export const add = mv.func(
{
name: "adder",
image: mv.nixPackages(["nodejs_22"]),
resources: mv.resources({ cpuCores: 1, memoryMb: 256, rootfsSizeMb: 512 }),
module: "adder",
},
function add(a: number, b: number) {
return a + b;
},
);
add(2, 3); // → 5 (local)
await add.remote(2, 3); // → 5 (dispatched in the VM)

mv.func({...}, fn) registers the workload + app + function entrypoint in one call and returns a RemoteFunction<F>. language defaults to "node"; override for cross-language manifest authoring.

Typed parameters and return types populate argsSchema / returnSchema automatically — no mv.zodSchema(...) needed for the common case (TypeScript number → JSON Schema number; use bigint for integer). See Payload schemas for the supported type table and when to reach for zod.

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 calls contribute additional entrypoints rather than registering a new app.

const add = mv.func({ name: "math-svc", module: "math", primary: true })(
function add(a: number, b: number) {
return a + b;
},
);
const mul = mv.func({ name: "math-svc", module: "math" })(
function mul(a: number, b: number) {
return a * b;
},
);

The first call carries the app-level config (image, resources, source, dependencies, network, dependsOn, mounts, env). Subsequent calls 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 call 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.

Top-level functions

FunctionPurpose
workload({ id })Declares the workload identity. Once per emit.
app({...})Registers an app. Long-form companion to func().
func({...}, fn)Workload + app + function entrypoint shortcut. Returns RemoteFunction<F>.
localPath(path, opts?)Source tree, relative to the manifest.
nixPackages(packages)Image declaration.
entrypoint({...})Command-style entrypoint.
entrypointFunction({ module, function, language?, format?, workingDir?, env?, argsSchema?, returnSchema?, extraImports? })Function-style entrypoint. language defaults to "node".
resources({ cpuCores, memoryMb, rootfsSizeMb })Resource sizing.
pythonDeps({ lockfile, tool? }) / nodeDeps({ lockfile, tool? }) / noDeps()Dependency declarations.
network({ mode?, ports?, egress?, peers?, dns? })Network posture. host mode rejected for function workloads.
egress(allowlist) / hostPort(host, port) / dnsNone() / dnsSystem() / dnsResolver(host, port?)Granular network grants.
zodSchema(schema)Convert a Zod schema to JSON Schema for argsSchema= / returnSchema= (requires zod + zod-to-json-schema).
canonicalize(value) / emitJson() / reset()Used by the emit subprocess; users normally don’t call these.

language field

Per ADR-0010 §4, entrypointFunction({ language }) and func({ language }) default to "node" — 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.

For language: "wasm": module is the relative path of a .wasm (WASI Preview 1) file inside the bundled source tree; function is captured for documentation but unused (wasmtime run invokes _start). Unlocks any compile-to-WASM toolchain (Rust + wasm32-wasi, TinyGo, Zig, AssemblyScript, etc.) without a per-language factory.


Runtime

mv.func({...}, fn) returns a RemoteFunction<F>:

export interface RemoteFunction<F extends (...args: any[]) => any> {
(...args: Parameters<F>): ReturnType<F>;
remote(...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>;
readonly local: F;
readonly workloadId: string;
readonly format: SerializationFormat;
}
AttributeNotes
add(2, 3)Calls the local function.
add.remote(2, 3)Async. Encodes args, shells out to mvmctl invoke, decodes the return.
add.localThe undecorated local function.
add.workloadId / add.formatWorkload id + wire format.

session(workloadId, body)

Async function holding a single VM warm across calls inside body:

await mv.session("adder", async () => {
await add.remote(2, 3);
await add.remote(4, 5);
});

Session id propagates via Node AsyncLocalStorage. Torn down on exit even if body rejected.

Errors

ClassWhen
RemoteErrorUser code raised. .kind, .errorId (stable), .remoteMessage.
MvmTransportErrorTransport failure.
MsgpackUnavailableformat: "msgpack" but @msgpack/msgpack not installed.
SecretInArgErrorStrict-mode counterpart to the secret-name heuristic.
EmittingContextErrorLayer-3 call fired during mvmforge emit (MVMFORGE_EMITTING=1 is set). Per ADR-0010 §2.

Transport caps + timeouts

Same env vars as the Python SDK — see Environment variables.

Dev / production posture

The runtime SDK is dev-only by design. Production microVMs are observed via mvmctl logs and output streams; no host-side .remote() calls. Two safety gates: MVMFORGE_EMITTING=1 (build-time) and the wrapper’s runtime mode = "prod" | "dev" (in-VM).


Sandbox

Mirror of the Python sandbox surface (plan-0010 phase A):

import * as mv from "mvm-sdk";
await using sb = await mv.Sandbox.create({ name: "sandbox-1" });
const info = await sb.info();
const proc = await sb.spawn(["node", "-e", "console.log('hello')"]);
await proc.awaitExit({ timeoutMs: 5000 });
await sb.writeFile("/tmp/hello", Buffer.from("hi"));
const entries = await sb.listDir("/tmp");

Surfaces:

SymbolPurpose
SandboxLifecycle.
Process / type ProcessInfoSpawned process handles.
type FileEntry / type FileStatFilesystem types.
type SandboxCreateOpts / type SandboxInfo / type SandboxMetrics / type SnapshotEntry / type UpOptsSandbox config + introspection types.
SandboxError / SandboxNotFoundFailure classes.
argvBuildersArgv-corpus shared with Python for parity tests.

Dev-only — same posture as the runtime SDK.


The emit subprocess contract

Per ADR-0002:

node --experimental-strip-types <ts-main> <entry.ts>
with MVMFORGE_IR_OUT=<output-path>
MVMFORGE_EMITTING=1

<ts-main> defaults to sdks/typescript/src/main.ts; override with MVMFORGE_TS_MAIN. The emitter imports <entry.ts>, calls emitJson(), writes to MVMFORGE_IR_OUT. Exit codes 0 / 1 / 2.

Terminal window
MVMFORGE_IR_OUT=/tmp/ir.json node --experimental-strip-types \
sdks/typescript/src/main.ts /path/to/app.ts

Internal layout

sdks/typescript/
package.json tsconfig.json
src/
index.ts # public DSL (authoring + runtime + sandbox re-exports)
main.ts # subprocess emitter
_remote.ts # RemoteFunction + transport + EmittingContextError
_session.ts # session() helper (AsyncLocalStorage)
_sandbox.ts # Sandbox surface
_subprocess.ts # capped + timed mvmctl wrapper
ir/workload.ts # generated lower layer (json-schema-to-typescript)
tests/
*.test.ts # node:test runner (no test framework dep)

The exports of index.ts are stable. src/ir/workload.ts and the underscore modules are internal — pin exact mvmforge versions if you import from them. A future ESLint no-restricted-imports rule will block reaching past the public surface.

Regenerating the lower layer

Terminal window
just sdk-ts-gen # writes sdks/typescript/src/ir/workload.ts
just sdk-ts-check # asserts the committed file matches fresh regeneration

just sdk-ts-check is wired into just ci and fails on drift.

Tests

Terminal window
just sdk-ts-test

94 tests use Node’s built-in node:test runner — no test framework dep. Coverage spans the DSL surface, the runtime transport (against a fake mvmctl), the emit-context guard, the secret-name heuristic, the sandbox surface, and the cross-language argv-corpus parity check shared with Python.

Cross-SDK conformance

The TypeScript SDK and Python SDK must produce byte-identical canonical IR for semantically equivalent source. The golden corpus at tests/corpus/ enforces it. just corpus-check runs every active SDK; when you change DSL behavior in one, change it in the other and add a corpus entry that exercises the change.