Metadata-Version: 2.4
Name: instavm
Version: 0.22.0
Summary: Official Python SDK and CLI for InstaVM APIs
Author-email: InstaVM <hello@instavm.io>
License: MIT
Project-URL: Documentation, https://instavm.io/docs/sdks/python/overview
Project-URL: Changelog, https://instavm.io/docs/sdks/python/changelog
Project-URL: Support, https://instavm.io/support
Project-URL: Source, https://github.com/BandarLabs/sandbox_client
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests
Requires-Dist: PyYAML<7,>=6
Requires-Dist: websockets<16,>=12.0
Requires-Dist: rich<15,>=13
Provides-Extra: agents
Requires-Dist: openai-agents<0.15,>=0.14.1; extra == "agents"
Provides-Extra: pty
Dynamic: license-file

# InstaVM Python SDK + CLI

Official Python SDK and installed CLI for [InstaVM](https://instavm.io). Use it to manage VMs, snapshots, shares, volumes, desktops, account settings, and code execution from Python or your shell.

## Installation

```bash
pip install instavm
```

**Requirements:** Python 3.10+

## Table of Contents

- [CLI](#cli)
  - [Auth & Config](#auth--config)
  - [Common Commands](#common-commands)
  - [Cookbooks](#cookbooks)
  - [Deploy](#deploy)
  - [YAML v2 Guide](#yaml-v2-guide)
  - [Command Reference](#command-reference)
- [Library Quick Start](#library-quick-start)
- [Code Execution](#code-execution)
  - [Sessions & Configuration](#sessions--configuration)
  - [File Operations](#file-operations)
  - [Async Execution](#async-execution)
- [Sessions & Sandboxes](#sessions--sandboxes)
- [VMs & Snapshots](#vms--snapshots)
- [Volumes](#volumes)
  - [Volume CRUD & Files](#volume-crud--files)
  - [VM Volume Attachments](#vm-volume-attachments)
- [Networking](#networking)
  - [Egress, Shares, SSH](#egress-shares-ssh)
- [Browser Automation](#browser-automation)
  - [Basic Browser Flow](#basic-browser-flow)
  - [Interactions: Click, Type, Fill, Scroll](#interactions-click-type-fill-scroll)
  - [Content Extraction](#content-extraction)
- [Computer Use](#computer-use)
- [PTY (Interactive Terminals)](#pty-interactive-terminals)
- [OpenAI Agents SDK](#openai-agents-sdk-sandbox-provider)
- [Secrets Vault](#secrets-vault)
- [Time Travel / Time Machine](#time-travel--time-machine)
- [Platform APIs](#platform-apis)
- [Error Handling](#error-handling)
- [Development & Testing](#development--testing)
- [Changelog](#changelog)

---

## CLI

`pip install instavm` installs the `instavm` command in the active Python environment.

```bash
instavm --help
python -m instavm.cli --help
```

The CLI stores defaults in `~/.instavm/config.json`, checks `INSTAVM_API_KEY` when no key is stored, and also respects `INSTAVM_BASE_URL` and `INSTAVM_SSH_HOST`. `instavm login` is the easiest way to populate the config, see [Auth & Config](#auth--config) below.

Multi-step commands (`login`, `vm create`, `cookbook deploy`, …) render a Clack-inspired progress rail with a single spinner per step. The UI auto-degrades to plain text when output is not a TTY, when `NO_COLOR` is set, when `TERM=dumb`, or when `--json` is passed. `--json` payloads on stdout are unchanged.

### Auth & Config

The fastest way to authenticate is the browser-based login (recommended for laptops/workstations):

```bash
instavm login
```

This opens your browser, asks you to confirm the pairing on the dashboard,
and writes a labelled API key (`CLI - <hostname>`) back to your local config.
The key never touches the URL bar, it's exchanged server-to-server over a
local loopback callback using PKCE. To switch accounts, run
`instavm auth logout` then `instavm login` again.

For headless environments (CI, servers without a browser) paste an existing key:

```bash
instavm auth set-key                          # prompts for the key (stdin / hidden)
printf '%s' "$INSTAVM_API_KEY" | instavm auth set-key
instavm auth status
```

#### Pointing the CLI at a non-production environment

The CLI honors `INSTAVM_BASE_URL` (and the `--base-url` flag). To run the same
flow against staging, useful for verifying a release before it reaches
production:

```bash
export INSTAVM_BASE_URL=https://api.staging.instavm.io
instavm login          # browser will open the staging dashboard
```

The login flow always opens the dashboard URL returned by the API for the
configured base URL, so a single `INSTAVM_BASE_URL` switches both the API and
the browser side of the handshake.

If your dashboard's API key gets revoked from the web UI, the next CLI call
will print a hint to re-run `instavm login`.

### Common Commands

```bash
instavm whoami
instavm ls
instavm ls -a
instavm ls --watch
instavm create --type computer-use --memory 4096
instavm connect vm_123
instavm pty vm_123                           # interactive PTY (like docker exec -it)
instavm pty vm_123 /bin/zsh                  # custom shell program
instavm deploy
instavm deploy --plan
instavm egress get --vm vm_123
instavm exec --cmd "print('hello from CLI')" --language python
instavm exec ./script.py
instavm browser read https://example.com
instavm browser screenshot https://example.com --out page.png
instavm browser session create
instavm browser navigate https://example.com --session $SID
instavm browser click "button#submit" --session $SID
instavm browser type "input[name=q]" "hello world" --session $SID
instavm browser fill "input[name=email]" "user@example.com" --session $SID
instavm browser scroll --session $SID --y 500
instavm browser extract --session $SID --selector "a"
instavm browser session close $SID
instavm snapshot ls
instavm snapshot get <snapshot_id> --watch
instavm volume ls
instavm volume files upload <volume_id> ./README.md --path docs/README.md
instavm share create vm_123 3000 --public
instavm share set-private <share_id>
instavm ssh-key list
instavm desktop viewer <session_id>
instavm doc
instavm billing
```

`instavm ls` shows active VMs only. Use `-a` or `--all` to include terminated VM records. On ANSI terminals the human-readable list uses colored status badges, and both `instavm ls` and `instavm snapshot get` support `--watch` for periodic refreshes.

### Cookbooks

`instavm cookbook` pulls curated starter apps from the public [`instavm/cookbooks`](https://github.com/instavm/cookbooks) catalog, creates a VM, starts the service, creates the share, and returns the public URL.

```bash
instavm cookbook list
instavm cookbook info neon-city-webgl
instavm cookbook deploy neon-city-webgl
instavm cookbook deploy hello-fastapi
```

The CLI syncs the cookbook repo into `~/.instavm/cookbooks/` (which **requires Git**, only when you run `instavm cookbook …` without `INSTAVM_COOKBOOKS_DIR`), checks for `ssh`, `scp`, and `tar` before every deploy, prompts for any required secrets, and auto-registers a local public SSH key if your account does not already have one.

### Deploy

`instavm deploy` tries to deploy the app in the current directory without asking you to create an `instavm.yaml` first. It detects a simple Node.js or Python web app, creates a VM, uploads the project, starts the service, and gives you a share URL.

```bash
instavm deploy
instavm deploy --plan
instavm deploy ./path/to/app
instavm deploy -f instavm.web.yaml
```

`--plan` shows the detected runtime, install command, start command, port, and secrets without creating a VM.

When a project has an `instavm.yaml`, `instavm deploy` uses it instead of zero-config detection. Manifest `schema_version: 2` is accepted alongside v1 and adds a top-level lifecycle `kind`:

```yaml
schema_version: 2
kind: service  # service | cron | job
deploy:
  kind: upload_and_run  # deploy.source is also accepted as an alias
```

`kind: service` deploys through the existing VM/share flow. `kind: cron` and `kind: job` are parsed and shown by `--plan`; deployment fails with a clear unsupported-control-plane message until the scheduler/job backend APIs are available in this CLI. Use `-f/--file` to target manifests such as `instavm.web.yaml` or `instavm.cron.yaml`.

`instavm deploy` is experimental right now. The zero-config path is working best for straightforward Node.js and Python apps. Some runtimes and projects still need follow-up fixes or backend support.

### YAML v2 Guide

The supported YAML v2 manifest guide lives in [`docs/instavm_yaml_v2.md`](docs/instavm_yaml_v2.md).
It explains the schema, the major blocks, and the quickest way to use a v2 manifest with `instavm deploy`.

### Command Reference

- `auth`: `set-key`, `status`, `logout`
- `whoami`: show account details and SSH keys
- `ls`/`list`: show active VMs by default; use `-a` or `--all` for all VM records
- `cookbook`: `list`, `info`, `deploy` for curated starter apps from `instavm/cookbooks`
- `deploy`: experimental zero-config deploy for the current app directory
- `egress`: `get`, `set` for session and VM network egress policy
- `exec`: run inline code or a local file, plus `result` for async task lookup
- `pty`: open an interactive PTY inside a VM (like `docker exec -it`)
- `browser`: `read`, `screenshot`, `navigate`, `click`, `type`, `fill`, `scroll`, `wait`, `extract`, `session`
- `create`/`new`, `rm`/`delete`, `clone`, `connect`: core VM workflows
- `snapshot`: `ls`, `create`, `build`, `get`, `rm`
- `desktop`: `status`, `start`, `stop`, `viewer`
- `volume`: `ls`, `get`, `create`, `update`, `rm`, `checkpoint`, `files`
- `share`: `create`, `set-public`, `set-private`, `revoke`
- `ssh-key`: `list`, `add`, `remove`
- `vm`/`vms`: VM-scoped operations mirroring `client.vms.*`: `update` (live `--memory-mb`, `--snapshot-on-terminate`, `--snapshot-name`)
- `desktop`: now also hosts `recordings` (`ls`, `get`, `download`, `rm`) for computer-use session video captures
- `billing`: defaults to printing the Stripe portal URL; subcommands cover the full credit/usage surface: `portal`, `status`, `allocation`, `usage` (daily $ breakdown), `usage-history` (per-event rows), `check`, `rates`, `trends`, `forecast`
- `doc`/`docs`: documentation links

All leaf commands support `--json`. Share visibility updates use `share_id`, which matches the public API.

## Library Quick Start

```python
import os
from instavm import InstaVM

client = InstaVM(
    api_key=os.environ.get("INSTAVM_API_KEY"),
    auto_start_session=False,
)

me = client.get_current_user()
vms = client.vms.list()

print(me["email"])
print(len(vms))
```

---

## Code Execution

### Sessions & Configuration

```python
from instavm import InstaVM

client = InstaVM(
    api_key="your_api_key",
    cpu_count=2,
    memory_mb=1024,
    env={"APP_ENV": "dev"},
    metadata={"team": "platform"},
)

result = client.execute("print('session id:', 'ok')")
print(result)
print(client.session_id)
```

### File Operations

```python
client = InstaVM(api_key="your_api_key")

client.upload_file("local_script.py", "/app/local_script.py")
client.execute("python /app/local_script.py", language="bash")
client.download_file("output.json", local_path="./output.json")
```

### Async Execution

```python
client = InstaVM(api_key="your_api_key")

task = client.execute_async("sleep 5 && echo 'done'", language="bash")
result = client.get_task_result(task["task_id"], poll_interval=2, timeout=60)
print(result)
```

---

## Sessions & Sandboxes

```python
client = InstaVM(api_key="your_api_key")

# Get the publicly-reachable app URL (optionally for a specific port)
app_url = client.get_session_app_url(port=8080)
print(app_url.get("app_url"))

# List sandbox records with optional metadata filter and limit
sandboxes = client.list_sandboxes(metadata={"env": "production"}, limit=50)
print(len(sandboxes))
```

---

## VMs & Snapshots

```python
client = InstaVM(api_key="your_api_key")

# Create a basic VM
vm = client.vms.create(wait=True, metadata={"purpose": "dev"})

# Create a VM with pre-attached volumes
vm_with_vols = client.vms.create(
    wait=True,
    volumes=[{"volume_id": "vol_abc", "mount_path": "/data", "mode": "rw"}],
)

# List VMs
vms = client.vms.list()                 # GET /v1/vms  (running)
all_records = client.vms.list_all_records()  # GET /v1/vms/ (all records)

# Snapshot a running VM
snap_from_vm = client.vms.snapshot(vm_id=vm["vm_id"], wait=True, name="dev-base")

# Build a snapshot from an OCI image
snap_from_oci = client.snapshots.create(
    oci_image="docker.io/library/python:3.11-slim",
    name="python-3-11-dev",
    vcpu_count=2,
    memory_mb=1024,
    snapshot_type="user",
    build_args={
        "git_clone_url": "https://github.com/example/repo.git",
        "git_clone_branch": "main",
        "envs": {"PIP_INDEX_URL": "https://pypi.org/simple"},
    },
)

user_snaps = client.snapshots.list(snapshot_type="user")
```

---

## Volumes

### Volume CRUD & Files

```python
client = InstaVM(api_key="your_api_key")

# Create
volume = client.volumes.create(name="project-data", quota_bytes=10 * 1024 * 1024 * 1024)
volume_id = volume["id"]

# Read / Update
client.volumes.list(refresh_usage=True)
client.volumes.get(volume_id, refresh_usage=True)
client.volumes.update(volume_id, name="project-data-v2", quota_bytes=20 * 1024 * 1024 * 1024)

# File operations
client.volumes.upload_file(volume_id, file_path="./README.md", path="docs/README.md", overwrite=True)
files = client.volumes.list_files(volume_id, prefix="docs/", recursive=True, limit=1000)
download = client.volumes.download_file(volume_id, path="docs/README.md")
client.volumes.delete_file(volume_id, path="docs/README.md")

# Checkpoints
checkpoint = client.volumes.create_checkpoint(volume_id, name="pre-release")
client.volumes.list_checkpoints(volume_id)
client.volumes.delete_checkpoint(volume_id, checkpoint["id"])

# Cleanup
client.volumes.delete(volume_id)
```

### VM Volume Attachments

```python
vm = client.vms.create(wait=True)
vm_id = vm["vm_id"]

client.vms.mount_volume(vm_id, volume_id, mount_path="/data", mode="rw", wait=True)
client.vms.list_volumes(vm_id)
client.vms.unmount_volume(vm_id, volume_id, mount_path="/data", wait=True)
```

---

## Networking

### Egress, Shares, SSH

```python
client = InstaVM(api_key="your_api_key")

# Egress policy
policy = client.set_session_egress(
    allow_package_managers=True,
    allow_http=False,
    allow_https=True,
    allowed_domains=["pypi.org", "files.pythonhosted.org"],
)

# Public/private share links
share = client.shares.create(port=3000, is_public=False)
client.shares.update(share_id=share["share_id"], is_public=True)

# SSH key registration
key = client.add_ssh_key("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... user@host")
```

---

## Browser Automation

### Basic Browser Flow

```python
client = InstaVM(api_key="your_api_key")

session = client.browser.create_session(viewport_width=1366, viewport_height=768)
session.navigate("https://example.com")
links = session.extract_elements("a", ["text", "href"])
shot_b64 = session.screenshot(full_page=True)
session.close()
```

### Interactions: Click, Type, Fill, Scroll

All interaction methods use **CSS selectors** to target elements (not pixel coordinates):

```python
client = InstaVM(api_key="your_api_key")

with client.browser.create_session() as session:
    session.navigate("https://www.google.com")

    # Type into an input field (keystroke-by-keystroke with configurable delay)
    session.type("textarea[name=q]", "InstaVM cloud VMs", delay=50)

    # Fill a form field (clears existing value first, sets value instantly)
    session.fill("input[name=email]", "user@example.com")

    # Click an element (supports force-click for obscured elements)
    session.click("button[type=submit]", force=False)

    # Scroll the page or a specific element
    session.scroll(y=500)
    session.scroll(selector="#results")

    # Wait for a condition before continuing
    session.wait_for("visible", selector="#results", timeout=5000)

    # Extract DOM elements with specific attributes
    elements = session.extract_elements("a.result-link", ["href", "text"])
    for el in elements:
        print(el["href"], el["text"])

    # Take a screenshot
    session.screenshot(full_page=True)
```

### Content Extraction

LLM-friendly extraction with optional interactive-element and anchor discovery:

```python
client = InstaVM(api_key="your_api_key")

content = client.browser.extract_content(
    url="https://example.com/docs",
    include_interactive=True,
    include_anchors=True,
    max_anchors=30,
)

print(content["readable_content"].get("title"))
for anchor in (content.get("content_anchors") or [])[:5]:
    print(anchor.get("text"), anchor.get("selector"))
```

---

## Computer Use

Control a full desktop environment inside a VM session:

```python
client = InstaVM(api_key="your_api_key")

session_id = client.session_id

# Viewer URL and state
viewer = client.computer_use.viewer_url(session_id)
state = client.computer_use.get(session_id, "/state")

# Proxy methods (GET, POST, HEAD)
head_resp = client.computer_use.head(session_id, "/state")

# VNC websockify URL for remote desktop streaming
vnc = client.computer_use.vnc_websockify(session_id)
```

---

## PTY (Interactive Terminals)

Create and manage interactive pseudo-terminal sessions inside VMs. Used by the OpenAI Agents SDK's `Shell` capability for `write_stdin` support.

```bash
pip install instavm[pty]  # adds websockets dependency
```

```python
import asyncio
from instavm import InstaVM

client = InstaVM(api_key="your_api_key")
session_id = client.session_id

# Create a PTY session
pty_info = client.pty.create(session_id, cols=120, rows=40)
pty_id = pty_info["session_id"]

# List / get / resize / kill PTY sessions
sessions = client.pty.list(session_id)
info = client.pty.get(session_id, pty_id)
client.pty.resize(session_id, pty_id, cols=200, rows=50)
client.pty.kill(session_id, pty_id)

# WebSocket URL for interactive I/O (use with websockets library)
ws_url = client.pty.ws_url(session_id, pty_id)
```

### WebSocket Protocol

Connect to `ws_url` for bidirectional PTY I/O:

- **Client → Server:** Binary frames = stdin, Text frames = JSON control (`{"type": "resize", "cols": N, "rows": N}`)
- **Server → Client:** Binary frames = stdout/stderr, Text frame = exit notification (`{"type": "exit", "exit_code": N}`)

### OpenAI Agents SDK Integration

With PTY support enabled, the `Shell` capability's `write_stdin` tool works automatically:

```python
from agents.sandbox import SandboxAgent

agent = SandboxAgent(
    name="Developer",
    model="gpt-5.4",
    instructions="Run interactive commands and inspect output.",
)
# Shell tool's write_stdin is available when supports_pty() returns True
```

---

## OpenAI Agents SDK: Sandbox Provider

To use InstaVM as a sandbox backend for the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python), install the `agents` extra:

```bash
pip install instavm[agents]
```

This pulls in `openai-agents>=0.14.1,<0.15` and registers InstaVM as a sandbox provider automatically. Each agent runs in its own cloud VM with filesystem, shell, networking, and snapshot support.

```python
import asyncio, os
from agents import Runner, RunConfig
from agents.sandbox import SandboxAgent, SandboxRunConfig
from instavm.integrations.openai_agents import (
    InstaVMSandboxClient,
    InstaVMSandboxClientOptions,
)

agent = SandboxAgent(
    name="Analyst",
    model="gpt-5.4",
    instructions="Analyze workspace files and answer concisely.",
)

async def main():
    client = InstaVMSandboxClient(api_key=os.environ["INSTAVM_API_KEY"])
    result = await Runner.run(
        agent,
        "What OS is this sandbox running?",
        run_config=RunConfig(
            sandbox=SandboxRunConfig(
                client=client,
                options=InstaVMSandboxClientOptions(memory_mb=1024),
            ),
        ),
    )
    print(result.final_output)

asyncio.run(main())
```

**Features:** streaming, resume & snapshots, persistent volumes, egress control, cloud bucket mounts, exposed ports, and more.

📖 **[Full documentation](docs/openai-agents.md)** · 📂 **[Examples](examples/sandbox/)**

---

## Secrets Vault

Store API keys (OpenAI, Anthropic, Stripe, GitHub, …) in InstaVM once, then **bind**
them to upstream hosts. Real values never leave the server: the in-VM transparent
MITM proxy substitutes them at TLS-write time, so any HTTPS SDK in any language
works unchanged. Code inside the VM only ever sees a placeholder name like
`OPENAI_KEY`; the wire carries the real token.

A vault is a unit of secret + access policy. Inside a vault:

- a **credential** is a named secret (`OPENAI_KEY`, `STRIPE_SECRET`, …).
- a **service** is a binding that says *"when this VM hits `api.openai.com`, inject
  credential `OPENAI_KEY` as a Bearer token"*. Use `--template openai|anthropic|…`
  for the built-in catalog or `--host` for a custom upstream.

### Vault CLI (`instavm vault …`, aliases `secrets`, `secret`)

| Subcommand | What it does |
|---|---|
| `vault list` / `vault ls` | List vaults you own. |
| `vault create <name> [--description …]` | Create a new vault. |
| `vault get <vault-id>` | Vault metadata. |
| `vault update <vault-id> [--name …] [--description …]` | Rename / re-describe. |
| `vault delete <vault-id>` / `vault rm` | Delete vault and all credentials. Asks for confirmation unless `--yes`. |
| `vault discover <vault-id>` | Safe summary (credential names + bound services, **no values**). |
| `vault catalog` | List built-in service templates (openai, anthropic, stripe, github, …). |
| `vault logs <vault-id> [--limit N]` | Recent request-log entries (which credential was injected, when, by which VM). |
| `vault setup [PATH]` | **Interactive bootstrap for a cookbook.** Reads `instavm.yaml`, scans org vaults, and walks you through creating + binding any missing host. Same flow `instavm deploy` triggers automatically when `vault.required: true` and nothing matches. Runs idempotently, re-running on a fully-covered cookbook is a no-op (`already_covered`). |

### Credentials (`instavm vault secret …`)

| Subcommand | What it does |
|---|---|
| `vault secret list <vault-id>` / `ls` | List credential names + ids in a vault. **Never returns values.** |
| `vault secret set <vault-id> <name>` | Add a credential. Value comes from `--value-file`, stdin, an interactive `getpass` prompt (default, never echoed, never recorded in shell history), or `--value` (last-resort; visible in shell history). `--type api_key` (default), `--description …`. |
| `vault secret rotate <vault-id> <credential-id>` | Replace a value while keeping the same name and id. Same value-source rules as `set`. |
| `vault secret delete <vault-id> <credential-id>` / `rm` | Delete a credential. |

```bash
# CI-friendly: stream a value from a process substitution, no shell history.
INSTAVM_API_KEY=$INSTAVM_API_KEY instavm vault secret set $VAULT_ID OPENAI_KEY \
  --value-file <(printf '%s' "$OPENAI_KEY")
```

### Service bindings (`instavm vault service …`)

| Subcommand | What it does |
|---|---|
| `vault service list <vault-id>` / `ls` | List bindings on a vault. |
| `vault service add <vault-id> --template openai \| --host api.example.com [--auth-type bearer\|header\|url-path] [--header X-API-Key] [--placeholder TOKEN] [--credential OPENAI_KEY] [--description …] [--disabled]` | Bind a credential to an upstream host. `--template` and `--host` are mutually exclusive, exactly one is required. Use `--auth-type url-path` for upstreams that embed the token in the URL path (e.g. Telegram's `/bot<TOKEN>/...`); `--placeholder` names the sentinel the in-VM caller will use (defaults to `--credential`). |
| `vault service remove <vault-id> <service-id>` / `rm` | Remove a binding. |

### SDK (`client.list_vaults`, `client.create_vault`, …)

```python
import os

from instavm import InstaVM

with InstaVM(api_key="…") as client:
    vault = client.create_vault("prod-keys", description="Shared agent secrets")

    # Add a credential, value is sent over TLS to the platform, then stored encrypted.
    cred = client.add_vault_credential(
        vault["id"], name="OPENAI_KEY", value=os.environ["OPENAI_API_KEY"],
        credential_type="api_key", description="Team OpenAI key",
    )

    # Bind it to api.openai.com using the built-in template.
    client.add_vault_services_from_templates(vault["id"], template_ids=["openai"])

    # Or bind a custom host:
    client.add_vault_service(
        vault["id"], host="api.example.com",
        auth_config={"type": "bearer", "token": "OPENAI_KEY"},
    )

    # Path-based auth (e.g. Telegram embeds the bot token in /bot<TOKEN>/…).
    # The placeholder is the sentinel the in-VM code will embed; the MITM
    # proxy swaps it for the real value before the request leaves the VM.
    client.add_vault_credential(
        vault["id"], name="TELEGRAM_BOT_TOKEN", value=os.environ["TELEGRAM_BOT_TOKEN"],
    )
    client.add_vault_services_from_templates(vault["id"], template_ids=["telegram"])
    # …or the same thing manually:
    client.add_vault_service(
        vault["id"], host="api.telegram.org",
        auth_config={
            "type": "url-path",
            "token": "TELEGRAM_BOT_TOKEN",
            "placeholder": "TELEGRAM_BOT_TOKEN",
        },
    )

    # Audit / inspect, values are never returned, only names + metadata.
    client.discover_vault(vault["id"])
    client.list_vault_credentials(vault["id"])
    client.list_vault_services(vault["id"])
    client.get_vault_request_logs(vault["id"], limit=50)

    # Lifecycle
    client.rotate_vault_credential(vault["id"], cred["id"], value=os.environ["OPENAI_API_KEY_NEW"])
    client.delete_vault_credential(vault["id"], cred["id"])
    client.delete_vault(vault["id"])
```

Once a VM is launched against the vault, code inside the sandbox can do this and
the real `OPENAI_API_KEY` is substituted on the wire, even though the VM's
environment only has the placeholder:

```python
# Inside the VM:
import os
from openai import OpenAI

# OPENAI_API_KEY is a placeholder string; the MITM swaps it with the real value.
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
client.chat.completions.create(model="gpt-4o-mini", messages=[…])
```

The same pattern works for upstreams that carry the credential in the URL path
instead of a header. The bot token is **never** present in the VM environment;
the MITM proxy injects it on the wire:

```python
# Inside the VM (Telegram example):
import os, requests

# TELEGRAM_BOT_TOKEN is just the literal string "TELEGRAM_BOT_TOKEN" inside the VM.
# The MITM proxy swaps it for the real value before the request leaves the VM.
token = os.environ.get("TELEGRAM_BOT_TOKEN", "TELEGRAM_BOT_TOKEN")
requests.get(f"https://api.telegram.org/bot{token}/getMe").raise_for_status()
```

### Supported injection modes

| `auth_type` | Where the credential lands on the wire | Typical templates |
|---|---|---|
| `bearer` | `Authorization: Bearer <secret>` header | `openai`, `openrouter` |
| `api-key` (a.k.a. `header`) | Custom header (`x-api-key`, `Authorization`, …) with optional prefix | `anthropic`, `gemini`, `stripe` |
| `basic` | `Authorization: Basic base64(user:pass)` header | `jira` |
| `custom` | One or more custom headers built from `{{ CRED }}` placeholders | enterprise APIs |
| `url-path` | Substring substitution inside the request URL path | `telegram` |
| `passthrough` | Allowlist-only; request passes through unmodified | audit-only hosts |

For the full list of verified-compatible HTTPS clients (Python, Node, Go, Java,
Ruby, PHP, Perl, curl, wget, …), see
[VAULT_SDK_COMPATIBILITY](https://github.com/BandarLabs/sandbox/blob/aws-staging/docs/VAULT_SDK_COMPATIBILITY.md).

---

## Time Travel / Time Machine

Every InstaVM session can be recorded as a **tape**, an append-only log of state
changes inside the VM. A tape captures:

| Lane | What it records | Source |
|---|---|---|
| `fs_summary` | filesystem step checkpoints (changed paths) | platform |
| `http_egress` | every outbound HTTP/HTTPS request from the VM | MITM proxy (auto) |
| `llm_call` | OpenAI / Anthropic / Bedrock / Gemini calls | MITM proxy (auto) |
| `tool_call_start` / `tool_call_end` | agent tool invocations | `ToolCallRecorder` |
| `vnc_marker` | desktop interaction markers | platform |
| `agent_log` / `branch` | freeform agent annotations and fork points | SDK |

Once a tape is recording, the platform's MITM proxy auto-emits `http_egress` and
`llm_call` events. **No SDK shim required** for those. The result is a tape you can
*scrub*, *diff*, *branch* (fork the VM at a past step), or *replay LLM calls offline*.

### CLI subcommands (`instavm tape …`)

| Subcommand | What it does |
|---|---|
| `tape ls` | List tapes. Filters: `--session`, `--vm`, `--status {recording\|stopped\|failed\|expired}`, `--limit`. |
| `tape start <session-id>` | Start recording. `--no-record-fs` to skip fs checkpoints. `--record-egress-content` to capture request/response bodies (when cassette support is enabled). `--retention-days N` (default 7). |
| `tape stop <tape-id>` | Stop a recording and finalize totals. |
| `tape get <tape-id>` | Show tape metadata (status, step count, retention, session/VM). |
| `tape events <tape-id>` | Stream events. `--after-step N` for incremental tail, `--kind fs_summary\|http_egress\|llm_call\|…`, `--limit`. |
| `tape lanes <tape-id>` | Per-lane event counts, the data behind a scrubber UI. |
| `tape play <tape-id>` | ASCII multi-track timeline + tail of recent events. `--limit`, `--tail`. |
| `tape diff <tape-id> --from S1 --to S2` | Filesystem changed-paths between two steps of the **same** tape. |
| `tape branch <tape-id> --at S` | Spawn a new VM/session forked from this tape at step `S`. `--mode live\|replay`. Prints `ssh <vm_id>@<host>` for the fork. |
| `tape export <tape-id> [--output file.json]` | Self-contained JSON bundle (metadata + events) for archival or external playback. |
| `tape rm <tape-id>` | Delete a tape. |

### SDK (`client.tapes`, `client.tape()`)

```python
from instavm import InstaVM

# InstaVM is itself a context manager; constructor auto-starts a session.
with InstaVM(api_key="…") as client:
    # tape with-block: starts on enter, stops + finalizes on exit (even on exceptions)
    with client.tape(record_fs=True, retention_days=7) as tape:
        client.execute("mkdir -p /data && echo hi > /data/note.md")
        client.execute("curl -s https://news.ycombinator.com -o /data/hn.html")
    print("recorded:", tape.id)

    # Inspect, same names as the CLI. events() returns a list[dict].
    client.tapes.list(session_id=…, vm_id=…, status="stopped")
    client.tapes.get(tape.id)
    for ev in client.tapes.events(tape.id, after_step=0, kind=None, limit=200):
        print(ev["kind"], ev.get("step_id"))
    client.tapes.lanes(tape.id)
    client.tapes.diff(tape.id, from_step=10, to_step=42)
    fork = client.tapes.branch(tape.id, at_step=42, mode="live")  # → new vm_id
    client.tapes.export(tape.id)                                  # full JSON bundle
    client.tapes.delete(tape.id)
```

### Recording agent tool calls

`ToolCallRecorder` is a generic middleware that emits `tool_call_start` /
`tool_call_end` events around any agent-loop tool invocation:

```python
from instavm.sandbox_client import ToolCallRecorder

recorder = ToolCallRecorder(client.tapes, tape.id)
with recorder.span("python_exec", payload={"code": "1+1"}) as step:
    out = client.execute("python -c 'print(1+1)'")
    step.attach({"output": out.get("stdout") if isinstance(out, dict) else out})
```

### Replaying LLM calls offline (cassette)

Once a tape has recorded `llm_call` events, `make_openai_client(tape_id=…)` returns a
ready-to-use `openai.OpenAI` instance whose HTTP transport is backed by the cassette -
no network, no API key, deterministic outputs:

```python
from instavm.cassette_replay import make_openai_client

oai = make_openai_client(tape_id=tape.id)   # returns a pre-wired openai.OpenAI client
oai.chat.completions.create(model="gpt-5-nano", messages=[…])
```

For other vendor SDKs (Anthropic, Bedrock, …) drop down a layer:

```python
import httpx
from instavm.cassette_replay import CassetteReplayClient

transport = CassetteReplayClient(tape_id=tape.id, strict=True).as_httpx_transport()
http = httpx.Client(transport=transport)
# pass `http` into your SDK's http_client= argument
```

Lookup is keyed by `(METHOD, URL, sha256(body))`; repeated identical requests are served
in cassette order so polling endpoints replay correctly. Cassette root defaults to
`/var/lib/instavm/tapes` and is overridable via `INSTAVM_TAPES_CASSETTE_ROOT`.

📂 **[Example](examples/tape_basic.py)**

---

## Platform APIs

API keys, audit logs, webhooks, recordings, credits, and usage:

```python
client = InstaVM(api_key="your_api_key")

# API Keys
api_key = client.api_keys.create(description="ci key")

# Audit log
audit_page = client.audit.events(limit=25, status="success")

# Webhooks
endpoint = client.webhooks.create_endpoint(
    url="https://example.com/instavm/webhook",
    event_patterns=["vm.*", "snapshot.*"],
)

deliveries = client.webhooks.list_deliveries(limit=10)

# Computer-use session recordings
recordings = client.recordings.list(status="ready", limit=10)
url = client.recordings.get_download_url(recordings[0]["id"])
client.recordings.download(recordings[0]["id"], "/tmp/session.mp4")

# Credits / usage
allocation = client.credits.allocation()
summary = client.credits.summary(period="current_month")
trends = client.credits.usage_trends(period="30d", granularity="daily")

# Day-wise spend breakdown (cost / CPU hours / RAM GiB-hours)
breakdown = client.get_usage_breakdown(days=30)
```

The same surface is available from the CLI. Money/usage commands live under `billing`; recordings live under `desktop` (since they're captures of computer-use desktop sessions); VM-scoped operations live under `vm`/`vms`:

```bash
# Recordings, under `desktop` because they're desktop session captures
instavm desktop recordings ls --status ready
instavm desktop recordings download rec_123 -o ~/Downloads/session.mp4

# Money / usage, all under `billing`
instavm billing                                      # default: print Stripe portal URL
instavm billing status                               # current-period credit summary
instavm billing usage --days 14                      # day-wise $ / CPU-h / RAM GiB-h
instavm billing usage-history --period last_30_days  # per-event credit history
instavm billing check --usage-type browser_automation --required-credits 5
instavm billing trends --period 30d --granularity daily

# VM-scoped, mirrors client.vms.*
instavm vm update vm_42 --memory-mb 1024 --snapshot-on-terminate --snapshot-name auto-vm_42
```

---

## Error Handling

All SDK errors extend a typed hierarchy for precise `except` handling:

```python
from instavm import (
    InstaVM,
    AuthenticationError,
    ExecutionError,
    NetworkError,
    RateLimitError,
    SessionError,
)

client = InstaVM(api_key="your_api_key")

try:
    client.execute("raise Exception('boom')")
except AuthenticationError:
    print("Invalid API key")
except RateLimitError:
    print("Rate limited")
except SessionError as exc:
    print(f"Session issue: {exc}")
except ExecutionError as exc:
    print(f"Execution failed: {exc}")
except NetworkError as exc:
    print(f"Network issue: {exc}")
```

---

## Development & Testing

```bash
pip install -e .                              # Install for development
python3 -m pytest tests/test_api_client.py -v # Unit tests
```

---

## Further Reading

- [Python SDK Overview](https://instavm.io/docs/sdks/python/overview)
- [VM Management](https://instavm.io/docs/sdks/python/vm-management)
- [Snapshots](https://instavm.io/docs/sdks/python/snapshots)
- [Egress and Networking](https://instavm.io/docs/sdks/python/egress-and-networking)
- [Platform APIs](https://instavm.io/docs/sdks/python/platform-apis)
- [Browser Automation](https://instavm.io/docs/sdks/python/browser-automation)
- [Error Handling](https://instavm.io/docs/sdks/python/error-handling)

---

## Changelog

Current package version: **0.22.0**

### 0.22.0

Rolls up all changes since 0.21.0 (the previous PyPI release). Adds the Secrets Vault command tree and vault-aware `instavm deploy`; tape recording/replay; CLI and SDK coverage for recordings, credits, usage breakdown, and `vm update`; `instavm.yaml` v2 manifests; a grouped progress rail for interactive CLI commands; `instavm events` and `instavm webhooks` plus an offline `verify()` helper; and `url-path` vault auth with a Telegram fallback template.

#### Secrets Vault

- New `instavm vault` command tree (aliases `secrets`, `secret`): create vaults, add credentials, bind upstream services, fetch request logs. Secret values are stored server-side and substituted by the in-VM MITM at TLS-write time, so any HTTPS client works unchanged. Verified clients: Python `openai` / `httpx` / `requests`, Node `fetch`, Go `net/http`, Java `HttpClient`, Ruby, PHP, Perl, `curl`, `wget` (see [VAULT_SDK_COMPATIBILITY](https://github.com/BandarLabs/sandbox/blob/aws-staging/docs/VAULT_SDK_COMPATIBILITY.md)).
  - `instavm vault list|create|get|update|delete|discover|catalog|logs`
  - `instavm vault secret list|set|rotate|delete`. Values come from `--value-file`, stdin, an interactive `getpass` prompt, or `--value` (visible in shell history).
  - `instavm vault service list|add|remove`. Bind a credential to a host with `--template openai|anthropic|stripe|github|…` or with a custom `--host` (mutually exclusive with `--template`).
  - SDK: `client.list_vaults`, `create_vault`, `add_vault_credential`, `add_vault_service`, `add_vault_services_from_templates`, `get_vault_request_logs`.
  - CI-friendly: `INSTAVM_API_KEY=$INSTAVM_API_KEY instavm vault secret set $VID OPENAI_KEY --value-file <(echo "$OPENAI_KEY")`.

- `instavm deploy` attaches org vaults to the new VM. Cookbooks declare upstream hosts via a `vault:` block in `instavm.yaml`; the CLI scans org vaults at deploy time, selects any vault with a service binding for at least one declared host, and passes the matching `vault_ids` to `POST /v1/vms`. Previously a vault-aware cookbook could 401 even when a matching org vault existed because `instavm deploy` had no way to attach it.
  - Manifest schema (additive, optional):
    ```yaml
    vault:
      required: true          # fail-fast deploy if no matching vault is found
      hosts:                  # services this cookbook expects MITM to inject
        - api.openai.com
    ```
    Unknown fields are ignored by older CLIs.
  - Flags on `instavm deploy` and `instavm cookbook deploy`:
    - `--vault VAULT_ID` (repeatable): attach specific vaults, bypassing auto-discovery.
    - `--no-vault`: opt out of auto-discovery even when the manifest declares hosts.
    - `--no-setup-vault`: disable the interactive bootstrap; deploy fails fast with a setup recipe instead.
  - SDK: `_cookbook.create_vm()` and `_run_deploy()` accept a `vault_ids=` kwarg that lands in the VM-create payload. `client.vms.create(**payload)` already forwards unknown keys, so `client.vms.create(vault_ids=["vlt_…"])` works directly.

- Interactive vault bootstrap on first deploy. When a cookbook declares `vault.required: true`, no matching org vault exists, and stdin is a TTY, the CLI prompts `Bootstrap a vault now and walk through N secret(s)? [Y/n]`. On `y` it: (1) reuses an existing vault if at least one declared host is already covered by it, otherwise creates one named `<slug>-vault`; (2) prompts via `getpass` for each missing credential; (3) stores the credentials and binds services using the server template catalog (`GET /v1/vaults/catalog`: `openai`, `anthropic`, `openrouter`, `gemini`, `stripe`, `github`, `linear`, `notion`, `groq`, `perplexity`, `elevenlabs`, `cloudflare`, `datadog`, `sentry`, `slack`, `vercel`, `supabase`, `jira`, `hubspot`, `twilio`, `pagerduty`, `postmark`, `resend`, `sendgrid`, `telegram`) when available, otherwise synthesizes a bearer-auth binding; (4) re-runs discovery and proceeds with the deploy.
  - Catalog binds are batched into a single `POST /v1/vaults/{id}/services:from_template` request.
  - `instavm vault setup [PATH]` runs the same flow without provisioning a VM. Reports `already_covered` (exit 0) when the vault is already wired up.
  - Credential keys come from the catalog (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, …); unknown hosts get `<HOST_LABEL>_API_KEY` derived from the host (e.g. `api.acme-internal.io` → `ACME_INTERNAL_API_KEY`). When extending an existing vault that already has the credential, the bootstrap skips the secret prompt and only adds the missing service binding (idempotent).
  - When stdin is not a TTY or `--no-setup-vault` is passed, deploy fails fast with the manual setup recipe.
  - `bootstrap_vault_for_manifest(client, manifest, missing_hosts=…, prompt_secret=…, prompt_confirm=…)` exposes the helper for library callers. `_run_deploy(...)` and `deploy_cookbook(...)` accept `allow_vault_bootstrap` (defaults to `True` in the CLI, `False` for library calls), `vault_bootstrap_prompt_secret`, and `vault_bootstrap_prompt_confirm`.

- `instavm deploy ./dir` no longer requires `git` in preflight. Preflight checks `ssh`, `scp`, and `tar`. `git` is verified lazily inside `sync_cookbook_repo()` when `instavm cookbook list|info|deploy <slug>` needs to clone or update `instavm/cookbooks`; if it is missing there, the error explains how to install it or set `INSTAVM_COOKBOOKS_DIR`.

- `url-path` vault auth type. `instavm vault service add … --auth-type url-path [--placeholder SENTINEL]` registers services that embed the secret in the request URL (e.g. Telegram Bot API). The matching MITM rewrite runs server-side. The cookbook bootstrap renders per-auth-type setup recipes (bearer, header, url-path) and includes a built-in `telegram` fallback template so the setup recipe still renders when the backend catalog is unreachable.

#### Tape recording and replay

- `instavm tape` CLI and `client.tapes` SDK for recording, replaying, branching, and exporting agent sessions. Lanes: `tool_call`, `fs_summary`, `http_egress`, `llm_call`, `vnc`, `branch`. Includes multi-track ASCII timeline rendering, a `ToolCallRecorder` middleware for agent loops, and an offline `CassetteReplayClient` that plugs into `httpx.MockTransport` to replay recorded LLM calls without a network round-trip.

#### Recordings, credits, usage breakdown, VM update

- SDK adds `client.recordings`, `client.credits`, and `client.get_usage_breakdown(...)`. The existing `client.vms.update()` gets a CLI counterpart. Endpoints wrapped: `GET/DELETE /v1/recordings*`, `GET /v1/credits/{allocation,usage,summary,check,rates,usage/trends,usage/forecasting}`, `GET /v1/users/me/usage-breakdown`, `PATCH /v1/vms/{vm_id}`.
- CLI surfaces:
  - `instavm desktop recordings ls|get|download|rm`. `download` resolves the short-lived S3 presigned URL via a no-redirect probe of `GET /v1/recordings/{id}/download`; with `-o FILE` it streams the bytes locally without sending the InstaVM API key to S3.
  - `instavm billing` is now a namespace. Running `instavm billing` with no subcommand still prints the Stripe portal URL (backwards compatible). Subcommands: `portal`, `status`, `allocation`, `usage`, `usage-history`, `check`, `rates`, `trends`, `forecast`.
  - `instavm vm update <vm_id>` (alias `instavm vms update …`) mirrors `client.vms.*`. Supports `--memory-mb` (live memory rebudget), `--snapshot-on-terminate` / `--no-snapshot-on-terminate` (mutex group), `--snapshot-name`. The top-level `create`/`rm`/`clone`/`connect`/`shell` shorthands are unchanged.

#### Audit events and webhooks

- `instavm events`: CLI for `AuditManager` (`/v1/audit/events`).
  - `tail`: polls newest-first / cursor-paginated audit events on an interval, tracks a `(seen_ids, last_ts)` watermark, and prints chronologically. Filters: `--vm`, `--session`, `--event`, `--group`, `--status`, `--since` (`10s` / `5m` / `2h` / `1d` or ISO-8601), `--limit` (1-200), `--interval` (default 2s), `--once` for a single batch. Defaults to follow mode.
  - `get <event_id>`: fetch one audit event.
  - `catalog`: list event groups and types known to the server.
- `instavm webhooks`: CLI for `WebhooksManager`. Subcommands: `create`, `list`, `get`, `update`, `delete`, `verify` (server-side challenge), `rotate-secret`, `test` (enqueue `webhook.test`), `deliveries` (filter by endpoint, status, event type, cursor, limit), `replay <delivery_id>`, and `verify-signature` for local verification of a captured body against a signing secret (`--header 'Name: value'` repeat, `--body | --body-file | stdin`, `--secret`, `--tolerance` with `-1` to disable the replay-window check).
- `instavm.webhooks` module: offline signature verification helper.
  - `verify(headers, body, secret, *, tolerance_seconds=300, now=None)` validates the contract `InstaVM-Signature: t=<unix>,v1=<hex hmac_sha256(secret, b'<ts>.' + raw_body)>` and returns the parsed event dict on success.
  - `WebhookVerificationError(reason, message)` with stable `reason` values: `missing_header`, `malformed_header`, `missing_signature`, `invalid_signature`, `stale_timestamp`, `invalid_payload`.
  - `compute_signature(secret, ts, body)` and `parse_signature_header()` are exposed for callers that drive verification themselves. Accepts any `Mapping[str, str]` of headers and the raw request bytes; header lookup is case-insensitive. Pass the bytes the server signed (do not re-serialize JSON).
  - Re-exported as `instavm.webhooks`, `instavm.WebhookVerificationError`, and `instavm.verify_webhook`.

#### `instavm.yaml` v2 manifests

- YAML v2 guide published at [`docs/instavm_yaml_v2.md`](docs/instavm_yaml_v2.md) and linked from the README. v2 tightens spec compliance for cookbook manifests; v1 continues to work, with v1 regression coverage added.

#### Progress rail for interactive CLI commands

- Grouped `┌` / `│` / `└` progress with a single accent color, rendered via `rich`, for TTY sessions. `--json`, `NO_COLOR`, `TERM=dumb`, and non-TTY output keep their existing machine-readable / plain output unchanged. Applies to VM create/delete/clone, snapshot create/delete, desktop start/stop, and VM update waits. Watch-mode refreshes use `rich.console.Console.clear()` on the rich path. Progress headers show command names only, not positional operands such as local paths or VM IDs.

### 0.21.0

- **`instavm login` (browser PKCE).** Loopback OAuth/PKCE flow opens the dashboard, completes auth in the browser, and writes credentials to `~/.instavm/config.json`. `instavm auth logout` clears them. `AuthenticationError` now hints to re-run `instavm login`.

### 0.19.1

- **Fix: `browser navigate` now shows page title.** The CLI was reading `title` from the wrong level of the API response, always showing `-`. Now correctly extracts title from the nested `data` field.
- **Fix: `egress set` now shows the new policy state.** Previously displayed the old state (all `allowed`) because the backend only returns `{"status": "ok"}`. The CLI now fetches the updated state after applying.

### 0.19.0

- **Full browser interaction CLI.** `instavm browser` now exposes `navigate`, `click`, `type`, `fill`, `scroll`, `wait`, and `extract` subcommands, all backed by Playwright via CSS selectors. Previously these were SDK-only; now they're available from the terminal.
- **Browser session management CLI.** `instavm browser session create|close|ls` for managing persistent browser sessions across multiple interactions.
- **PTY CLI.** `instavm pty <vm_id>` opens an interactive terminal inside a VM, like `docker exec -it`. Supports custom shell programs and automatic window resize.
- **ForbiddenError handling.** The SDK now catches 403 responses as `ForbiddenError` instead of retrying indefinitely, with proper error messages for tier limit violations.

### 0.18.0

- **OpenAI Agents SDK sandbox provider.** `pip install instavm[agents]` adds InstaVM as a backend for Sandbox Agents with auto-registration, no SDK patches needed.
- **PTY (interactive terminal) support.** `pip install instavm[pty]` enables `PtyManager` for creating, resizing, and killing interactive terminal sessions over WebSocket. The OpenAI Agents SDK's `Shell` capability (`write_stdin`) works automatically when PTY is available.
- `InstaVMSandboxClient` / `InstaVMSandboxSession` implementing the full sandbox session contract (exec, read, write, persist, hydrate, resume, exposed ports).
- `InstaVMSandboxClientOptions` with VM sizing, snapshots, egress control, exposed ports, environment variables, and metadata.
- `InstaVMCloudBucketMountStrategy` for S3/R2/GCS/Azure Blob mounts via rclone+FUSE.
- Persistent JuiceFS volume helpers (mount, unmount, list).
- Workspace snapshots with mount-safe tar archiving (unmount, tar, remount).
- Exit-code auto-detection: uses native API `exit_code` when available, falls back to sentinel wrapping on older backends.
- Egress policy management (allow/block HTTP/HTTPS, package managers, domain/CIDR allowlists).
- 125 unit tests, 21 e2e integration tests, full `Runner.run` agent smoke test verified.
- CI matrix: Python 3.10-3.13, Ubuntu/macOS/Windows, plus early-warning job tracking `openai-agents @ git+main`.

### 0.17.0

- Added CLI parity for `egress`, `exec`, and terminal-first `browser` workflows
- Added colored VM status badges, `--watch` refreshes for `ls` and `snapshot get`, and snapshot-build spinner coverage

### 0.16.1

- Fixed deploy and cookbook uploads through the SSH gateway by forcing legacy SCP mode

### 0.16.0

- Expanded CLI docs for `instavm cookbook`
- Added experimental `instavm deploy` for zero-config app deploys from the current directory

### 0.15.1

- `ls` now matches the SSH gateway: active VMs by default, `-a` or `--all` for all VM records
- `whoami` now uses the live `/v1/users/me` endpoint

### 0.15.0

- Installed `instavm` CLI for `pip install instavm`, including `python -m instavm.cli`
- Stored CLI auth/config in `~/.instavm/config.json` with `INSTAVM_API_KEY` fallback
- Added [`get_current_user()`](#library-quick-start) and [`get_session_status(session_id=None)`](#computer-use) helpers for account and desktop workflows

### 0.13.0

- [`get_session_app_url(session_id?, port?)`](#sessions--sandboxes): session app URL with optional port
- [`list_sandboxes(metadata?, limit?)`](#sessions--sandboxes): list sandbox records with metadata filtering
- [`computer_use.head(session_id, path)`](#computer-use): HEAD proxy method for computer-use sessions
- [`computer_use.vnc_websockify(session_id)`](#computer-use): VNC websockify URL for remote desktop streaming
- VM creation now accepts [`volumes`](#vms--snapshots) for pre-attached volume mounts

### 0.12.0

- Manager-based APIs across VMs, volumes, snapshots, shares, custom domains, computer use, API keys, audit, and webhooks
- Snapshot build args support for env vars and Git clone inputs
- Distinct VM list helpers for `/v1/vms` and `/v1/vms/`

For detailed history, see repository tags and PR history.
