docs/sdk/python/sandbox.mdx
Create and control a microVM sandbox: boot it from an image, run commands, stream logs and metrics, then shut it down. See Overview for configuration examples and Lifecycle for state management.
For local runtime installation and verification, see Runtime setup.
<Tooltip tip="On microsandbox cloud this is false; the cloud worker owns the sandbox process, not your handle."><span className="msb-badge-note">On cloud <Icon icon="circle-info" size={11} /></span></Tooltip>
@property
async def owns_lifecycle(self) -> bool
Whether this handle owns the sandbox lifecycle. A sandbox returned directly by create() or start() owns lifecycle, including when created with detached=True, until you call detach(). Handles upgraded via SandboxHandle.connect() do not own lifecycle. This is an async property; use await sb.owns_lifecycle.
@property
def fs(self) -> SandboxFsOps
await sb.fs.write("/tmp/hello.txt", b"hi")
Get a filesystem handle for reading and writing files inside the running sandbox. This is a synchronous property; use sb.fs (no await). See Filesystem for API details.
@staticmethod
async def create(name: str, **kwargs) -> Sandbox
async with await Sandbox.create("my-sandbox", image="alpine") as sb:
output = await sb.shell("echo hello")
print(output.stdout_text)
# sandbox is automatically killed and removed on exit
Create and boot a sandbox. Keyword arguments provide individual config fields; see SandboxConfig for the full set. Pulls the image if needed, boots the VM, starts the guest agent, and waits until it is ready to accept commands. Sandbox names must be non-empty and no longer than 128 UTF-8 bytes.
The returned Sandbox is an async context manager. Use async with to guarantee cleanup; on exit the sandbox is killed and its persisted state removed.
@staticmethod
def create_with_progress(name: str, **kwargs) -> PullSession
session = Sandbox.create_with_progress("my-sandbox", image="ubuntu:latest")
async with session:
async for event in session.progress:
print(event.event_type)
sb = await session.result()
Same parameters as create() but returns a PullSession that lets you track image pull progress before the sandbox is ready. This method is synchronous (not awaitable); the async work happens through the PullSession.
@staticmethod
async def start(name: str, *, detached: bool = False) -> Sandbox
sb = await Sandbox.start("api")
Restart a previously stopped sandbox. The VM reboots using the persisted configuration.
<p className="msb-label">Parameters</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><code>name</code><span className="msb-type">str</span></div> <div className="msb-param-desc">Name of a stopped sandbox, up to 128 UTF-8 bytes.</div> </div> <div className="msb-param"> <div className="msb-param-key"><code>detached</code><span className="msb-type">bool</span></div> <div className="msb-param-desc">When <code>True</code>, the sandbox survives after your process exits. Default <code>False</code>.</div> </div> </div> <p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><a className="msb-type" href="#instance-methods">Sandbox</a></div> <div className="msb-param-desc">Running sandbox.</div> </div> </div>@staticmethod
async def get(name: str) -> SandboxHandle
handle = await Sandbox.get("api")
print(handle.status)
Get a handle to an existing sandbox (running or stopped). The handle provides status, configuration, and lifecycle control without requiring a full connection to the guest agent.
<p className="msb-label">Parameters</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><code>name</code><span className="msb-type">str</span></div> <div className="msb-param-desc">Sandbox name, up to 128 UTF-8 bytes.</div> </div> </div> <p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><a className="msb-type" href="#sandboxhandle">SandboxHandle</a></div> <div className="msb-param-desc">Handle with status and lifecycle control.</div> </div> </div>@staticmethod
async def list() -> SandboxPage
page = await Sandbox.list()
for h in page.sandboxes:
print(h.name, h.status)
Return the first page of sandboxes (running, stopped, and crashed), ordered newest first. The default page size is 20.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">SandboxPage</span></div> <div className="msb-param-desc">Handles in this page and an optional cursor for the next page.</div> </div> </div>@staticmethod
async def list_with(
*,
cursor: str | None = None,
limit: int | None = None,
labels: Mapping[str, str] | None = None,
) -> SandboxPage
page = await Sandbox.list_with(limit=50, labels={"role": "worker"})
if page.next_cursor is not None:
next_page = await Sandbox.list_with(
cursor=page.next_cursor,
limit=50,
labels={"role": "worker"},
)
Return a configured page of sandboxes. Label filters are applied before pagination and match every supplied key/value pair.
<p className="msb-label">Parameters</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><code>cursor</code><span className="msb-type">str | None</span></div> <div className="msb-param-desc">Opaque <code>next_cursor</code> from the preceding page.</div> </div> <div className="msb-param"> <div className="msb-param-key"><code>limit</code><span className="msb-type">int | None</span></div> <div className="msb-param-desc">Page size from 1 through 100. Defaults to 20.</div> </div> <div className="msb-param"> <div className="msb-param-key"><code>labels</code><span className="msb-type">Mapping[str, str] | None</span></div> <div className="msb-param-desc">Label key/value pairs to match. <code>None</code> returns every sandbox, like <a href="#sandbox-list">list()</a>.</div> </div> </div> <p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">SandboxPage</span></div> <div className="msb-param-desc">Matching handles and an optional cursor for the next page.</div> </div> </div>@staticmethod
async def remove(name: str) -> None
await Sandbox.remove("api")
Delete a stopped sandbox by name. See Remove for the exact local deletion scope and the external resources that are preserved. Fails if the sandbox is still running; stop it first.
<p className="msb-label">Parameters</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><code>name</code><span className="msb-type">str</span></div> <div className="msb-param-desc">Sandbox name, up to 128 UTF-8 bytes.</div> </div> </div> <p className="msb-member-group">Instance methods</p>Command execution (exec, exec_stream, shell, shell_stream) is documented on the Execution page; SSH (ssh) on the SSH page. The lifecycle, attach, metrics, and logs methods follow.
async def name(self) -> str
print(await sb.name())
Return the sandbox name.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">str</span></div> <div className="msb-param-desc">Sandbox name, up to 128 UTF-8 bytes.</div> </div> </div>async def attach(
self,
cmd: str,
args: list[str] | None = None,
*,
cwd: str | None = None,
user: str | None = None,
env: Mapping[str, str] | None = None,
detach_keys: str | None = None,
) -> int
code = await sb.attach("python", ["-i"])
Bridge your terminal directly to a process inside the sandbox for a fully interactive PTY session. Returns the process exit code once the session ends.
<p className="msb-label">Parameters</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><code>cmd</code><span className="msb-type">str</span></div> <div className="msb-param-desc">Command to run.</div> </div> <div className="msb-param"> <div className="msb-param-key"><code>args</code><span className="msb-type">list[str] | None</span></div> <div className="msb-param-desc">Command arguments.</div> </div> <div className="msb-param"> <div className="msb-param-key"><code>cwd</code><span className="msb-type">str | None</span></div> <div className="msb-param-desc">Working directory.</div> </div> <div className="msb-param"> <div className="msb-param-key"><code>user</code><span className="msb-type">str | None</span></div> <div className="msb-param-desc">Guest user.</div> </div> <div className="msb-param"> <div className="msb-param-key"><code>env</code><span className="msb-type">Mapping[str, str] | None</span></div> <div className="msb-param-desc">Environment variables.</div> </div> <div className="msb-param"> <div className="msb-param-key"><code>detach_keys</code><span className="msb-type">str | None</span></div> <div className="msb-param-desc">Custom detach key sequence.</div> </div> </div> <p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">int</span></div> <div className="msb-param-desc">Exit code of the process.</div> </div> </div>async def attach_shell(self) -> int
await sb.attach_shell()
Attach your terminal to the sandbox's default shell for an interactive session.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><span className="msb-type">int</span></div> <div className="msb-param-desc">Exit code.</div> </div> </div><Tooltip tip="Not available on microsandbox cloud."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
async def ping(self) -> SandboxPingResult
health = await sb.ping()
print(f"{health.name}: {health.latency_ms:.1f} ms")
Check that the running sandbox's guest agent is reachable without refreshing idle activity. This sends core.ping and waits for core.pong; it does not start stopped sandboxes and raises an error if the sandbox is not running or agentd cannot respond. After upgrading from a runtime that predates protocol generation 6, restart already-running sandboxes so the guest agent understands the message.
<Tooltip tip="Not available on microsandbox cloud."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
async def touch(self) -> SandboxTouchResult
keepalive = await sb.touch()
print(f"{keepalive.name}: {keepalive.activity_seq}")
Explicitly refresh the running sandbox's idle activity. This sends core.touch, receives core.touched, and advances the guest activity sequence used by the runtime idle-timeout monitor. It does not start stopped sandboxes and it does not bypass max_duration.
<Tooltip tip="modify is not available on microsandbox cloud; recreate the sandbox with the new configuration."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
async def modify(
self,
*,
cpus: int | None = None,
max_cpus: int | None = None,
memory: int | None = None,
max_memory: int | None = None,
root_disk_size: int | None = None,
env: Mapping[str, str] | None = None,
env_rm: list[str] | None = None,
labels: Mapping[str, str] | None = None,
labels_rm: list[str] | None = None,
workdir: str | None = None,
secrets: Mapping[str, SecretModifySpec] | None = None,
secrets_rm: list[str] | None = None,
policy: ModificationPolicy | None = None,
dry_run: bool = False,
) -> SandboxModificationPlan
from microsandbox import (
ModificationDisposition,
ModificationPolicy,
ResourceConvergenceState,
)
# Live resize: applies to the running VM when within the booted capacity
plan = await sb.modify(cpus=4, memory=4096)
for r in plan.get("resize_status", []):
if r["state"] is ResourceConvergenceState.APPLIED:
print(f'{r["resource"]}: {r["requested"]} -> {r["actual"]}')
# Preview a change without applying it
plan = await sb.modify(max_memory=16384, dry_run=True)
for change in plan["changes"]:
if change["disposition"] is ModificationDisposition.REQUIRES_RESTART:
print(f'{change["field"]} needs a restart')
# Make an env change active now by restarting
await sb.modify(env={"MODE": "prod"}, policy=ModificationPolicy.RESTART)
# Grow the managed OCI root disk offline and restart
await sb.modify(root_disk_size=8192, policy=ModificationPolicy.RESTART)
# Add or rotate a host-environment secret; restart if it is newly added
await sb.modify(
secrets={
"API_KEY": {
"env": "API_KEY",
"allowed_hosts": ["api.example.com"],
},
},
policy=ModificationPolicy.RESTART,
)
# Remove an existing secret
await sb.modify(secrets_rm=["OLD_API_KEY"])
Plan or apply a configuration change. The returned plan uses ModificationDisposition to classify when each change takes effect, and apply is all-or-nothing.
cpus and memory resize live within the max_cpus / max_memory ceilings; raising a ceiling requires a restart. root_disk_size changes are offline: managed and flat OCI root disks grow only, tmpfs root disks can change in either direction on the next boot, and user-supplied disk images are rejected. Env and workdir changes affect future execs only. On a stopped sandbox, changes are saved for the next boot.
Secret specs are keyed by stable secret name. Each SecretModifySpec selects at most one source—env, value, or store—and may also set placeholder and allowed_hosts; omitting a source updates only the other supplied fields. Plans expose only safe references and metadata; raw secret values never appear in a plan. Removal is explicit through secrets_rm.
The returned SandboxModificationPlan is a typed dictionary:
| Key | Type | Description |
|---|---|---|
sandbox | str | Sandbox being modified |
status | SandboxStatus | Status used for classification |
applied | bool | Whether the changes were applied; False with dry_run=True |
policy | ModificationPolicy | Policy used to produce the plan |
changes | list[ConfigPlannedChange | SecretPlannedChange] | Typed config or secret changes, discriminated by PlannedChangeKind |
conflicts | list[dict] | Conflicts (field + message) that must be resolved before the patch can apply |
warnings | list[dict] | Non-fatal warnings (field + message), e.g. the future-execs-only env caveat |
resize_status | list[ResourceResizeStatus] | Live resize outcomes after apply, using ResourceKind and ResourceConvergenceState. Omitted when no live resize ran |
A live CPU or memory resize can take a moment to settle. The new limits are enforced immediately, and resize_status reports when the sandbox has finished adjusting.
<Tooltip tip="Resource metrics are not available on microsandbox cloud; use an external monitoring system."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
async def metrics(self) -> SandboxMetrics
m = await sb.metrics()
print(f"cpu {m.cpu_percent:.1f}% · mem {m.memory_bytes // 1_048_576} MiB")
Get a point-in-time snapshot of the sandbox's resource usage: CPU, memory, disk I/O, network I/O, optional upper disk usage, and uptime.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><a className="msb-type" href="#sandboxmetrics">SandboxMetrics</a></div> <div className="msb-param-desc">Resource metrics.</div> </div> </div><Tooltip tip="Resource metrics are not available on microsandbox cloud; use an external monitoring system."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
async def metrics_stream(self, interval: float = 1.0) -> MetricsStream
stream = await sb.metrics_stream(1.0)
async for snapshot in stream:
print(f"{snapshot.cpu_percent:.1f}%")
Stream resource metrics at a regular interval. The returned MetricsStream supports both recv() and async for.
<Tooltip tip="Bounded log reads are not available on microsandbox cloud; follow live with log streaming and persist output externally."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
async def logs(
self,
tail: int | None = None,
since_ms: float | None = None,
until_ms: float | None = None,
sources: list[LogReadSource] | None = None,
) -> list[LogEntry]
import time
from microsandbox import LogReadSource, Sandbox
handle = await Sandbox.get("web")
# Default: all user-program output, regardless of pipe/pty mode
entries = await handle.logs()
for e in entries:
label = {"stdout": "OUT", "stderr": "ERR", "output": "PTY", "system": "SYS"}[e.source]
print(f"[{e.timestamp_ms / 1000:.3f}] {label} {e.session_id}: {e.text().rstrip()}")
# Filtered: last 50 entries from the past hour, including system lines
recent = await handle.logs(
tail=50,
since_ms=(time.time() - 3600) * 1000,
sources=[
LogReadSource.STDOUT,
LogReadSource.STDERR,
LogReadSource.OUTPUT,
LogReadSource.SYSTEM,
],
)
Read captured output from the sandbox's exec.log. Backed by an on-disk JSON Lines file the runtime writes via the relay tap. Works on running and stopped sandboxes alike; there is no protocol traffic. The same method is available on SandboxHandle for callers that don't want to start the sandbox first.
The default sources are STDOUT, STDERR, and OUTPUT (PTY-merged). Add LogReadSource.SYSTEM to include synthetic lifecycle markers and runtime/kernel diagnostic lines, or use LogReadSource.ALL as shorthand for all four. Timestamps are exposed as float ms since the Unix epoch (UTC) for parity with SandboxMetrics.timestamp_ms.
<Tooltip tip="On microsandbox cloud, log streaming is follow-only; set follow. Bounded, non-follow reads are not available."><span className="msb-badge-limited">Limited on cloud <Icon icon="circle-info" size={11} /></span></Tooltip>
async def log_stream(
self,
sources: list[LogReadSource] | None = None,
since_ms: float | None = None,
from_cursor: str | None = None,
until_ms: float | None = None,
follow: bool = False,
) -> LogStream
stream = await sb.log_stream(follow=True)
async for entry in stream:
print(entry.text().rstrip())
Stream captured log entries as a LogStream. With follow=True the stream stays open and yields new entries as they are written, like tail -f. Resume an earlier stream by passing the cursor of the last entry you saw as from_cursor. Also available on SandboxHandle.
async def stop(self, timeout: float | None = None) -> None
await sb.stop()
Gracefully shut down the sandbox and wait until stopped state is observed. Lets the sandbox finish writing any pending data to disk before it exits, so files written inside the sandbox aren't lost across a later restart. Waits up to ten seconds by default; pass timeout to override the graceful shutdown window before force-kill escalation.
async def request_stop(self) -> None
await sb.request_stop()
Request graceful shutdown and return once the request is sent, without waiting for stopped state. Pair with wait_until_stopped() when the caller needs to observe the terminal state.
<Tooltip tip="Not available on microsandbox cloud; use a graceful stop."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
async def kill(self, timeout: float | None = None) -> None
await sb.kill() # SIGKILL, no graceful shutdown
Force-terminate the sandbox and wait until stopped state is observed. No graceful shutdown; use when the sandbox is unresponsive. Pending writes that the workload hasn't fsync'd may be lost, same durability semantics as a sudden power loss on a physical machine. Prefer stop() for graceful shutdown that gives the workload a chance to flush.
<Tooltip tip="Not available on microsandbox cloud; use a graceful stop."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
async def request_kill(self) -> None
await sb.request_kill()
Request force termination and return once the signal is sent, without waiting for stopped state.
<Tooltip tip="Not available on microsandbox cloud; use a graceful stop."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
async def request_drain(self) -> None
await sb.request_drain()
Request a graceful drain and return once the request is sent. Existing commands run to completion, but new exec calls are rejected; the sandbox transitions to stopped when all in-flight commands finish. Useful for zero-downtime rotation of worker sandboxes. Use wait_until_stopped() when the caller needs stopped-state observation.
async def wait_until_stopped(self) -> SandboxStopResult
result = await sb.wait_until_stopped()
print(result.status, result.exit_code)
Block until the sandbox is observed in a terminal non-running state, without triggering a stop or kill request.
<p className="msb-label">Returns</p> <div className="msb-params"> <div className="msb-param"> <div className="msb-param-key"><a className="msb-type" href="#sandboxstopresult">SandboxStopResult</a></div> <div className="msb-param-desc">Terminal status and optional observed exit code.</div> </div> </div>async def detach(self) -> None
sb = await Sandbox.create("worker", image="python", detached=True)
await sb.detach() # keeps running in the background
Release the handle without stopping the sandbox. The sandbox continues running as a background process. Reconnect later with Sandbox.get().
Factory for pre-boot root filesystem patches.
@staticmethod
def text(path: str, content: str, *, mode: int | None = None, replace: bool = False) -> PatchConfig
from microsandbox import Patch, Sandbox
sb = await Sandbox.create(
"api",
image="python",
patches=[Patch.text("/etc/app.conf", "debug=1\n", mode=0o644)],
)
Write UTF-8 text content at path.
@staticmethod
def file(path: str, content: bytes, *, mode: int | None = None, replace: bool = False) -> PatchConfig
Write arbitrary bytes at path. Use text() for UTF-8 text.
@staticmethod
def append(path: str, content: str) -> PatchConfig
Append content to an existing file at path. If the file lives in a lower image layer, it's copied up first.
<Tooltip tip="On microsandbox cloud, host sources resolve against your organization's host volume, not the computer running the SDK or CLI."><span className="msb-badge-note">On cloud <Icon icon="circle-info" size={11} /></span></Tooltip>
@staticmethod
def mkdir(path: str, *, mode: int | None = None) -> PatchConfig
Create a directory at path. Idempotent: a no-op if the directory already exists.
@staticmethod
def remove(path: str) -> PatchConfig
Delete a file or directory at path. Idempotent: a no-op if the path doesn't exist.
<Tooltip tip="On microsandbox cloud, host sources resolve against your organization's host volume, not the computer running the SDK or CLI."><span className="msb-badge-note">On cloud <Icon icon="circle-info" size={11} /></span></Tooltip>
@staticmethod
def copy_file(src: str, dst: str, *, mode: int | None = None, replace: bool = False) -> PatchConfig
Copy a single host file at src into the guest rootfs at dst.
<Tooltip tip="On microsandbox cloud, host sources resolve against your organization's host volume, not the computer running the SDK or CLI."><span className="msb-badge-note">On cloud <Icon icon="circle-info" size={11} /></span></Tooltip>
@staticmethod
def copy_dir(src: str, dst: str, *, replace: bool = False) -> PatchConfig
Recursively copy a host directory at src into the guest rootfs at dst.
@staticmethod
def symlink(target: str, link: str, *, replace: bool = False) -> PatchConfig
Create a symlink at link pointing to target.
A metadata and lifecycle handle for an existing sandbox.
str
Sandbox name, up to 128 UTF-8 bytes
Current status
str
Raw JSON configuration
float \| None
Creation timestamp (ms since epoch)
float \| None
Last update timestamp (ms since epoch)
config()
Parsed configuration
<p className="msb-label">Returns</p>dict[str, Any]
refresh()
Re-fetch status and metadata, returning a fresh handle
<p className="msb-label">Returns</p>Awaitable[SandboxHandle]
ping()
Check agent reachability without refreshing idle activity; does not start stopped sandboxes
<p className="msb-label">Returns</p>Awaitable[SandboxPingResult]
touch()
Explicitly refresh idle activity; does not start stopped sandboxes
<p className="msb-label">Returns</p>Awaitable[SandboxTouchResult]
modify(...)
Plan or apply a configuration change; same kwargs as modify(). Does not start stopped sandboxes; changes persist for the next boot
Awaitable[SandboxModificationPlan]
connect(timeout=None)
Connect to a running sandbox, optionally with an explicit timeout in seconds
<p className="msb-label">Returns</p>Awaitable[Sandbox]
start(*, detached=False)
Start in attached or detached mode
<p className="msb-label">Returns</p>Awaitable[Sandbox]
stop(timeout=None)
Gracefully shut down and wait until stopped state is observed
<p className="msb-label">Returns</p>Awaitable[None]
request_stop()
Request graceful shutdown without waiting
<p className="msb-label">Returns</p>Awaitable[None]
kill(timeout=None)
Force terminate and wait until stopped state is observed
<p className="msb-label">Returns</p>Awaitable[None]
request_kill()
Request force termination without waiting
<p className="msb-label">Returns</p>Awaitable[None]
request_drain()
Request graceful drain without waiting
<p className="msb-label">Returns</p>Awaitable[None]
wait_until_stopped()
Block until the sandbox reaches terminal state
<p className="msb-label">Returns</p>Awaitable[SandboxStopResult]
remove()
Delete sandbox and state
<p className="msb-label">Returns</p>Awaitable[None]
metrics()
Point-in-time resource metrics
<p className="msb-label">Returns</p>Awaitable[SandboxMetrics]
logs(...)
Read captured exec.log (works without starting)
Awaitable[list[LogEntry]]
log_stream(...)
Stream captured log entries (works without starting)
<p className="msb-label">Returns</p>Awaitable[LogStream]
snapshot(name)
Create a named snapshot of the sandbox
<p className="msb-label">Returns</p>Awaitable[Snapshot]
Async stream for receiving periodic metrics snapshots.
__aiter__()
Use with async for
__anext__()
Use with async for
A single captured log entry returned by logs() or iterated from a LogStream.
float
Wall-clock capture time (ms since Unix epoch, UTC)
Where the chunk came from
int \| None
Relay-monotonic session id; None for "system" entries
str
Opaque resume token; pass back via log_stream(from_cursor=...)
bytes
The chunk's raw bytes
text()
Convenience: UTF-8 decode of data (lossy; invalid bytes are replaced)
str
Async stream of LogEntry values, returned by log_stream().
__aiter__()
Use with async for
__anext__()
Use with async for
Returned by create_with_progress(). The factory itself is synchronous; use the returned session as an async context manager to track image pull progress.
AsyncIterator[PullEvent]
Async iterator of pull progress events
result()
Await once to get the final running sandbox. A second call raises RuntimeError
Awaitable[Sandbox]
session = Sandbox.create_with_progress("my-sandbox", image="ubuntu:latest")
async with session:
async for event in session.progress:
print(event)
sb = await session.result()
The keyword arguments accepted by create() and create_with_progress(). There is no SandboxConfig object you construct directly; these are passed as **kwargs.
| Field | Type | Default | Description |
|---|---|---|---|
| image | str | os.PathLike[str] | ImageSource | - | OCI image, local path, or disk image. Required unless from_snapshot= is passed. Use Image.oci("python:3.12", root_disk=RootDisk.managed(8192)) to set a managed OCI root-disk size |
| from_snapshot | str | os.PathLike[str] | - | Snapshot artifact to boot from instead of image=. Mutually exclusive with image= |
| cpus | int | 1 | Virtual CPUs. This is a limit, not a reservation |
| max_cpus | int | same as cpus | Boot-time maximum possible virtual CPUs |
| memory | int | 512 | Guest memory in MiB. This is a limit, not a reservation |
| max_memory | int | same as memory | Boot-time maximum hotpluggable memory in MiB |
| thp | "always" | "madvise" | "never" | "madvise" | Guest transparent huge-page policy selected at boot |
| workdir | str | - | Default working directory for commands |
| shell | str | "/bin/sh" | Shell for shell() calls |
| security | SecurityProfile | DEFAULT | In-guest security profile. RESTRICTED sets no_new_privs, drops mount-admin capability from user commands, and forces nosuid,nodev on user mounts |
| hostname | str | - | Guest hostname |
| user | str | - | Default guest user |
| entrypoint | Sequence[str] | - | Override the image's stored ENTRYPOINT used by exec_default; literal exec and shell ignore it |
| cmd | Sequence[str] | - | Override the image's stored CMD used by exec_default; an empty sequence clears CMD |
| init | str | InitConfig|InitOptions | - | Hand off PID 1 to a guest init binary. See Custom init system and InitConfig for accepted shapes |
| replace | bool | False | Replace an existing sandbox with the same name (10s SIGTERM grace, then SIGKILL) |
| replace_with_timeout | float | 10 | Seconds to wait after SIGTERM before escalating to SIGKILL (0 skips SIGTERM). Implies replace=True |
| max_duration | float | - | Maximum sandbox lifetime in seconds |
| idle_timeout | float | - | Idle timeout in seconds |
| ephemeral | bool | False | If True, the sandbox and all its persisted state are removed automatically once it stops, rather than left on disk for restart |
| env | Mapping[str, str] | {} | Environment variables visible to all commands |
| labels | Mapping[str, str] | {} | User-defined labels attached to the sandbox. Filter with list_with(); see Select in bulk. Keys starting with sandbox., microsandbox., or service. are reserved and rejected |
| scripts | Mapping[str, str] | {} | Named scripts mounted at /.msb/scripts/ and added to PATH |
| pull_policy | PullPolicy | IF_MISSING | Image pull behavior |
| log_level | LogLevel | - | Override log verbosity |
| registry_auth | RegistryAuth | - | Private registry credentials |
| registry_insecure | bool | False | Pull images over plain HTTP instead of HTTPS (local or self-hosted registries) |
| registry_ca_certs | list[bytes | bytearray | str | os.PathLike] | [] | Additional CA roots for image pulls. File paths are read when create() is called. |
| volumes | Mapping[str, MountConfig] | {} | Volume mounts. See Volumes |
| patches | Sequence[PatchConfig] | [] | Rootfs modifications applied before boot |
| ports | Mapping[int, int] | Sequence[PortBinding] | {} | Port mappings. Mapping form is TCP and binds to 127.0.0.1; use PortBinding for explicit bind addresses or UDP |
| network | Network | public profile | Network policy and configuration |
| secrets | Sequence[SecretEntry] | [] | Secret injection |
| on_secret_violation | ViolationAction|ViolationPolicy | BLOCK_AND_LOG | Sandbox-wide default action when a secret placeholder would leak to a non-eligible destination. Shorthand for Network.on_secret_violation; if both are provided, this top-level value takes precedence. See Violation policy |
| detached | bool | False | If True, spawn the sandbox in detached mode; call detach() before dropping the returned handle when it should keep running |
Custom init specification. Pass it (or one of the equivalent shorthand shapes) as the init= kwarg to create() to hand PID 1 inside the guest off to your own init binary after agentd's setup. Frozen dataclass. See Custom init system for image picks, shutdown semantics, and tradeoffs.
| Field | Type | Default | Description |
|---|---|---|---|
| cmd | str | - | Absolute path or "auto" to the init binary. Auto honors a known image ENTRYPOINT init before probing /sbin/init, /lib/systemd/systemd, and /usr/lib/systemd/systemd, and preserves attached init-entrypoint commands |
| args | tuple[str, ...] | () | Supplemental argv (argv[0] is implicitly cmd) |
| env | Mapping[str, str] | {} | Extra env vars merged on top of the inherited env |
The init= kwarg accepts a bare scalar for the simple case, an InitConfig dataclass, or an InitOptions typed dictionary for the rich case.
| Form | Equivalent to |
|---|---|
init="auto" or init="/sbin/init" | InitConfig(cmd=...) |
init={"cmd": ..., "args": [...], "env": {...}} | dict equivalent of InitConfig |
init=InitConfig(cmd="/sbin/init", args=("--foo",)) | itself |
from microsandbox import InitConfig, Sandbox
# Common case: bare string.
sb = await Sandbox.create("worker", image="jrei/systemd-debian:12", init="auto")
# Argv / env: dataclass.
sb = await Sandbox.create(
"worker",
image="jrei/systemd-debian:12",
init=InitConfig(
cmd="/lib/systemd/systemd",
args=("--unit=multi-user.target",),
env={"container": "microsandbox"},
),
)
Typed-dictionary form of InitConfig. cmd is required; args and env are optional.
| Key | Type | Description |
|---|---|---|
cmd | str | Absolute path or "auto" to the init binary |
args | list[str] | Supplemental argv |
env | dict[str, str] | Extra environment variables |
Sandbox-wide in-guest security profile.
| Member | Value | Description |
|---|---|---|
SecurityProfile.DEFAULT | "default" | Standard profile |
SecurityProfile.RESTRICTED | "restricted" | Sets no_new_privs, drops mount-admin capability from user commands, and forces nosuid,nodev on user mounts |
One stable, newest-first page returned by Sandbox.list() or Sandbox.list_with().
| Property | Type | Description |
|---|---|---|
sandboxes | list[SandboxHandle] | Handles in this page |
next_cursor | str | None | Opaque continuation cursor, or None on the final page |
Agent reachability result.
| Property | Type | Description |
|---|---|---|
| name | str | Sandbox name |
| latency_ms | float | Round-trip latency in milliseconds |
Explicit idle-refresh result.
| Property | Type | Description |
|---|---|---|
| name | str | Sandbox name |
| activity_seq | int | Monotonic activity sequence after the touch |
Observed terminal sandbox state returned by wait_until_stopped().
| Property | Type | Description |
|---|---|---|
| name | str | Sandbox name |
| status | SandboxStatus | Terminal status that was observed |
| exit_code | int | None | Process exit code when it is available |
| signal | int | None | Terminating signal number when the sandbox was killed by a signal |
| observed_at | float | When the terminal state was observed (ms since epoch) |
| source | str | None | Where the terminal observation came from |
Sandbox lifecycle status returned by status fields.
| Member | Value | Description |
|---|---|---|
SandboxStatus.CREATED | "created" | Sandbox metadata exists but the VM has not started |
SandboxStatus.STARTING | "starting" | A start request is in progress |
SandboxStatus.RUNNING | "running" | Guest agent is ready; exec, shell, and fs work |
SandboxStatus.STOPPED | "stopped" | VM shut down; configuration persisted; can be restarted |
SandboxStatus.CRASHED | "crashed" | VM exited unexpectedly (kernel panic, OOM, etc.) |
SandboxStatus.DRAINING | "draining" | Graceful shutdown in progress; existing commands finish, new ones rejected |
SandboxStatus.PAUSED | "paused" | VM paused |
Selected sandbox backend.
| Member | Value | Description |
|---|---|---|
BackendKind.LOCAL | "local" | Local libkrun backend |
BackendKind.CLOUD | "cloud" | Microsandbox cloud backend; requires an API key or named profile |
from microsandbox import BackendKind, set_default_backend
set_default_backend(BackendKind.CLOUD, profile="production")
Controls how sandbox modifications that cannot apply live are handled.
| Member | Value | Description |
|---|---|---|
ModificationPolicy.NO_RESTART | "no_restart" | Apply only changes that do not require a restart |
ModificationPolicy.NEXT_START | "next_start" | Persist changes for the next start without restarting a running VM |
ModificationPolicy.RESTART | "restart" | Restart when required so changes become active immediately |
Desired state for one secret. env, value, and store are mutually exclusive sources. Omit all three to update only the placeholder or allowed hosts.
| Key | Type | Description |
|---|---|---|
env | str | Host environment variable to resolve when applying the modification |
value | str | Raw secret value. Stored in the durable sandbox config until replaced by a source reference |
store | str | Reserved for a host-side secret store reference; currently unsupported |
placeholder | str | Explicit guest-visible placeholder. New secrets default to $MSB_<NAME> |
allowed_hosts | list[str] | Desired allowed host patterns. A new secret requires at least one; an empty list leaves existing hosts unchanged |
Typed dictionary returned by Sandbox.modify() and SandboxHandle.modify(). Config changes and secret changes use different typed shapes and are discriminated by their kind member.
| Key | Type | Description |
|---|---|---|
sandbox | str | Sandbox being modified |
status | SandboxStatus | Status used to classify the requested changes |
applied | bool | Whether the plan was applied |
policy | ModificationPolicy | Policy used to produce the plan |
changes | list[ConfigPlannedChange | SecretPlannedChange] | Planned changes |
conflicts | list[ModificationConflict] | Blocking conflicts, each with field and message |
warnings | list[ModificationWarning] | Non-fatal warnings, each with field and message |
resize_status | list[ResourceResizeStatus] | Optional live-resize convergence results |
ConfigPlannedChange has kind, field, change, and disposition, plus optional before, after, and reason. SecretPlannedChange has kind, field, name, change, and disposition, plus optional before_ref, after_ref, allow_hosts, and reason. Secret values never appear in a plan.
Discriminator for entries in SandboxModificationPlan.changes.
| Member | Value | Description |
|---|---|---|
PlannedChangeKind.CONFIG | "config" | Ordinary configuration change |
PlannedChangeKind.SECRET | "secret" | Secret metadata or material change |
Natural operation for a configuration change.
| Member | Value | Description |
|---|---|---|
ChangeKind.ADDED | "added" | A field is being added |
ChangeKind.UPDATED | "updated" | A field is being updated |
ChangeKind.REMOVED | "removed" | A field is being removed |
Natural operation for a secret change.
| Member | Value | Description |
|---|---|---|
SecretChangeKind.ADDED | "added" | A secret is being added |
SecretChangeKind.ROTATED | "rotated" | Secret material is being rotated |
SecretChangeKind.REMOVED | "removed" | A secret is being removed |
SecretChangeKind.RENAMED | "renamed" | A secret is being renamed |
SecretChangeKind.HOSTS_UPDATED | "hosts updated" | Allowed hosts are being updated |
SecretChangeKind.PLACEHOLDER_UPDATED | "placeholder updated" | The guest-visible placeholder is being updated |
When or whether a planned change can take effect.
| Member | Value | Description |
|---|---|---|
ModificationDisposition.LIVE | "live" | Applies to the running VM now |
ModificationDisposition.NEXT_START | "next start" | Applies the next time the sandbox starts |
ModificationDisposition.REQUIRES_RESTART | "requires restart" | Requires a restart before taking effect |
ModificationDisposition.UNSUPPORTED | "unsupported" | Cannot be changed through modify() |
Resource reported by a live-resize result.
| Member | Value | Description |
|---|---|---|
ResourceKind.CPUS | "cpus" | vCPU count |
ResourceKind.MEMORY | "memory" | Guest memory |
Observed convergence state for an accepted live resize.
| Member | Value | Description |
|---|---|---|
ResourceConvergenceState.ACCEPTED | "accepted" | Runtime accepted the request |
ResourceConvergenceState.CONVERGING | "converging" | Guest and VMM are still converging |
ResourceConvergenceState.APPLIED | "applied" | Desired, actual, and enforced values match |
ResourceConvergenceState.GUEST_REFUSED | "guest-refused" | Guest refused or failed to cooperate |
ResourceConvergenceState.FAILED | "failed" | Resize failed |
Point-in-time resource usage snapshot.
| Field | Type | Description |
|---|---|---|
| cpu_percent | float | CPU usage as a percentage |
| vcpu_time_ns | int | Cumulative vCPU time consumed since boot, in nanoseconds |
| memory_bytes | int | Current memory usage in bytes |
| memory_available_bytes | int | None | Guest-reported available memory in bytes when known |
| memory_host_resident_bytes | int | None | Host-resident memory backing the guest in bytes when known |
| memory_limit_bytes | int | Memory limit in bytes |
| disk_read_bytes | int | Total bytes read from disk since boot |
| disk_write_bytes | int | Total bytes written to disk since boot |
| net_rx_bytes | int | Total bytes received over the network since boot |
| net_tx_bytes | int | Total bytes sent over the network since boot |
| upper_used_bytes | int | None | Guest-visible OCI upper filesystem used bytes when the protected reporter is available and fresh |
| upper_free_bytes | int | None | Guest-visible OCI upper filesystem free bytes when the protected reporter is available and fresh |
| upper_host_allocated_bytes | int | None | Host-allocated bytes for the writable OCI upper image when available |
| uptime_ms | int | Time since the sandbox was created (ms) |
| timestamp_ms | float | When this measurement was taken (ms since epoch) |
Source attached to each LogEntry.
| Member | Value | Description |
|---|---|---|
LogSource.STDOUT | "stdout" | Captured from a session's stdout (pipe mode, streams stayed separated) |
LogSource.STDERR | "stderr" | Captured from a session's stderr (pipe mode) |
LogSource.OUTPUT | "output" | Captured from a PTY session, where stdout and stderr are merged by the guest kernel |
LogSource.SYSTEM | "system" | Synthetic lifecycle or runtime diagnostic entry |
Log source selector accepted by logs() and log_stream().
| Member | Value | Description |
|---|---|---|
LogReadSource.STDOUT | "stdout" | Select stdout entries |
LogReadSource.STDERR | "stderr" | Select stderr entries |
LogReadSource.OUTPUT | "output" | Select PTY-merged output entries |
LogReadSource.SYSTEM | "system" | Select lifecycle and runtime diagnostic entries |
LogReadSource.ALL | "all" | Select all four sources |
Sandbox process log verbosity.
| Member | Value | Description |
|---|---|---|
LogLevel.TRACE | "trace" | Most verbose, all diagnostic output |
LogLevel.DEBUG | "debug" | Debug and higher |
LogLevel.INFO | "info" | Info and higher |
LogLevel.WARN | "warn" | Warnings and errors only |
LogLevel.ERROR | "error" | Errors only |
Controls when the SDK fetches an OCI image from the registry.
| Member | Value | Description |
|---|---|---|
PullPolicy.ALWAYS | "always" | Pull the image every time, even if cached locally |
PullPolicy.IF_MISSING | "if-missing" | Pull only if the image is not already cached. This is the default |
PullPolicy.NEVER | "never" | Never pull; fail if the image is not cached locally |
Credentials for authenticating to a private container registry. Frozen dataclass; construct directly or via RegistryAuth.basic(username, password).
| Field | Type | Description |
|---|---|---|
| username | str | Registry username |
| password | str | Registry password |
A single rootfs patch. Produced by the Patch factory; you'd normally not construct one directly. Frozen dataclass.
| Field | Type | Description |
|---|---|---|
| kind | PatchKind | Patch operation |
| path | str | None | Absolute guest path (text / file / mkdir / remove / append) |
| content | str | bytes | None | Text content for TEXT / APPEND, or binary content for FILE |
| src | str | None | Host source path (copy_file / copy_dir) |
| dst | str | None | Guest destination path (copy_file / copy_dir) |
| target | str | None | Symlink target |
| link | str | None | Symlink path |
| mode | int | None | File / directory mode for text, file, copy-file, or mkdir patches (e.g. 0o644) |
| replace | bool | When True, overwrite an existing destination for text, file, copy, or symlink patches. Defaults to False |
Rootfs patch operation.
| Member | Value | Description |
|---|---|---|
PatchKind.TEXT | "text" | Write a UTF-8 text file |
PatchKind.FILE | "file" | Write an arbitrary binary file |
PatchKind.APPEND | "append" | Append to a text file |
PatchKind.COPY_FILE | "copy_file" | Copy a host file into the guest |
PatchKind.COPY_DIR | "copy_dir" | Copy a host directory into the guest |
PatchKind.SYMLINK | "symlink" | Create a symbolic link |
PatchKind.MKDIR | "mkdir" | Create a directory |
PatchKind.REMOVE | "remove" | Remove a path |
Native event object emitted by PullSession.progress. Inspect event_type and the fields relevant to that event; fields that do not apply to a particular event are None.
| Field | Type | Description |
|---|---|---|
| event_type | PullEventType | Event discriminator |
| reference | str | None | Image reference being pulled |
| manifest_digest | str | None | Resolved manifest digest |
| layer_count | int | None | Number of layers |
| total_download_bytes | int | None | Total bytes to download across layers |
| layer_index | int | None | Index of the layer this event concerns |
| digest | str | None | Layer blob digest |
| diff_id | str | None | Layer diff id |
| downloaded_bytes | int | None | Bytes downloaded so far for the layer |
| total_bytes | int | None | Total bytes for the layer |
| bytes_read | int | None | Bytes read during materialization |
from microsandbox import PullEventType, Sandbox
session = Sandbox.create_with_progress("my-sandbox", image="ubuntu:latest")
async with session:
async for event in session.progress:
if event.event_type is PullEventType.RESOLVED:
print(f"{event.layer_count} layers, {event.total_download_bytes} bytes")
elif event.event_type is PullEventType.LAYER_DOWNLOAD_PROGRESS:
print(f"layer {event.layer_index}: {event.downloaded_bytes}/{event.total_bytes}")
sb = await session.result()
Discriminator returned by PullEvent.event_type.
| Member | Value | Description |
|---|---|---|
PullEventType.RESOLVING | "resolving" | Resolving the image reference |
PullEventType.RESOLVED | "resolved" | Manifest resolved |
PullEventType.LAYER_DOWNLOAD_PROGRESS | "layer_download_progress" | Layer download advanced |
PullEventType.LAYER_DOWNLOAD_COMPLETE | "layer_download_complete" | Layer download completed |
PullEventType.LAYER_DOWNLOAD_VERIFYING | "layer_download_verifying" | Layer digest is being verified |
PullEventType.LAYER_MATERIALIZE_STARTED | "layer_materialize_started" | Layer materialization started |
PullEventType.LAYER_MATERIALIZE_PROGRESS | "layer_materialize_progress" | Layer materialization advanced |
PullEventType.LAYER_MATERIALIZE_WRITING | "layer_materialize_writing" | Materialized layer is being written |
PullEventType.LAYER_MATERIALIZE_COMPLETE | "layer_materialize_complete" | Layer materialization completed |
PullEventType.STITCH_MERGING_TREES | "stitch_merging_trees" | Layer trees are being merged |
PullEventType.STITCH_WRITING_FSMETA | "stitch_writing_fsmeta" | Filesystem metadata is being written |
PullEventType.STITCH_WRITING_VMDK | "stitch_writing_vmdk" | VMDK image is being written |
PullEventType.STITCH_COMPLETE | "stitch_complete" | Image stitching completed |
PullEventType.COMPLETE | "complete" | Image pull completed |