Skip to content

VM

createVM resolves with a UserspaceVM (mode: "userspace") or a SystemVM (mode: "system"). Both extend VMBase, which has every member on this page except spawnJS, which only UserspaceVM has. VM is the union of the two. All types are exported from @oxidevm/sdk.

Member Type Meaning
mode "userspace" | "system" The mode the VM runs in.
image ImageInfo The image actually running. See mode and image.
capabilities VMCapabilities Optional features this VM supports. See capabilities.
workspace Workspace Files under /workspace. See Workspace.
exec(spec) Promise<ExecResult> Runs a command to completion and collects its output. See exec.
spawn(spec) Promise<ProcessHandle> or Promise<TerminalHandle> Starts a process with pipes, or on a terminal when spec.terminal is set. See spawn.
exposePort(port, options?) Promise<PreviewHandle> Waits for a guest server on port and opens a preview route to it. See Previews.
listeningPorts(options?) Promise<readonly ListeningPort[]> The ports guest programs listen on now.
watchListeningPorts(listener, options) Promise<void> Reports listening ports now and after every change.
network VMNetwork Guest internet access through a relay. See network.
start VMStart How this VM started: cold, or resumed from a restore point. See start.
snapshots VMSnapshots Restore points and backups. See snapshots.
suspend(options) Promise<SnapshotInfo> Saves a restore point, then disposes the VM. See suspend.
checkForUpdate(options?) Promise<ImageInfo | null> Checks whether the configured image differs from the running one.
prepareImage(options?) Promise<void> Downloads and verifies missing or damaged parts of the running image.
allowWorkspaceHandoff(listener, options) Promise<void> Lets another tab take over this workspace. See allowWorkspaceHandoff.
dispose() Promise<void> Stops the VM and releases everything it holds. See dispose.
spawnJS(spec) Promise<ProcessHandle> or Promise<TerminalHandle> Userspace only: runs a trusted JavaScript worker as a Linux process. See spawnJS.

After dispose(), or once suspend() has resolved, the VM is gone: its workspace, process, preview, image and handoff methods fail with PROTOCOL_ERROR. Calling dispose() again is safe.

mode is the mode you passed to createVM. image describes the image actually running:

Field Type Meaning
id string The image ID from its recipe.
version string The image version from its recipe.
mode "userspace" | "system" Always equal to vm.mode.
manifestSHA256 SHA256 SHA-256 of the exact signed manifest, as 64 lowercase hex digits.
cpuProfile "x86-64-v2" | "x86-64-v3" The x86-64 CPU profile the image declares.
signatureVerified true The manifest signature was verified with the trusted key.
keyId string The ID of that key.

vm.image follows a restore that changes the running image, including a restore to an older image.

Field Type Meaning
previews FeatureSupport { available: true } when createVM received preview, otherwise { available: false, reason: "not-configured" }.
snapshots FeatureSupport Whether restore points work in this VM. Check it before you show restore controls.
maximumVcpus 1 | 2 | 3 | 4 The most virtual CPUs this VM can use; currently 1.
crossOriginIsolated boolean Whether the page is cross-origin isolated.
FeatureSupport
type FeatureSupport =
| { available: true }
| {
available: false;
reason: "mode-unsupported" | "image-unsupported" | "browser-unsupported" | "release-gate-open" | "not-configured";
};

Runs a command, waits for it to exit and returns everything it printed.

Field Type Default Meaning
command string required An absolute path, a command name or a relative path. Not empty, no NUL. See command lookup.
args readonly string[] [] At most 4,096 arguments, each passed as its own string. No shell evaluates them.
cwd string "/workspace" The working directory, an absolute guest path.
env Readonly<Record<string, string>> {} At most 4,096 variables, names matching [A-Za-z_][A-Za-z0-9_]*, laid over the defaults.
stdin Uint8Array none Bytes written to standard input before it is closed.
maxOutputBytes number 1048576 (1 MiB) Limit for stdout and stderr together, an integer from 1 to 67,108,864 (64 MiB).
signal AbortSignal none Stops the command.

It resolves with ExecResult:

ExecResult
interface ExecResult {
stdout: Uint8Array;
stderr: Uint8Array;
exit: ProcessExit;
}
  • Standard input is closed right after stdin is written, or at once without stdin. Both output streams are read concurrently.
  • A non-zero exit code or a signal is an ordinary result in exit, not a rejection.
  • When the output passes maxOutputBytes, the process group is killed and exec rejects with QUOTA_EXCEEDED and detail.maxOutputBytes.
  • When signal aborts, the process group is killed, its cleanup is awaited and exec rejects with ABORTED.
  • If the process can no longer be reached during that cleanup, the rejection carries detail.cleanupFailed: true rather than a made-up exit.

Use spawn and stream the output for installs, builds, servers and other long or verbose commands.

node-version.ts
import type { VM } from "@oxidevm/sdk";
export async function nodeVersion(vm: VM): Promise<string> {
const result = await vm.exec({ command: "node", args: ["--version"] });
if (result.exit.kind !== "exited" || result.exit.code !== 0) {
throw new Error(new TextDecoder().decode(result.stderr));
}
return new TextDecoder().decode(result.stdout).trim();
}

Every process starts with PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin and HOME=/workspace. A value in env replaces the default, including an explicit empty string.

An absolute command runs that executable directly. A command name or a relative path is resolved by the image’s own /bin/sh with the PATH in effect, so these forms need /bin/sh in the image; the arguments still pass through unchanged. In Userspace, a name that is not found exits with code 127. To run shell syntax, start the shell yourself: { command: "/bin/sh", args: ["-c", "ls -la | head"] }.

spawn
spawn(spec: TerminalSpawnSpec): Promise<TerminalHandle>;
spawn(spec: PipeSpawnSpec): Promise<ProcessHandle>;

PipeSpawnSpec takes command, args, cwd and env as for exec, plus signal. TerminalSpawnSpec adds terminal: { columns: number; rows: number }, integers from 1 to 1,000, and runs the process on a pseudo-terminal.

  • spawn resolves once the process runs; it does not wait for the exit.
  • signal cancels creation only. If it aborts while the process starts, the process group is killed and spawn rejects with ABORTED. After spawn resolved, stop the process with kill.
  • Read the output (both stdout and stderr of a pipe process) and close stdin when there is no more input. A process whose output nobody reads can stall.
run-streaming.ts
import type { ProcessExit, VM } from "@oxidevm/sdk";
export async function runStreaming(
vm: VM, command: string, args: readonly string[], log: (text: string) => void,
): Promise<ProcessExit> {
const child = await vm.spawn({ command, args });
const sink = () => {
const decoder = new TextDecoder();
return new WritableStream<Uint8Array>({ write: (bytes) => log(decoder.decode(bytes, { stream: true })) });
};
const [exit] = await Promise.all([
child.exit,
child.stdout.pipeTo(sink()),
child.stderr.pipeTo(sink()),
child.stdin.close(),
]);
return exit;
}
Member Type Meaning
kind "process" Discriminant.
pid ProcessId The guest process ID, a positive integer.
stdin WritableStream<Uint8Array> Standard input.
stdout ReadableStream<Uint8Array> Standard output.
stderr ReadableStream<Uint8Array> Standard error.
exit Promise<ProcessExit> Resolves with the actual exit.
kill(signal?, options?) Promise<void> Sends a Linux signal. See kill.
snapshotKey string | undefined Equals the key of the matching ResumedTerminal after a restore. Keep it in app data to restore tab names and order.
Member Type Meaning
kind "terminal" Discriminant.
pid, stdin, exit, kill, snapshotKey see ProcessHandle The same members as on a pipe process.
output ReadableStream<Uint8Array> Everything the terminal prints, standard output and standard error together.
resize(size) Promise<void> Sets { columns, rows }, integers from 1 to 1,000.
interrupt() Promise<void> Sends Ctrl+C to the terminal’s foreground process group. The shell stays usable when the guest program allows it.

A terminal handle has no stdout or stderr. Write key presses to stdin as bytes, one write at a time. After the process exited, resize and interrupt fail with PROTOCOL_ERROR; an image whose guest integration lacks them fails with FEATURE_NOT_AVAILABLE and detail.reason: "image-unsupported". Closing your terminal view does not stop the shell; kill it when the tab closes. See Terminals.

kill
kill(signal?: LinuxSignal, options?: { scope?: "process" | "process-group" }): Promise<void>;
  • The default signal is "SIGTERM" and the default scope is "process". "process-group" signals the process’s whole process group.
  • A signal sent after the exit was confirmed does nothing and can never reach another process that reused the PID.
  • An unknown signal name or scope fails with INVALID_ARGUMENT.
LinuxSignal
type LinuxSignal =
| "SIGHUP" | "SIGINT" | "SIGQUIT" | "SIGILL" | "SIGTRAP" | "SIGABRT" | "SIGBUS" | "SIGFPE"
| "SIGKILL" | "SIGUSR1" | "SIGSEGV" | "SIGUSR2" | "SIGPIPE" | "SIGALRM" | "SIGTERM" | "SIGSTKFLT"
| "SIGCHLD" | "SIGCONT" | "SIGSTOP" | "SIGTSTP" | "SIGTTIN" | "SIGTTOU" | "SIGURG" | "SIGXCPU"
| "SIGXFSZ" | "SIGVTALRM" | "SIGPROF" | "SIGWINCH" | "SIGIO" | "SIGPWR" | "SIGSYS";
ProcessExit
type ProcessExit =
| { kind: "exited"; code: number }
| { kind: "signaled"; signal: LinuxSignal | null; signalNumber: number };

signal is null for a signal without a name in LinuxSignal, such as a real-time signal; signalNumber always holds the exact number.

exposePort
exposePort(port: number, options?: { signal?: AbortSignal }): Promise<PreviewHandle>;
  • port is an integer from 1 to 65535; anything else fails with INVALID_ARGUMENT.
  • It needs preview in the createVM options, otherwise it fails with FEATURE_NOT_AVAILABLE and detail.reason: "not-configured".
  • It waits until a guest program listens on port where a preview can reach it, then opens the route. It does not start a server.
  • Calls for the same port share one route until the last of their handles is closed.
  • Aborting signal stops the wait with ABORTED.
PreviewHandle member Type Meaning
port number The guest port.
url URL The preview address. Load it in an iframe or open it in a tab.
isolation "per-session" | "shared" The preview.isolation of the VM.
close() Promise<void> Closes the route. The guest server keeps running.

dispose() closes every route the VM opened. See Previews.

Resolves with at most 256 ListeningPort entries, lowest port first. options.signal cancels the read.

ListeningPort field Type Meaning
port number 1 to 65535.
address string The address as bound, for example "0.0.0.0", "::", "127.0.0.1" or "::1".
program string | null The command that started the listener, such as the command typed in a terminal, at most 256 bytes of UTF-8; null when unknown.
previewable boolean Whether a preview can reach this listener.

Calls listener with the current list, then again each time the list changes. The returned promise resolves when options.signal aborts and rejects when reading the ports fails.

watch-ports.ts
import type { VM } from "@oxidevm/sdk";
export function watchPorts(vm: VM, signal: AbortSignal): Promise<void> {
return vm.watchListeningPorts((ports) => {
console.log(ports.filter((entry) => entry.previewable).map((entry) => entry.port));
}, { signal });
}

vm.network is a VMNetwork, the same object for the VM’s whole lifetime.

Member Type Meaning
state NetworkState The current relay state.
connect(connection, options?) Promise<void> Connects through a relay with RelayConnectionOptions; options.signal cancels. Resolves once the relay session and the transport are ready. On failure, internet access stays off.
disconnect() Promise<void> Revokes internet access. Safe to call repeatedly. Files, processes, terminals and previews keep working.
watch(onChange, options) Promise<void> Reports the current state at once, then every change. Resolves when options.signal aborts or the VM is disposed; rejects if onChange throws.

connect fails with RELAY_ERROR, PERMISSION_DENIED, QUOTA_EXCEEDED or PROTOCOL_ERROR, and detail.reason names the relay failure. Invalid connection options fail with INVALID_ARGUMENT.

NetworkState kind Other fields Meaning
"off" none No internet access.
"discovering" none Looking for the user’s personal relay.
"awaiting-approval" code, origin, workspaceId, workspaceName, expiresAt Waiting for the user to approve the six-digit code in their personal relay.
"connecting" relay: "personal" | "server" Connecting.
"connected" relay, expiresAt Internet access is on.
"failed" reason: RelayFailureReason The last connection attempt failed.

Restoring a restore point, suspend and dispose revoke relay access, and restore points and backups never keep it. See Networking.

VMStart
type VMStart =
| {
kind: "booted";
reason: ManagedBootReason;
skipped?: SnapshotInfo;
shipped?: { reason: ShippedSkipReason };
update?: { id: string; version: string };
workspaceDetails?: UserspaceWorkspaceStart;
}
| {
kind: "resumed";
snapshot: SnapshotInfo;
appData: Uint8Array | null;
terminals: readonly ResumedTerminal[];
newerFilesDropped: boolean;
workspace: "in-sync" | "reconciled" | "conflict-preserved";
skipped?: SnapshotInfo;
update?: { id: string; version: string };
workspaceDetails?: UserspaceWorkspaceStart;
};
Field Meaning
kind "booted": Linux started cold. "resumed": it continued from a restore point.
reason Why the start was cold. See the table below.
skipped A restore point that was tried and could not resume.
shipped Why the requested shippedSnapshot prepared start did not open.
update The configured image, when the workspace keeps running another image. Start with restorePoint: "update-image" to move to it.
snapshot The restore point that resumed.
appData The bytes stored with snapshots.setAppData for that save, or null.
terminals Processes the restore point brought back. See ResumedTerminal below.
newerFilesDropped The workspace held files newer than the restore point. restorePoint: "pre-resume" keeps them.
workspace How /workspace relates to file changes made since the save.
workspaceDetails Userspace: how the saved /workspace met this image.
ManagedBootReason Meaning
"first-start" No restore point exists yet.
"restart" A cold start was requested: restorePoint: "none" or "pre-resume".
"fresh-image" restorePoint: "fresh-image" was requested.
"update-image" restorePoint: "update-image" was requested.
"no-save" Restore points exist, but none matches where the VM left off: it last booted cold.
"incompatible" The restore point is from another format, image or machine shape, or its image is gone.
"damaged" Every candidate restore point was damaged.
"no-space" The browser had no room to resume.
"restore-failed" Restoring failed for another reason.
"agent-unavailable" The restored guest agent did not answer.
ShippedSkipReason Meaning
"untrusted" No trusted key was given, or the manifest is signed by another key.
"signature-invalid" The signature does not verify.
"newer", "older" It needs a newer SDK, or comes from an older one.
"incompatible" It belongs to another image or does not fit this VM.
"damaged" Its data is damaged.
"unavailable" It could not be downloaded.
"no-space" The browser had no room for it.
"restore-failed" Restoring it failed.
"agent-unavailable" Its guest agent did not answer.
ResumedTerminal
type ResumedTerminal =
| { source: "console"; key: "console"; screenTail: Uint8Array }
| {
source: "process";
key: string;
command: string;
screenTail: Uint8Array;
attach(options?: { signal?: AbortSignal }): Promise<ProcessHandle | TerminalHandle>;
};

screenTail holds the newest output saved with the process, at most 64 KiB. attach takes over a restored process: it replays the saved screen once, then streams what the process prints from then on. Handles from before a restore are retired; attach again.

Field Type Meaning
state "in-sync" | "reconciled" | "conflict-preserved" in-sync: nothing was saved, or it was saved on this image. reconciled: it was saved on another image and carried over cleanly. conflict-preserved: see conflicts and preserved.
conflicts readonly { path: string; reason: string }[] At most 256 /workspace paths that could not be carried over cleanly. reason is "changed-in-both" (the user’s version stayed), "base-unknown" (the earlier image is unknown here; the user’s version stayed) or "content-unavailable" (the user’s version named a file this browser no longer holds; the image’s version stayed).
omittedConflicts number Conflicts beyond the first 256.
preserved { id: string; reason: string } or absent The earlier save was kept byte for byte under preserved/<id>/ in the workspace’s overlay store. reason is "unreadable", "unreplayable", "base-unavailable" or "content-unavailable".

A damaged save never stops the VM from starting.

Member Returns Meaning
list(options?) Promise<readonly SnapshotInfo[]> The workspace’s restore points.
capture(options) Promise<SnapshotInfo> Saves a restore point. options: label (not blank, at most 256 UTF-8 bytes), kind? ("named" by default, "safety" or "automatic"), signal?.
restore(ref, options?) Promise<SnapshotRestoreResult> Returns to a restore point. By default it saves the current state as a safety point first (saveCurrentFirst: true). Resolves with { start, undo? }, where undo is that safety point. options also take onProgress and signal.
rename(ref, label, options?) Promise<SnapshotInfo> Renames a restore point.
delete(ref, options?) Promise<void> Deletes a restore point.
usage(options?) Promise<SnapshotUsage> { referencedBytes, reclaimableBytes, estimate, persisted }, with the browser’s storage estimate ({ usage, quota } or null).
setAppData(data) Promise<void> Stores up to 1 MiB of your own bytes (or null) with later restore points. They return as start.appData.
observe(listener, options) void Delivers SnapshotEvents, such as save-started, save-finished, save-failed, auto-save-paused and restore-progress, until options.signal aborts.
autoSave.configure(policy, options?) Promise<void> { enabled: boolean }. Automatic saves start disabled.
autoSave.hint(hint) void Tells automatic saving about the page: "hidden", "visible", "activity" or "files-changed".
export(ref, options?) Promise<BackupExportResult> Writes a backup to options.destination, such as a file handle from showSaveFilePicker(), and resolves with { kind: "written", bytes }; without a destination it resolves with { kind: "file", file, suggestedName }. options also take onProgress and signal.
inspect(file, options?) Promise<BackupInspection> Reads a backup Blob without importing it.
import(file, options?) Promise<SnapshotInfo> Adds a backup Blob as a restore point.

ref is a SnapshotRef, { id: string; createdAt: string }. A restore point from the other mode fails with FEATURE_NOT_AVAILABLE and detail.reason: "runtime-mismatch". While a JavaScript process from spawnJS or acceleration runs, capture fails with FEATURE_NOT_AVAILABLE and detail.reason: "js-process-active".

SnapshotInfo field Type Meaning
mode "system" | "userspace" The mode it belongs to.
ref SnapshotRef Its identity.
kind "automatic" | "named" | "safety" | "imported" | "shipped" An automatic save, a named point, a safety point taken before a restore, reset or update, an imported backup, or a prepared start.
label string | undefined Its name.
capturedAtUnixMs number When it was captured.
profile { id: string; version: string } | null The managed image it runs, or null.
bytes { ram: number; disk: number; unique: number } Its memory, its changed disk content, and the bytes only it keeps (about what deleting it frees).
resumable true or a reason true, or why it cannot resume here: "older-format", "newer-format", "profile-mismatch", "machine-mismatch", "runtime-mismatch", "base-missing" or "corrupt".

See Restore points.

suspend
suspend(options: { label: string; signal?: AbortSignal }): Promise<SnapshotInfo>;

Captures a consistent restore point, releases the VM’s resources and its workspace lease, and disposes the VM before it resolves. Open the workspace again with restorePoint: "latest" to continue. If suspending fails, the VM stays usable whenever it can be kept. Like capture, it fails with js-process-active while a JavaScript process runs.

Member Behavior
checkForUpdate(options?) Verifies the image configured for this VM (the image option, or the default image). Resolves with its ImageInfo when it differs from the running image, otherwise null. It never installs anything; start with restorePoint: "update-image" to move to the offered image. vm.start.update keeps the offer known at startup.
prepareImage(options?) Downloads and verifies whatever is missing or damaged in the running image, without restarting the guest or changing the image version. Processes and terminals keep running. Verified progress is kept when options.signal aborts.
allowWorkspaceHandoff
allowWorkspaceHandoff(
listener: (moved: WorkspaceMovedAway) => void,
options: { signal: AbortSignal },
): Promise<void>;

Opts in to handing the workspace to another tab. When another tab calls requestWorkspaceHandoff for this VM’s workspace ID, this VM saves its state when it can, is disposed, and then calls listener with { saved: boolean }. The returned promise resolves when options.signal aborts or the VM is disposed; aborting stops accepting requests.

The requesting tab calls requestWorkspaceHandoff from the package root before it creates its own VM:

take-over.ts
import { createVM, requestWorkspaceHandoff } from "@oxidevm/sdk";
const outcome = await requestWorkspaceHandoff("my-project", { timeoutMs: 10_000 });
if (outcome === "no-response") throw new Error("Another tab still holds this workspace");
const vm = await createVM({ mode: "system", workspaceId: "my-project", restorePoint: "latest" });
requestWorkspaceHandoff option Type Meaning
timeoutMs number How long to wait for the holding tab to answer, from 1 to 600,000 ms; default 10,000.
signal AbortSignal Cancels the request.

It resolves with "released" once the workspace is free, at once when no VM holds it, or with "no-response" when no tab answered in time. If the holder’s save fails, the request rejects with the holder’s error and the holder keeps running.

Stops the VM’s processes, closes its preview routes, revokes relay access, saves a durable workspace’s files, releases the workspace lease and frees the VM’s workers and assets. Calling it again returns the same promise. Always await it, and await it before you create another VM with the same workspaceId.

Userspace only. Runs a trusted JavaScript module worker as a Linux process: it gets a PID, file descriptors, credentials and signals like any other process, and reaches files, sockets and child processes through the Linux side.

spawnJS
spawnJS(spec: TerminalJSProcessSpec): Promise<TerminalHandle>;
spawnJS(spec: PipeJSProcessSpec): Promise<ProcessHandle>;
Field Type Default Meaning
worker URL required The worker module. It must be on the page’s origin, and the page must be cross-origin isolated; otherwise FEATURE_NOT_AVAILABLE.
args readonly string[] [] Arguments, available to the worker.
cwd string "/workspace" Working directory.
env Readonly<Record<string, string>> {} Environment, handled as for spawn.
terminal { columns: number; rows: number } none Runs it on a pseudo-terminal and returns a TerminalHandle.
signal AbortSignal none Cancels creation only, as for spawn.

The VM must use the default initialUser: "user"; other credential profiles fail. The worker calls serveProcess(handler) from @oxidevm/sdk/js-process. The handler returns an exit code from 0 to 255, or nothing for 0. A failure to construct or run the worker, or of its transport, exits with code 125, and the process is never retried as a Linux program. At most four JavaScript processes run at once. While one runs, snapshots.capture and suspend fail with js-process-active.

run-worker.ts
import type { UserspaceVM } from "@oxidevm/sdk";
export async function runWorker(vm: UserspaceVM, worker: URL): Promise<string> {
const child = await vm.spawnJS({ worker, args: ["hello"] });
const [stdout, stderr, exit] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exit,
child.stdin.close(),
]);
if (exit.kind !== "exited" || exit.code !== 0) throw new Error(`worker failed: ${stderr}`);
return stdout;
}