---
title: Sandbox
description: "One MicroVM's whole life."
---

```python
@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.

## Constructor

```python
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.

## Properties

### `adopted`

```python
adopted: bool
```

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

### `bootstrap_count`

```python
bootstrap_count: int
```

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

### `endpoint`

```python
endpoint: str | None
```

The proxy endpoint, once launched.

### `image`

```python
image: Image | None
```

The image, once built.

### `image_exists`

```python
image_exists: bool
```

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

### `is_detached`

```python
is_detached: bool
```

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

### `launch_image_arn`

```python
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`

```python
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`

```python
microvm_id: str | None
```

The VM id, once launched.

### `session`

```python
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`

```python
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`

```python
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`

```python
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`

```python
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`

```python
was_terminated: bool
```

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

## Methods

### `adopt`

```python
@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.

### `adopt_async`

```python
@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`.

### `build_artifact`

```python
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.

### `build_image`

```python
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`.

### `build_image_async`

```python
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.

### `create_async`

```python
@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.

### `detach`

```python
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.

### `ensure_image`

```python
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.

### `ensure_image_async`

```python
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.

### `from_name`

```python
@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.

### `from_name_async`

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

The awaitable twin of `Sandbox.from_name`.

### `managed_base_versions`

```python
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.

### `managed_base_versions_async`

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

The awaitable twin of `managed_base_versions`.

### `preflight`

```python
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.

### `resume`

```python
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.

### `resume_async`

```python
def resume_async() -> Session
```

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

### `run`

```python
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.

### `run_async`

```python
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.

### `suspend`

```python
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.

### `suspend_async`

```python
def suspend_async() -> str
```

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

### `terminate`

```python
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.

### `terminate_async`

```python
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.

### `wait_until_running`

```python
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.

### `wait_until_running_async`

```python
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.

## Special methods

### `__aenter__`

```python
def __aenter__() -> Sandbox
```

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

### `__aexit__`

```python
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.

### `__enter__`

```python
def __enter__() -> Sandbox
```

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

### `__exit__`

```python
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.

### `__repr__`

```python
def __repr__() -> str
```

## Provenance

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.