---
title: Session
description: "One running MicroVM's control API, with the proxy auth handled for you."
---

```python
@final
class Session
```

One running MicroVM's control API, with the proxy auth handled for you.

## Constructor

`Session` declares no `__new__` or `__init__`, so it is not constructed directly: an instance comes back from another call in this package.

## Properties

### `agent_token`

```python
agent_token: str
```

The guest bearer credential. Store only in a private encrypted control record.
It is never included in repr or ordinary status output.

### `egress_posture`

```python
egress_posture: str
```

The launch's egress posture: `"open"`, `"unsealed"`, `"best-effort"`, or `"sealed"`.

The value the CLI envelope's `egressPosture` reports for the same launch options, and
what `egress_posture_for` answers before the launch. A session that does not hold its
launch options (`Session.direct`, `Session.attach`, an adopted sandbox) reports
`"unsealed"`. Advertise network isolation only for `"sealed"`.

### `endpoint`

```python
endpoint: str
```

The endpoint this session addresses.

A `String` rather than a `&str` because a sandbox-held session reads it under the
lock, and a reference would outlive the guard.

### `launch_metrics`

```python
launch_metrics: dict | None
```

Launch observations, or None for a session not created by a launch.

### `port`

```python
port: int
```

The port the proxy token is scoped to.

### `proxy_mint_count`

```python
proxy_mint_count: int | None
```

How many proxy tokens this session has minted, or `None` for a direct session.

Exposed because it is the only observable that distinguishes a client which
re-minted after a resume from one that kept a stale token (STATE-8), and a harness
asserting on that behaviour needs to be able to read it. The **token itself** is
not exposed and cannot be: the core's `ProxyToken` has no `Display`, no `as_str`,
and no `Deref`, so "treat `authToken` as a string" is as inexpressible here as it
is there (TRAP-7).

## Methods

### `attach`

```python
@staticmethod
def attach(
    region: Region,
    microvm_id: str,
    endpoint: str,
    agent_token: str,
    *,
    port: SupportsIndex | None = None,
    request_timeout: SupportsFloat | SupportsIndex | None = None,
) -> Session
```

Reattach using a private control record, without bootstrapping again.
AWS credentials mint fresh proxy tokens. This session owns its transport and
shares no sandbox lock; supervisors should attach separately for keepalives,
with a short `request_timeout`. Never publish or log `agent_token`.

### `attach_async`

```python
@staticmethod
def attach_async(
    region: Region,
    microvm_id: str,
    endpoint: str,
    agent_token: str,
    *,
    port: SupportsIndex | None = None,
    request_timeout: SupportsFloat | SupportsIndex | None = None,
) -> Session
```

The awaitable twin of `Session.attach`.

### `connect_headers`

```python
def connect_headers(port: SupportsIndex) -> dict[str, str]
```

Both proxy headers for `port`, so a caller can open its **own** connection.

A `dict[str, str]`, for pairing with `endpoint` when something outside this client
opens a socket to a port on this VM. Minted through the session's existing token
cache, so this does not burn a second control-plane call and does not open a second
refresh schedule (TRAP-9).

**Empty for a direct session**, which is the true answer rather than a missing one: a
daemon reached directly takes no proxy headers.

The auth value is a bearer credential and this returns it. Nothing here logs or
stores it, and no method *takes* a header map — so a caller cannot feed a forged or
expired one back in.

### `connect_headers_async`

```python
def connect_headers_async(port: SupportsIndex) -> dict[str, str]
```

The awaitable twin of `connect_headers`.

### `connect_subprotocols`

```python
def connect_subprotocols(port: SupportsIndex) -> list[str] | None
```

The three WebSocket subprotocols for `port`, in the order a handshake offers them.

A MicroVM endpoint takes the auth and the target port as `Sec-WebSocket-Protocol`
values rather than as headers, because the browser `WebSocket` constructor cannot set
a header — and the platform strips all three before forwarding, so a server inside
the VM never negotiates them.

`None` for a direct session, and deliberately not an empty list: the subprotocol form
exists only for a request through the endpoint proxy, so a list carrying the base
value with no token would open a handshake refused for a reason naming neither the
token nor the port.

The middle string carries the credential. Same rule as `connect_headers`.

### `connect_subprotocols_async`

```python
def connect_subprotocols_async(port: SupportsIndex) -> list[str] | None
```

The awaitable twin of `connect_subprotocols`.

### `direct`

```python
@staticmethod
def direct(endpoint: str, agent_token: str) -> Session
```

A session against a daemon reached **directly**, with no proxy headers.

The shape for a local binary, a test server, or a VM reached over a tunnel. There
is deliberately no constructor that takes a proxy token: minting one is the
control plane's job and it happens inside every request (TRAP-9), so a caller
handing a token in would be handing in one that expires.

### `discovery`

```python
def discovery() -> Discovery | None
```

Image configuration, or None if this daemon predates discovery.

### `discovery_async`

```python
def discovery_async() -> Discovery | None
```

The awaitable twin of `discovery`.

### `download_dir`

```python
def download_dir(
    remote: str,
    local_dir: str | PathLike[str],
    globs: SupportsGetItem[int, str],
) -> list[DownloadedFile]
```

Brings the files of a directory in the VM back under `local_dir`: the regular files
`globs` select, and nothing else.

The daemon packs `remote`, and the archive describes the VM's filesystem, where
untrusted work runs, so core writes only regular-file members that match a glob, never
under `.git` whatever the globs say, and never outside `local_dir`. A symlink, a special
file or a `../` member is skipped, not refused. `["**"]` brings every regular file back.
Returns what was written. A local directory that can't be written to raises
`InvalidArgError`.

### `download_dir_async`

```python
def download_dir_async(
    remote: str,
    local_dir: str | PathLike[str],
    globs: SupportsGetItem[int, str],
) -> list[DownloadedFile]
```

The awaitable twin of `download_dir`. The local writes run on the shared runtime, not
on the event loop.

### `download_file`

```python
def download_file(
    path: str,
    *,
    start_line: SupportsIndex | None = None,
    end_line: SupportsIndex | None = None,
) -> bytes
```

Reads one file, or lines `start_line` through `end_line` of it.

The range is 1-based and inclusive, and the daemon slices the file, so reading lines
40 to 60 of a large log reads those lines alone. Either bound may be `None` (line 1,
through EOF), and an `end_line` past the last line reads through EOF. Line 0 and an end
before the start raise `InvalidArgError` before any request.

### `download_file_async`

```python
def download_file_async(
    path: str,
    *,
    start_line: SupportsIndex | None = None,
    end_line: SupportsIndex | None = None,
) -> bytes
```

The awaitable twin of `download_file`.

### `download_tar`

```python
def download_tar(remote: str) -> bytes
```

The raw tar bytes of a remote tree.

### `download_tar_async`

```python
def download_tar_async(remote: str) -> bytes
```

The awaitable twin of `download_tar`.

### `exec`

```python
def exec(exec_id: str) -> ExecHandle
```

A handle for an exec started earlier, possibly by another process.

The reattach path. Nothing is checked against the daemon here — the handle is an
id plus a transport, and a poll is what discovers whether the exec exists.

### `file_exists`

```python
def file_exists(path: str) -> bool
```

Whether a path exists, distinguishing absence from every other refusal.

### `file_exists_async`

```python
def file_exists_async(path: str) -> bool
```

The awaitable twin of `file_exists`.

### `health`

```python
def health() -> Health
```

Unauthenticated liveness.

### `health_async`

```python
def health_async() -> Health
```

The awaitable twin of `health`.

### `keep_awake`

```python
def keep_awake(
    interval: SupportsFloat | SupportsIndex | None = None,
    *,
    while_busy: bool = False,
    max_duration: SupportsFloat | SupportsIndex | None = None,
    idle_window: SupportsFloat | SupportsIndex | None = None,
    tolerated_errors: SupportsIndex | None = None,
) -> KeepAwake
```

Keeps the VM awake by polling health from this process until stopped.

The platform counts only inbound requests as activity, so an exec working with no
client traffic is suspended once `maxIdleDurationSeconds` passes. This polls
`/v1/health` every `interval` seconds (default: a third of the idle window, at most
20\) on a background task and returns at once. `while_busy` ends it once no exec is
running; `max_duration` ends it after that many seconds. `idle_window` is the VM's
`maxIdleDurationSeconds`: a sandbox-held session knows it, an attached one assumes
the platform minimum of 60, and `interval` may be at most half of it.
`tolerated_errors` is how many retryable poll failures in a row it retries, a second
apart, before it ends with the error; omitted, it's the core's
`DEFAULT_TOLERATED_ERRORS`.

On a sandbox-held session a suspend or terminate through the sandbox ends the
keepalive before its next poll. Stop it before suspending through anything else,
or the next poll auto-resumes the VM. Dropping the returned handle stops it.

### `kill`

```python
def kill(exec_id: str) -> bool
```

Signals an exec's whole process group. Returns whether anything was signalled.

### `kill_async`

```python
def kill_async(exec_id: str) -> bool
```

The awaitable twin of `kill`.

### `port_forward`

```python
def port_forward(
    guest_port: SupportsIndex,
    *,
    bind: str | None = None,
    max_connections: SupportsIndex | None = None,
) -> PortForward
```

Serves a local port as an HTTP and WebSocket forward to `guest_port` in the VM, on a
background task, and returns its handle at once: `microvm port-forward`'s loop.

Connections are served at once rather than one after another, so a slow request
doesn't hold the next. A request the endpoint proxy refuses is listed in the report
with its status while the forward keeps serving. `bind` and `max_connections` are
`tunnel`'s. Dropping the handle stops the forward.

### `port_forward_async`

```python
def port_forward_async(
    guest_port: SupportsIndex,
    *,
    bind: str | None = None,
    max_connections: SupportsIndex | None = None,
) -> PortForward
```

The awaitable twin of `port_forward`, for `tunnel_async`'s reason.

### `procs`

```python
def procs() -> list[ProcGroup]
```

Process accounting: every registered exec with its group's live pids.

The daemon reads `/proc` itself, so this needs no `ps` in the guest. A `ProcGroup`
with `child_exited` and a non-empty `pids` is a command that finished while
something it backgrounded did not; pass its `exec_id` to `kill`.

### `procs_async`

```python
def procs_async() -> list[ProcGroup]
```

The awaitable twin of `procs`.

### `run`

```python
def run(
    command: SupportsGetItem[int, str] | str,
    *,
    shell: bool | str = ...,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    user: SupportsIndex | str | None = None,
    group: SupportsIndex | str | None = None,
    timeout_sec: SupportsFloat | SupportsIndex | None = None,
    stdin: bool = False,
    exec_id: str | None = None,
    reap_group_on_exit: bool = False,
    poweroff_on_exit: bool = False,
    inherit_image_env: bool = False,
) -> ExecHandle
```

Starts a command and returns its handle. Does not wait.

`command` is a list, or a string that becomes a one-element argv — never
whitespace-split. `shell=True` wants a single script string for `/bin/sh -c`;
`shell="bash"` runs it under a shell the daemon resolves in the guest, and a shell
the guest does not have is refused (`unknown_shell`) before anything starts.

`user` and `group` are a numeric id or a name the daemon resolves against the
guest's `/etc/passwd` and `/etc/group`; an unknown name is refused (`unknown_user`,
`unknown_group`) before anything starts. A user with a passwd row gets `HOME`,
`USER` and `LOGNAME` from it, beneath the launch environment and `env`.

`inherit_image_env` starts the child's environment from the image's `ENV` (minus
`AGENTD_*`, never the token), beneath everything else; off by default, which keeps
the child's environment exactly the launch environment plus `env`.

`reap_group_on_exit` asks the daemon to signal the whole process group once the
command's own child exits, so nothing it backgrounded outlives it; off by default,
which keeps the backgrounded-grandchild-output guarantee for callers who rely on it.
`poweroff_on_exit` asks a supporting daemon to power off the whole guest after
the original process group has no live members. Other work ends too, and the
start acknowledgment or result can be lost. It does not keep an idle VM awake.

### `run_async`

```python
def run_async(
    command: SupportsGetItem[int, str] | str,
    *,
    shell: bool | str = ...,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    user: SupportsIndex | str | None = None,
    group: SupportsIndex | str | None = None,
    timeout_sec: SupportsFloat | SupportsIndex | None = None,
    stdin: bool = False,
    exec_id: str | None = None,
    reap_group_on_exit: bool = False,
    poweroff_on_exit: bool = False,
    inherit_image_env: bool = False,
) -> ExecHandle
```

The awaitable twin of `run`, with its keywords. Cancelling it before the daemon
answers may leave the exec started, under the `exec_id` the call was given or minted;
`Session.exec(exec_id)` reaches it, which is why a caller that cancels passes one.

### `run_sync`

```python
def run_sync(
    command: SupportsGetItem[int, str] | str,
    *,
    timeout: SupportsFloat | SupportsIndex = ...,
    shell: bool | str = ...,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    user: SupportsIndex | str | None = None,
    group: SupportsIndex | str | None = None,
    timeout_sec: SupportsFloat | SupportsIndex | None = None,
    stdin: bool = False,
    exec_id: str | None = None,
    reap_group_on_exit: bool = False,
    poweroff_on_exit: bool = False,
    inherit_image_env: bool = False,
) -> ExecResult
```

Start, wait, ack. The one-shot shape, for when output is all you want.

### `run_sync_async`

```python
def run_sync_async(
    command: SupportsGetItem[int, str] | str,
    *,
    timeout: SupportsFloat | SupportsIndex = ...,
    shell: bool | str = ...,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    user: SupportsIndex | str | None = None,
    group: SupportsIndex | str | None = None,
    timeout_sec: SupportsFloat | SupportsIndex | None = None,
    stdin: bool = False,
    exec_id: str | None = None,
    reap_group_on_exit: bool = False,
    poweroff_on_exit: bool = False,
    inherit_image_env: bool = False,
) -> ExecResult
```

The awaitable twin of `run_sync`, with its keywords. Cancelling it stops the wait,
as a `timeout` would: the exec keeps running and its output stays until acked.

### `run_to_completion`

```python
def run_to_completion(
    command: SupportsGetItem[int, str] | str,
    *,
    on_output: Any | None = None,
    shell: bool | str = ...,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    user: SupportsIndex | str | None = None,
    group: SupportsIndex | str | None = None,
    timeout_sec: SupportsFloat | SupportsIndex | None = None,
    exec_id: str | None = None,
    reap_group_on_exit: bool = False,
    poweroff_on_exit: bool = False,
    inherit_image_env: bool = False,
    client_grace_sec: SupportsFloat | SupportsIndex = ...,
) -> ExecResult
```

Start, stream, and collect one command: exactly one `ExecResult` back (BIND-6..10).

With `on_output`, each `OutputChunk` is handed to it as it arrives. The result then
comes from the ack that follows the terminal `exit` event, or, when the stream ends
without one, from a wait and ack. When `timeout_sec + client_grace_sec` passes first
(or, with no `timeout_sec`, the VM's maximum lifetime), the process group is killed
and the exec waited for and acked within `client_grace_sec` once more; if that fails
too the result is synthesized with `posix_exit_code` 124. `posix_exit_code` and
`notes` say what ended the command.

An exception from `on_output` stops delivery; the exec is still waited for and acked
so nothing is left behind, and then the exception is re-raised. `shell`, `user`,
`group`, `reap_group_on_exit`, `poweroff_on_exit` and `inherit_image_env` mean what they mean on `run()`:
`shell="bash"` with a script string runs it under bash, which dash-based images need for
`pipefail`.

### `run_to_completion_async`

```python
def run_to_completion_async(
    command: SupportsGetItem[int, str] | str,
    *,
    on_output: Any | None = None,
    shell: bool | str = ...,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    user: SupportsIndex | str | None = None,
    group: SupportsIndex | str | None = None,
    timeout_sec: SupportsFloat | SupportsIndex | None = None,
    exec_id: str | None = None,
    reap_group_on_exit: bool = False,
    poweroff_on_exit: bool = False,
    inherit_image_env: bool = False,
    client_grace_sec: SupportsFloat | SupportsIndex = ...,
) -> ExecResult
```

The awaitable twin of `run_to_completion`, with its keywords.

`on_output` is a plain function, called on the event loop's thread between the
coroutine's steps, one chunk at a time: the drive on the shared runtime waits for each
call to return before it reads on, so a slow callback slows the stream rather than
buffering it. An exception from it stops delivery and is re-raised once the exec is
waited for and acked, as in `run_to_completion`. Cancelling the call stops the drive and
leaves the exec running in the VM.

### `spawn`

```python
def spawn(
    command: SupportsGetItem[int, str] | str,
    *,
    shell: bool | str = ...,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    user: SupportsIndex | str | None = None,
    group: SupportsIndex | str | None = None,
    timeout_sec: SupportsFloat | SupportsIndex | None = None,
    stdin: bool = False,
    exec_id: str | None = None,
    reap_group_on_exit: bool = False,
    poweroff_on_exit: bool = False,
    inherit_image_env: bool = False,
    offset: SupportsIndex = 0,
    reconnect: bool = True,
    max_reconnects: SupportsIndex | None = None,
    idle_timeout: SupportsFloat | SupportsIndex | None = None,
    gap_policy: str | None = None,
) -> ExecProcess
```

Starts a command and returns it as two byte iterators, a `wait()`, and a `kill()`.

The **process** shape, as against `run`'s handle: the same start request and keyword
arguments, so `spawn` and `run` with one `exec_id` address one server-side child.
`proc.stdout` and `proc.stderr` iterate `bytes`, split out of the one stream the
daemon sends, which reconnects at the byte cursor after a cut, so a suspend and resume
doesn't end them early. Read both sides, from two threads when a command writes much
to both: each holds one unread chunk, like a pipe.

`gap_policy` is what an evicted byte range does. `"error"`, the default when it's
`None`, raises `PlatformError` (wire kind `OutputGap`) from both iterators naming the
range, since the wire can't say which side lost the bytes; `"event"` records it on
`proc.gaps` and keeps both going. `offset`, `reconnect`, `max_reconnects` and
`idle_timeout` are `ExecHandle.stream()`'s.

### `spawn_async`

```python
def spawn_async(
    command: SupportsGetItem[int, str] | str,
    *,
    shell: bool | str = ...,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
    user: SupportsIndex | str | None = None,
    group: SupportsIndex | str | None = None,
    timeout_sec: SupportsFloat | SupportsIndex | None = None,
    stdin: bool = False,
    exec_id: str | None = None,
    reap_group_on_exit: bool = False,
    poweroff_on_exit: bool = False,
    inherit_image_env: bool = False,
    offset: SupportsIndex = 0,
    reconnect: bool = True,
    max_reconnects: SupportsIndex | None = None,
    idle_timeout: SupportsFloat | SupportsIndex | None = None,
    gap_policy: str | None = None,
) -> ExecProcess
```

The awaitable twin of `spawn`, with its keywords. The process it answers iterates
with `async for` as well as `for`; cancelling the call is `run_async`'s.

### `sync_dir`

```python
def sync_dir(
    local_dir: str | PathLike[str],
    *,
    full: bool = False,
    delete_timeout: SupportsFloat | SupportsIndex | None = None,
) -> SyncReport
```

Syncs `local_dir` into the VM's `/workspace` once, uploading only what changed.

Core's one pass, `microvm sync`'s: the local tree is hashed and diffed against the
manifest the last sync left in the VM, the changed members travel as one archive, the
paths gone locally are removed in the VM with one `rm` whose deadline is
`delete_timeout` (core's default when `None`), and the manifest is rewritten. `full=True`
ignores the manifest and uploads everything. `.git`, `target`, `node_modules` and
`.venv` never travel. A tree over the daemon's budgets raises `InvalidArgError` before
anything is sent.

### `sync_dir_async`

```python
def sync_dir_async(
    local_dir: str | PathLike[str],
    *,
    full: bool = False,
    delete_timeout: SupportsFloat | SupportsIndex | None = None,
) -> SyncReport
```

The awaitable twin of `sync_dir`. The hashing and the local reads run on the shared
runtime, not on the event loop.

### `tunnel`

```python
def tunnel(
    guest_port: SupportsIndex,
    *,
    bind: str | None = None,
    verify_identity: TunnelIdentity | None = None,
    max_connections: SupportsIndex | None = None,
) -> Tunnel
```

Serves a local TCP port as a tunnel to `guest_port` in the VM, on a background task,
and returns its handle at once: `microvm tunnel`'s loop.

Each local connection gets a WebSocket of its own through the endpoint proxy, and a
connection the daemon refuses is listed in the report while the tunnel keeps serving.
`bind` is a `host:port` to listen on, loopback on a port the OS picks by default, and
the handle's `local_address` says which. `max_connections` stops accepting after that
many. With `verify_identity` (a `TunnelIdentity`, such as `Sandbox.tunnel_identity`),
each connection first proves the far end is the daemon of the VM that identity was
launched with, and a connection that can't is refused.

The tunnel carries this session's credentials to whoever connects, so bind beyond
loopback only on a network you trust. Dropping the handle stops the tunnel.

### `tunnel_async`

```python
def tunnel_async(
    guest_port: SupportsIndex,
    *,
    bind: str | None = None,
    verify_identity: TunnelIdentity | None = None,
    max_connections: SupportsIndex | None = None,
) -> Tunnel
```

The awaitable twin of `tunnel`: binds the listener on the shared runtime and answers
the same handle, whose loop runs there either way.

### `upload_file`

```python
def upload_file(path: str, data: bytes, *, mode: str | None = None) -> None
```

Writes one file, creating parents. `mode` is an **octal string** (`"0755"`), which
is the daemon's shape — an integer here would be ambiguous between 0o755 and 755.

### `upload_file_async`

```python
def upload_file_async(path: str, data: bytes, *, mode: str | None = None) -> None
```

The awaitable twin of `upload_file`.

### `upload_tar`

```python
def upload_tar(remote: str, archive: bytes) -> None
```

Extracts pre-built tar bytes under `remote`.

Bytes rather than a local path: packing a directory is the caller's, because the
symlink and permission decisions in a pack belong to whoever knows what the tree
means.

### `upload_tar_async`

```python
def upload_tar_async(remote: str, archive: bytes) -> None
```

The awaitable twin of `upload_tar`.

### `wait_until_ready`

```python
def wait_until_ready(timeout: SupportsFloat | SupportsIndex = ...) -> Health
```

Polls health until the daemon reports bootstrapped.

Connection errors on the way are expected rather than exceptional: a VM that has
just reached RUNNING commonly refuses a connection or two before the proxy path is
wired up. A *fatal* error ends the wait at once, because retrying a 401 until the
deadline is the mistake the retryable split exists to prevent.

### `wait_until_ready_async`

```python
def wait_until_ready_async(timeout: SupportsFloat | SupportsIndex = ...) -> Health
```

The awaitable twin of `wait_until_ready`.

## Special methods

### `__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.