Skip to content

Sandbox

@final
class Sandbox

One MicroVM’s whole life.

The five transitions are build_image, run, suspend, resume, and terminate, and every state guard lives in the core — see the module docs. Each has an awaitable twin (run_async and so on) that drives the same future; [crate::runtime] says how.

Sandbox(
region: Region,
*,
retry: tuple[SupportsIndex, SupportsFloat | SupportsIndex] | None = None,
)

Resolves credentials for region and returns a sandbox with nothing launched.

region is a [PyRegion] and not a string, so a region name is parsed once, at Region.parse or a named constructor, and a malformed one never reaches this call.

adopted: bool

Whether this sandbox was built by adopt rather than by its own launch.

bootstrap_count: int

How many times the token has been installed. Never above one (STATE-3).

endpoint: str | None

The proxy endpoint, once launched.

image: Image | None

The image, once built.

image_exists: bool

Whether an image is recorded as existing (STATE-1).

is_detached: bool

Whether detach() handed this sandbox’s VM to another process.

launch_image_arn: str | None

The image ARN run sent in RunMicrovm: the caller’s identifier with a bare name resolved, or the built image’s.

Recorded before the call, so a launch the service refused still names the image it asked for. None until run has resolved an image, and on an adopted sandbox.

lifecycle: str

The lifecycle state: "PENDING", "RUNNING", "SUSPENDING", "SUSPENDED", "TERMINATING", or "TERMINATED".

Spelled as the service spells it, because a reader compares it against a GetMicrovm response and Suspended beside SUSPENDED reads like two facts.

microvm_id: str | None

The VM id, once launched.

session: Session | None

The session, once launched.

A new wrapper each call, all reaching the same session under the same lock. There is no cached Py<PySession>: caching one would mean a session object that outlives the VM it addresses, and the Held::InSandbox indirection exists precisely so a post-terminate call reports the lifecycle rather than a dangling handle.

state_reason: str | None

Why the VM is in its current state, when the service said.

The absence is information: TRAP-8’s message distinguishes “no stateReason” from an empty one.

suspended_window_seconds: float | None

The suspended window this sandbox asked for at launch, in seconds.

None before this sandbox launches a VM. This accessor reports the requested window; GetMicrovm also returns the service’s idle policy.

token_installed: bool

Whether the agent token has been installed (STATE-2).

Set by the platform reporting RUNNING, not by the launch call: the run hook is what delivers the token, and a launch that died during startup delivered nothing.

tunnel_identity: TunnelIdentity | None

The tunnel identity a launch with identity=True generated, or that an adopted record carried; None otherwise.

Holds the host’s secret half: pass it to Session.tunnel(verify_identity=...), and store it only where the agent token goes.

was_terminated: bool

Whether this VM was ever terminated (STATE-11).

@staticmethod
def adopt(
region: Region,
microvm_id: str,
endpoint: str,
agent_token: str,
*,
port: SupportsIndex | None = None,
) -> Sandbox

A sandbox for a VM another process launched, from its private record.

The lifecycle is read from GetMicrovm, so suspend, resume, and terminate start from the service’s state and keep every guard. The VM was bootstrapped by its own launch, so run is refused and no run-hook payload is ever sent. Keep agent_token in private encrypted storage; it never appears in repr or an error.

@staticmethod
def adopt_async(
region: Region,
microvm_id: str,
endpoint: str,
agent_token: str,
*,
port: SupportsIndex | None = None,
) -> Sandbox

The awaitable twin of Sandbox.adopt.

def build_artifact(
*,
name: str,
binary: SupportsGetItem[int, SupportsIndex],
code_artifact_uri: str,
build_role_arn: str,
base_image: BaseImage | None = None,
dockerfile: str | None = None,
project_dir: str | PathLike[str] | None = None,
inherit_workdir: bool = False,
) -> bytes

The artifact bytes to upload to code_artifact_uri.

The upload is the caller’s on this path (ensure_image uploads for itself). Same parameters as [Self::build_image] so the bytes a caller puts in the bucket are the bytes the build will receive.

def build_image(
*,
name: str,
binary: SupportsGetItem[int, SupportsIndex],
code_artifact_uri: str,
build_role_arn: str,
size: SizeClass | None = None,
base_image: BaseImage | None = None,
base_image_version: str | None = None,
dockerfile: str | None = None,
project_dir: str | PathLike[str] | None = None,
repair_guest_identity: bool = False,
inherit_workdir: bool = False,
run_hook_timeout: RunHookTimeout | None = None,
build_hook_timeout: BuildHookTimeout | None = None,
tags: dict[str, str] | None = None,
log_group: str | None = None,
log_stream: str | None = None,
token_scope: str | None = None,
) -> Image

Builds an image and waits for it to become usable.

Every local guard runs before the call, which matters because the create happens after the caller’s artifact upload: a rejection AWS raises costs the upload first.

base_image_version pins the managed base to one version, a value managed_base_versions lists. project_dir bakes the directory’s one manifest+lockfile pair into an environment layer, by the rule the CLI’s --project uses; a directory without exactly one pair raises PreconditionError before any call.

What is deliberately not a parameter

A client_token. There is no such field on the core’s request type and none here: a digest-derived token replays the original create and wedges an image in CREATING for fifteen hours with no error at all (TRAP-1). token_scope is a CloudTrail label folded in beside a fresh nonce and cannot become the token.

A capabilities list. repair_guest_identity is a bool and the request injects ["ALL"] itself, so ["CAP_SYS_ADMIN"] — the request AWS rejects after the upload — is not something a caller can write (TRAP-3).

An architecture. The model’s enum has exactly one value, so the only thing a field could express is a rejected request.

log_stream is a prefix, never the exact stream name

The platform’s logging.logStream member is an EXACT stream name (prefixes unsupported), and one build is three VMs writing three streams — so a fixed name would collapse every build’s logs into one stream. The client appends /<16 hex> of fresh randomness per build attempt, and the resolved exact name comes back on Image.log_stream. log_stream requires log_group.

def build_image_async(
*,
name: str,
binary: SupportsGetItem[int, SupportsIndex],
code_artifact_uri: str,
build_role_arn: str,
size: SizeClass | None = None,
base_image: BaseImage | None = None,
base_image_version: str | None = None,
dockerfile: str | None = None,
project_dir: str | PathLike[str] | None = None,
repair_guest_identity: bool = False,
inherit_workdir: bool = False,
run_hook_timeout: RunHookTimeout | None = None,
build_hook_timeout: BuildHookTimeout | None = None,
tags: dict[str, str] | None = None,
log_group: str | None = None,
log_stream: str | None = None,
token_scope: str | None = None,
) -> Image

The awaitable twin of build_image, with its keywords. A lifecycle transition: cancelling the awaitable leaves the build running to completion on this sandbox.

@staticmethod
def create_async(
region: Region,
*,
retry: tuple[SupportsIndex, SupportsFloat | SupportsIndex] | None = None,
) -> Sandbox

The awaitable twin of Sandbox(region): resolves credentials on the shared runtime.

def detach() -> Detached

Hands the VM off to another process and returns what that process adopts it with.

For a workflow whose steps run in different processes: the launching step calls this instead of dropping the sandbox (which warns that a live VM was abandoned), persists the returned record privately, and a later step calls Sandbox.adopt with it. The VM keeps running and no AWS call is made. Afterwards this sandbox is inert: its session is gone and run, wait_until_running, suspend, resume, and terminate are refused. Raises PreconditionError without a live VM or when already detached.

def ensure_image(
*,
name_prefix: str,
binary: SupportsGetItem[int, SupportsIndex],
dockerfile: str,
s3_bucket: str,
build_role_arn: str,
context_dir: str | PathLike[str] | None = None,
s3_key_prefix: str | None = None,
size: SizeClass | None = None,
base_image: BaseImage | None = None,
base_image_version: str | None = None,
project_dir: str | PathLike[str] | None = None,
force: bool = False,
repair_guest_identity: bool = False,
tags: dict[str, str] | None = None,
wait_timeout: SupportsFloat | SupportsIndex | None = None,
) -> EnsuredImage

Builds or reuses the content-addressed image for a task: one call from build inputs to a ready image.

The name is <name_prefix>-<hash12>, the hash over the daemon, the Dockerfile, the build context, the base image and the size class, so equal inputs name one image. The ARN is built from the caller’s account (looked up once per sandbox). The image is described, then:

  • ready: returned, with reused=True and no upload;
  • building: waited out and returned, reused=True;
  • failed, or any state under force=True: deleted, the name awaited free, rebuilt;
  • absent: the artifact is uploaded to s3://<s3_bucket>/<s3_key_prefix>/<name>/ artifact.zip and the image created and waited for, reused=False.

When a concurrent caller creates the name first, this call’s create is refused; it describes again and waits for that build, returning it with reused=True.

dockerfile is usually wrap_dockerfile(task). context_dir is the directory the Dockerfile’s COPY lines read, taken as docker build takes it: Dockerfile.dockerignore, else .dockerignore, is honoured, and symlinks are skipped with a line in warnings. base_image defaults to BaseImage.from_dockerfile(dockerfile). base_image_version and project_dir are build_image’s, and both join the name’s hash. wait_timeout is the build wait in seconds (45 minutes by default). repair_guest_identity repairs identity during creation and joins the name’s hash. Every local check runs before the first AWS call.

def ensure_image_async(
*,
name_prefix: str,
binary: SupportsGetItem[int, SupportsIndex],
dockerfile: str,
s3_bucket: str,
build_role_arn: str,
context_dir: str | PathLike[str] | None = None,
s3_key_prefix: str | None = None,
size: SizeClass | None = None,
base_image: BaseImage | None = None,
base_image_version: str | None = None,
project_dir: str | PathLike[str] | None = None,
force: bool = False,
repair_guest_identity: bool = False,
tags: dict[str, str] | None = None,
wait_timeout: SupportsFloat | SupportsIndex | None = None,
) -> EnsuredImage

The awaitable twin of ensure_image, with its keywords. Cancellation stops the local wait and releases this sandbox without recording an image. A build already accepted by the service keeps running; ensure the same inputs again to join it.

@staticmethod
def from_name(
region: Region,
name: str,
registry: NameRegistry,
*,
port: SupportsIndex | None = None,
) -> Sandbox

Adopts the VM registered as name in registry; see Sandbox.adopt.

region must match the record’s: an id from another region addresses nothing here.

@staticmethod
def from_name_async(
region: Region,
name: str,
registry: NameRegistry,
*,
port: SupportsIndex | None = None,
) -> Sandbox

The awaitable twin of Sandbox.from_name.

def managed_base_versions(base_image_arn: str) -> list[ManagedBaseVersion]

ListManagedMicrovmImageVersions, every page: the versions of a managed base, the values build_image’s base-version pin takes.

base_image_arn is the base’s full ARN, such as arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1; a bare name is refused.

def managed_base_versions_async(base_image_arn: str) -> list[ManagedBaseVersion]

The awaitable twin of managed_base_versions.

def preflight(
*,
name: str,
binary: SupportsGetItem[int, SupportsIndex],
code_artifact_uri: str,
build_role_arn: str,
size: SizeClass | None = None,
base_image: BaseImage | None = None,
base_image_version: str | None = None,
dockerfile: str | None = None,
project_dir: str | PathLike[str] | None = None,
repair_guest_identity: bool = False,
inherit_workdir: bool = False,
run_hook_timeout: RunHookTimeout | None = None,
build_hook_timeout: BuildHookTimeout | None = None,
tags: dict[str, str] | None = None,
log_group: str | None = None,
log_stream: str | None = None,
token_scope: str | None = None,
) -> None

Every local guard build_image runs, with zero calls: raises the refusal build_image would, so a caller who uploads its own artifact checks the request before paying for the upload. Takes build_image’s keywords.

def resume() -> Session

Thaws the VM and returns a usable session.

What the core refuses, before any wire call

A resume after terminate (STATE-11) — a terminated VM never returns to RUNNING, and even a call the service accepted would hand back a different machine. A handle in neither RUNNING nor SUSPENDED also refuses locally (STATE-7).

What the service decides (STATE-12)

How long the VM has been suspended. The idlePolicy terminates a VM the idle policy suspended once suspended_sec passes, and a VM this sandbox suspended stayed resumable 1283 s into a 60 s window (measured 2026-10-12, us-east-1). WindowClosedError reports a VM the service says is TERMINATING or TERMINATED, whatever ended it. The sandbox records it as TERMINATED.

With a locally RUNNING handle, resume reads GetMicrovm first. It resumes an observed SUSPENDED VM and reports WindowClosedError for a VM that has ended. Other known service states refuse the request; a later call can read the service again.

Nothing is re-delivered: no run-hook payload, no token, no bootstrap. The in-memory token survived the freeze, and re-delivering it would hit the daemon’s one-shot bootstrap and be refused — a 409 that reads like a broken VM.

def resume_async() -> Session

The awaitable twin of resume. A lifecycle transition: cancelling the awaitable leaves the resume running to completion.

def run(
*,
image_identifier: str | None = None,
image_version: str | None = None,
execution_role_arn: str | None = None,
agent_token: str | None = None,
client_token: str | None = None,
launch_env: dict[str, str] | None = None,
identity: bool = False,
egress: bool = False,
egress_network_connectors: SupportsGetItem[int, str] | None = None,
deny_egress: bool = False,
shell: bool = False,
max_idle_sec: SupportsIndex | None = None,
suspended_sec: SupportsIndex | None = None,
auto_resume: bool = False,
max_duration_sec: SupportsIndex | None = None,
ready_timeout: SupportsFloat | SupportsIndex | None = None,
token_scope: str | None = None,
wait: bool = True,
log_group: str | None = None,
log_stream: str | None = None,
disable_logging: bool = False,
) -> Session

Launches a MicroVM and returns its session. By default it waits for RUNNING and for the daemon to answer; wait=False skips those readiness waits.

ready_timeout bounds the wait for RUNNING (default session_constants()["defaultRunningTimeoutSeconds"]); the wait for the daemon after it is defaultReadyTimeoutSeconds, and a daemon that never answers raises TimeoutError with the VM left RUNNING.

image_identifier is an image ARN or a bare image name, or omitted for the image build_image built. The core resolves a name to its ARN with one ListMicrovmImages read, and a name no image carries raises PreconditionError before anything launches.

egress requests the managed INTERNET_EGRESS connector. Omission does not block outbound traffic. For no egress, pass existing VPC connector ARNs through egress_network_connectors, using a VPC without an internet gateway or NAT gateway. deny_egress sets advisory proxy variables that workloads can bypass.

identity=True generates a tunnel identity and delivers the VM’s half with the launch, so Session.tunnel(port, verify_identity=sandbox.tunnel_identity) can prove the far end is this VM’s daemon.

What the core refuses here, and this file does not

A second run on one sandbox, with zero control-plane calls: the agent token is installed at most once per VM lifetime (STATE-3), and a second VM needs a second Sandbox. A run with no image at all, before any call. Neither check is in this file.

agent_token is optional because the common case is a per-VM secret nobody needs to see; a caller who has one already — a harness minting its own, or a retry that must reuse the first attempt’s — passes it. It rides in runHookPayload, which is what keeps it out of the shared image snapshot.

def run_async(
*,
image_identifier: str | None = None,
image_version: str | None = None,
execution_role_arn: str | None = None,
agent_token: str | None = None,
client_token: str | None = None,
launch_env: dict[str, str] | None = None,
identity: bool = False,
egress: bool = False,
egress_network_connectors: SupportsGetItem[int, str] | None = None,
deny_egress: bool = False,
shell: bool = False,
max_idle_sec: SupportsIndex | None = None,
suspended_sec: SupportsIndex | None = None,
auto_resume: bool = False,
max_duration_sec: SupportsIndex | None = None,
ready_timeout: SupportsFloat | SupportsIndex | None = None,
token_scope: str | None = None,
wait: bool = True,
log_group: str | None = None,
log_stream: str | None = None,
disable_logging: bool = False,
) -> Session

The awaitable twin of run, with its keywords. A lifecycle transition: cancelling the awaitable stops pending retries. An in-flight request finishes so an accepted VM is still recorded on this sandbox and remains available to terminate.

def suspend() -> str

Freezes the VM and waits for the platform to report it.

A freeze and restore rather than a stop and start: the guest keeps its memory, so the token, the filesystem, and every exec record survive. The one thing that does not is the guest’s view of time — it observes the whole suspension as a single jump, so any timeout, lease, or TLS session a running command holds expires at once on resume.

A suspend from anything but RUNNING is refused by the core with zero control-plane calls (STATE-5). Returns the state reached, which may be "TERMINATED": a VM that dies while suspending is a state to report rather than an exception out of the middle of a teardown.

def suspend_async() -> str

The awaitable twin of suspend. A lifecycle transition: cancelling the awaitable leaves the suspend running to completion.

def terminate(
*,
delete_image: bool = False,
delete_log_group: bool = False,
delete_attempts: SupportsIndex | None = None,
delete_backoff: SupportsFloat | SupportsIndex | None = None,
wait_for_terminated: bool | SupportsFloat | SupportsIndex = ...,
) -> TeardownReport

Tears down, best-effort, never raising.

Order: VM, then image, then the log group last, because the service can recreate a group deleted before its image.

Both deletions are opt-in, because both destroy something a caller may still want: the image is reusable across runs, and the log group is where a failed build’s only evidence lives. delete_log_group=True names the group in report.undeleted rather than deleting it — CloudWatch is not in the core’s dependency set, and reporting a leak beats reporting a clean teardown over one.

wait_for_terminated=False by default: the caller is on the way out, and a teardown that blocked five minutes on a state nobody reads is five minutes of a CI job. The report then honestly ends in "TERMINATING". True waits for TERMINATED up to the core’s lifecycle default; a number of seconds waits up to that instead.

def terminate_async(
*,
delete_image: bool = False,
delete_log_group: bool = False,
delete_attempts: SupportsIndex | None = None,
delete_backoff: SupportsFloat | SupportsIndex | None = None,
wait_for_terminated: bool | SupportsFloat | SupportsIndex = ...,
) -> TeardownReport

The awaitable twin of terminate, with its keywords; it never raises either. A lifecycle transition: cancelling the awaitable leaves the teardown running to completion, since a teardown abandoned halfway is a leak.

def wait_until_running(
*,
timeout: SupportsFloat | SupportsIndex | None = None,
) -> Session

Finishes a run(wait=False): waits for RUNNING and for the daemon to answer, and returns the session. timeout bounds the wait for RUNNING, as ready_timeout does.

A launch whose client_token adopted an existing, idle-suspended VM resumes it; a fresh launch that reaches a terminal state first raises LaunchDiedError with the service’s stateReason. Refused unless the launch is still PENDING.

def wait_until_running_async(
*,
timeout: SupportsFloat | SupportsIndex | None = None,
) -> Session

The awaitable twin of wait_until_running. Cancelling it stops the wait, not the launch: the VM keeps starting, and a later wait_until_running picks it up.

def __aenter__() -> Sandbox

async with sandbox as s: — returns the sandbox itself.

def __aexit__(
exc_type: Any | None = None,
exc_value: Any | None = None,
traceback: Any | None = None,
) -> bool

__exit__’s teardown, awaited: discards the report, returns False, and, a lifecycle transition, runs to completion even when the task awaiting it is cancelled.

def __enter__() -> Sandbox

with sandbox as s: — returns the sandbox itself.

def __exit__(
exc_type: Any | None = None,
exc_value: Any | None = None,
traceback: Any | None = None,
) -> bool

Tears down on the way out, whatever happened inside the block.

Returns False, so an exception raised inside the block propagates: the teardown is what has to happen, not what has to be reported. Any leak is on the report the caller can also get by calling terminate() themselves.

def __repr__() -> str

This page is generated from bindings/microvms-py/microvms.pyi by Griffe 2.3.0, run from site/scripts/gen-reference.mjs on every pnpm run sync, so an edit made here is overwritten by the next run.

To change the page, change the doc comment in the binding’s Rust source, then regenerate the declarations with mise run stubs. mise run stubs:check fails when the committed file no longer matches the source.