# FastVM API Public REST API for managing FastVM VMs, snapshots, firewalls, and command execution. > This document concatenates the FastVM Python SDK, TypeScript SDK, and REST API reference for direct LLM ingestion. It is regenerated from `api/openapi.yaml` + `api/helpers.yaml` on every build (see `frontend/scripts/gen-sdk-docs.ts`); do not edit by hand. ## Python SDK ```shell pip install fastvm ``` ### Top-level helpers #### `health` *GET /healthz* ```python client.health() -> HealthResponse ``` Health check **Returns:** `HealthResponse` #### `upload` ```python client.upload( vm_id: str, local_path: str, remote_path: str, *, fetch_timeout_sec: int = 600, exec_timeout_sec: int = 600, ) -> None ``` Copy a local file or directory into the VM. Uses `vms.files.presign` and `vms.files.fetch` under the hood. Directories are tarred on the fly before upload and extracted VM-side after fetch. Streams end-to-end with no intermediate copy to `/tmp` on the client, so multi-GB transfers are bounded by VM disk, not RAM. Directory mode needs the `tar` binary on the client's `PATH` (standard on macOS and Linux; available on modern Windows via bsdtar). **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `vm_id` | `str` | required | Target VM id. | | `local_path` | `str` | required | Local file or directory path. | | `remote_path` | `str` | required | Destination path inside the VM. | | `fetch_timeout_sec` | `int` | 600 | Timeout on the VM-side /files/fetch call. | | `exec_timeout_sec` | `int` | 600 | Timeout on VM-side tar extraction (dir mode only). | **Returns:** `None` **Example** ```python client.upload(vm.id, "./config.toml", "/etc/app.toml") # file client.upload(vm.id, "./src", "/root/src") # directory (tar-streamed) ``` #### `download` ```python client.download( vm_id: str, remote_path: str, local_path: str, *, exec_timeout_sec: int = 600, ) -> None ``` Copy a file or directory from the VM to the client. Uses `vms.files.presign` plus a VM-side exec to classify the path and stream its contents out. Directories are tarred VM-side and un-tarred on the client, rooted at `./` so upload and download are symmetric. Streams end-to-end with no intermediate copy. Missing paths raise `FileNotFoundError` (Python) or `FileTransferError` with `code: 'ENOENT'` (TypeScript). **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `vm_id` | `str` | required | Target VM id. | | `remote_path` | `str` | required | Source path inside the VM. | | `local_path` | `str` | required | Destination path on the client. | | `exec_timeout_sec` | `int` | 600 | Timeout on VM-side exec (classify + stream). | **Returns:** `None` **Example** ```python client.download(vm.id, "/root/out.log", "./out.log") # file client.download(vm.id, "/var/log", "./log-backup") # directory ``` #### `wait_for_vm_ready` ```python client.wait_for_vm_ready( vm_id: str, *, poll_interval: float = 2.0, timeout: float = 300.0, ) -> VM ``` Poll `GET /v1/vms/{id}` until the VM reaches `status == "running"` or a terminal failure status. Same polling logic as `vms.launch`; use this when you already have a VM id from `vms.list()` or another flow. **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `vm_id` | `str` | required | Target VM id. | | `poll_interval` | `float` | 2.0 | Seconds between polls. | | `timeout` | `float` | 300.0 | Total wait deadline in seconds. | **Returns:** `VM` **Example** ```python vm = client.vms.retrieve(some_id) vm = client.wait_for_vm_ready(vm.id, timeout=120) ``` ### VMs #### `list` *GET /v1/vms* ```python client.vms.list( status: VMStatus, ) -> VM[] ``` List VMs **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `status` | `VMStatus` | — | Restrict to VMs with this status. Accepts any value of `VMStatus`; unknown values return an empty list. | **Returns:** `VM[]` #### `launch` *POST /v1/vms* ```python client.vms.launch( *, machine_type: MachineType | None = None, snapshot_id: str | None = None, name: str | None = None, metadata: dict[str, str] | None = None, firewall: FirewallPolicy | None = None, wait: bool = True, poll_interval: float = 2.0, wait_timeout: float = 300.0, timeout: float | httpx.Timeout | None = None, max_retries: int = 0, ) -> VM ``` Launch a VM and (by default) block until it reaches `status == "running"`. `POST /v1/vms` returns 201 for immediately-running VMs and 202 for queued VMs; the override handles both paths transparently by polling `GET /v1/vms/{id}`. Pass `wait=false` (TS) / `wait=False` (Python) to skip polling and return the raw 201/202 body. Pass `snapshot_id` / `snapshotId` to restore from a snapshot instead of cold-booting. Terminal failure statuses (`error`, `stopped`, `deleting`) raise `VMLaunchError`. Polling-deadline exceeded raises `VMNotReadyError`. **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `machine_type` | `MachineType | None` | None | VM flavor (c1m2, c2m4, ...). Required unless snapshot_id is set. | | `snapshot_id` | `str | None` | None | Restore from snapshot instead of cold-booting. | | `name` | `str | None` | None | Human-readable VM name. | | `metadata` | `dict[str, str] | None` | None | Free-form key/value labels. | | `firewall` | `FirewallPolicy | None` | None | Initial firewall policy. | | `wait` | `bool` | True | Block until RUNNING. Set False for raw 201/202 behavior. | | `poll_interval` | `float` | 2.0 | Seconds between polls when wait=True. | | `wait_timeout` | `float` | 300.0 | Max seconds to wait for RUNNING. Raises VMNotReadyError on exceed. | | `timeout` | `float | httpx.Timeout | None` | None | Per-request HTTP timeout (forwarded to generated launch verbatim). | | `max_retries` | `int` | 0 | Auto-retry on 5xx/connect errors. POST is non-idempotent, default 0. | **Returns:** `VM` **Example** ```python from fastvm import FastvmClient client = FastvmClient() vm = client.vms.launch(machine_type="c1m2", name="dev") print(vm.id, vm.status) # "running" # Restore from snapshot vm = client.vms.launch(snapshot_id="snp_...") # Skip polling — get the raw 201/202 body vm = client.vms.launch(machine_type="c1m2", wait=False) ``` #### `retrieve` *GET /v1/vms/{id}* ```python client.vms.retrieve( id: str, ) -> VM ``` Get a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | **Returns:** `VM` #### `update` *PATCH /v1/vms/{id}* ```python client.vms.update( id: str, name: str, metadata: Metadata, ttl: unknown, ) -> VM ``` Update a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `name` | `str` | — | | | `metadata` | `Metadata` | — | | | `ttl` | `unknown` | — | | **Returns:** `VM` #### `delete` *DELETE /v1/vms/{id}* ```python client.vms.delete( id: str, ) -> DeleteResponse ``` Delete a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | **Returns:** `DeleteResponse` #### `pause` *POST /v1/vms/{id}/pause* ```python client.vms.pause( id: str, ) -> VM ``` Pause a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | **Returns:** `VM` #### `resume` *POST /v1/vms/{id}/resume* ```python client.vms.resume( id: str, ) -> VM ``` Resume a paused VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | **Returns:** `VM` #### `refresh_ttl` *POST /v1/vms/{id}/ttl/refresh* ```python client.vms.refresh_ttl( id: str, ) -> VM ``` Reset the VM's TTL cycle **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | **Returns:** `VM` #### `set_firewall` *PUT /v1/vms/{id}/firewall* ```python client.vms.set_firewall( id: str, ingress: IngressPolicy, egress: EgressPolicy, dns: DNSPolicy, ) -> VM ``` Replace firewall policy **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `ingress` | `IngressPolicy` | — | | | `egress` | `EgressPolicy` | — | | | `dns` | `DNSPolicy` | — | | **Returns:** `VM` #### `patch_firewall` *PATCH /v1/vms/{id}/firewall* ```python client.vms.patch_firewall( id: str, ingress: IngressPolicy, egress: EgressPolicy, dns: DNSPolicy, ) -> VM ``` Patch firewall policy **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `ingress` | `IngressPolicy` | — | | | `egress` | `EgressPolicy` | — | | | `dns` | `DNSPolicy` | — | | **Returns:** `VM` #### `console_token` *POST /v1/vms/{id}/console-token* ```python client.vms.console_token( id: str, ) -> ConsoleTokenResponse ``` Mint a console token **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | **Returns:** `ConsoleTokenResponse` #### `run` *POST /v1/vms/{id}/exec* ```python client.vms.run( id: str, *, command: str | Sequence[str], timeout_sec: int | None = None, max_retries: int = 0, ) -> ExecVMResponse ``` Execute a command inside a VM. The override accepts `str` in addition to `Sequence[str]`: plain shell strings are auto-wrapped into `["sh", "-c", ""]` before hitting the API. Argv-style calls pass through unchanged. The wrap guards against Python's silent string-to-chars iteration when a `Sequence[str]` parameter is passed a bare string, which would otherwise produce a nonsensical argv like `["l","s"," ","-","l","a"]`. **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Target VM id. | | `command` | `str | Sequence[str]` | required | Shell string (auto-wrapped) or argv. | | `timeout_sec` | `int | None` | None | Server-side execution timeout. | | `max_retries` | `int` | 0 | Auto-retry on 5xx. Non-idempotent, default 0. | **Returns:** `ExecVMResponse` **Example** ```python # Shell strings work — auto-wrapped into ["sh", "-c", ...] result = client.vms.run(vm.id, command="ls -la /root") # Argv lists pass through unchanged result = client.vms.run(vm.id, command=["python3", "main.py", "--flag"]) print(result.exit_code, result.stdout) ``` #### `stream` ```python client.vms.stream( id: str, *, command: str | Sequence[str], timeout_sec: int | None = None, ) -> Iterator[ExecEvent] ``` Stream exec output as typed events via `Accept: application/x-ndjson`. Same endpoint as `vms.run` (`POST /v1/vms/{id}/exec`), but the server emits a newline-delimited stream of `ExecEvent` objects instead of a single buffered JSON response. Events are: - `"o"` — stdout chunk (decoded bytes in `data`) - `"e"` — stderr chunk (decoded bytes in `data`) - `"x"` — terminal exit event (`exit_code`, `timed_out`, `duration_ms`) There is no 4 MiB per-stream cap on output. The HTTP connection stays open until the command exits or `timeout_sec` fires server-side. Use this for long-running processes (builds, test runners, live logs) where you need incremental output without buffering the entire result. Shell strings (Python only) are auto-wrapped into `["sh", "-c", ...]` exactly like `vms.run`. **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Target VM id. | | `command` | `str | Sequence[str]` | required | Shell string (auto-wrapped) or argv list. | | `timeout_sec` | `int | None` | None | Server-side execution timeout in seconds. | **Returns:** `Iterator[ExecEvent]` **Example** ```python from fastvm import FastvmClient, ExecEvent client = FastvmClient() for event in client.vms.stream(vm.id, command="make -j8"): if event.type == "o": sys.stdout.buffer.write(event.data) elif event.type == "e": sys.stderr.buffer.write(event.data) elif event.type == "x": print(f"exit {event.exit_code} in {event.duration_ms} ms") ``` ### VMs.Services #### `list` *GET /v1/vms/{id}/services* ```python client.vms.services.list( id: str, ) -> Service[] ``` List service registrations **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | **Returns:** `Service[]` #### `register` *POST /v1/vms/{id}/services* ```python client.vms.services.register( id: str, name: str, port: int, h2c: bool, ) -> Service ``` Register a service on a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `name` | `str` | required | | | `port` | `int` | required | | | `h2c` | `bool` | false | Optional. When true, the proxy uses HTTP/2 cleartext to the backend (required for gRPC). Defaults to false (HTTP/1.1). | **Returns:** `Service` #### `update` *PUT /v1/vms/{id}/services/{serviceName}* ```python client.vms.services.update( id: str, service_name: str, port: int, h2c: bool, ) -> Service ``` Register or update a service on a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `service_name` | `str` | required | Service registration name. 1–29 chars, lowercase letters and digits with optional single internal hyphens (no leading, trailing, or consecutive hyphens). Embedded in the public URL as the leftmost label. | | `port` | `int` | required | New TCP port. Same value as the existing entry is a no-op. | | `h2c` | `bool` | false | Optional. When true, the proxy uses HTTP/2 cleartext to the backend. Same value as the existing entry is a no-op; a different value updates the registered transport. | **Returns:** `Service` #### `delete` *DELETE /v1/vms/{id}/services/{serviceName}* ```python client.vms.services.delete( id: str, service_name: str, ) ``` Deregister a service from a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `service_name` | `str` | required | Service registration name. 1–29 chars, lowercase letters and digits with optional single internal hyphens (no leading, trailing, or consecutive hyphens). Embedded in the public URL as the leftmost label. | ### VMs.Files #### `presign` *POST /v1/vms/{id}/files/presign* ```python client.vms.files.presign( id: str, path: str, ) -> FilePresignResponse ``` Mint signed URLs for uploading a file to a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `path` | `str` | required | Absolute destination path inside the guest filesystem (where the file will land after `fetchFileToVm`). Used only to scope the staging object key; any value server-side is accepted here. | **Returns:** `FilePresignResponse` **Example** ```python # High-level helpers — handle presign + PUT/GET + fetch + (for dirs) tar # for both file and directory transfers automatically. client.upload(vm.id, "./local/file.txt", "/root/file.txt") client.upload(vm.id, "./local-dir", "/root/remote-dir") client.download(vm.id, "/root/out.log", "./out.log") client.download(vm.id, "/var/log", "./log-backup") # Raw call if you need manual control over the signed-URL flow: presign = client.vms.files.presign(vm.id, path="/root/file.txt") ``` #### `fetch` *POST /v1/vms/{id}/files/fetch* ```python client.vms.files.fetch( id: str, url: str, path: str, timeout_sec: int, ) -> ExecVMResponse ``` Fetch a file into a VM from a presigned URL **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `url` | `str` | required | Must be the `downloadUrl` previously returned by `POST /v1/vms/{id}/files/presign` (URLs from other sources are rejected). | | `path` | `str` | required | Absolute destination path inside the guest filesystem. | | `timeout_sec` | `int` | — | Per-fetch timeout in seconds. | **Returns:** `ExecVMResponse` **Example** ```python # You usually don't call this directly — client.upload() composes # presign + PUT + fetch in a single call. Use it when you need to # pipe an already-hosted URL (still from /files/presign) into the VM. client.vms.files.fetch(vm.id, url=presign.download_url, path="/root/file.txt") ``` ### VMs.Volumes #### `attach` *POST /v1/vms/{id}/volumes* ```python client.vms.volumes.attach( id: str, volume_id: str, mount_path: str, read_only: bool, ) -> VolumeAttachmentItem ``` Attach a volume to a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `volume_id` | `str` | required | | | `mount_path` | `str` | required | Absolute path; must start with /mnt/ or /data/. | | `read_only` | `bool` | false | | **Returns:** `VolumeAttachmentItem` #### `detach` *DELETE /v1/vms/{id}/volumes/{volumeId}* ```python client.vms.volumes.detach( id: str, volume_id: str, ) -> DetachVolumeResponse ``` Detach a volume from a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `volume_id` | `str` | required | | **Returns:** `DetachVolumeResponse` ### VMs.Bucket_mounts #### `list` *GET /v1/vms/{id}/bucket-mounts* ```python client.vms.bucket_mounts.list( id: str, ) -> BucketMount[] ``` List bucket-mounts on a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | **Returns:** `BucketMount[]` #### `attach` *POST /v1/vms/{id}/bucket-mounts* ```python client.vms.bucket_mounts.attach( id: str, bucket_uri: str, mount_path: str, read_only: bool, credentials: BucketMountCredentials, ) -> BucketMount ``` Attach a customer GCS / S3 bucket to a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `bucket_uri` | `str` | required | Customer's GCS or S3 bucket URI. `gs://[/prefix]` or `s3://[/prefix]`. | | `mount_path` | `str` | required | | | `read_only` | `bool` | false | | | `credentials` | `BucketMountCredentials` | required | | **Returns:** `BucketMount` #### `retrieve` *GET /v1/vms/{id}/bucket-mounts/{bucketMountId}* ```python client.vms.bucket_mounts.retrieve( id: str, bucket_mount_id: str, ) -> BucketMount ``` Get a bucket-mount **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `bucket_mount_id` | `str` | required | BucketMount identifier (e.g. `bm_<22-char-lowercase-hex>`), unique per VM. | **Returns:** `BucketMount` #### `rotate` *PATCH /v1/vms/{id}/bucket-mounts/{bucketMountId}* ```python client.vms.bucket_mounts.rotate( id: str, bucket_mount_id: str, credentials: BucketMountCredentials, ) -> BucketMount ``` Rotate bucket-mount credentials in-place **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `bucket_mount_id` | `str` | required | BucketMount identifier (e.g. `bm_<22-char-lowercase-hex>`), unique per VM. | | `credentials` | `BucketMountCredentials` | required | | **Returns:** `BucketMount` #### `delete` *DELETE /v1/vms/{id}/bucket-mounts/{bucketMountId}* ```python client.vms.bucket_mounts.delete( id: str, bucket_mount_id: str, ) ``` Detach and delete a bucket-mount **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | VM ID (UUID). | | `bucket_mount_id` | `str` | required | BucketMount identifier (e.g. `bm_<22-char-lowercase-hex>`), unique per VM. | ### Me.Ssh_keys #### `list` *GET /v1/me/ssh-keys* ```python client.me.ssh_keys.list() -> SshKeyListResponse ``` List the calling user's authorized SSH keys **Returns:** `SshKeyListResponse` #### `add` *POST /v1/me/ssh-keys* ```python client.me.ssh_keys.add( name: str, public_key: str, ) -> SshKey ``` Register an SSH public key for the calling user **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | `str` | — | Optional human label. | | `public_key` | `str` | required | OpenSSH-format public key (`ssh-ed25519 AAA...`). Comments are stripped. Newlines are rejected. | **Returns:** `SshKey` **Example** ```python with open(os.path.expanduser("~/.ssh/id_ed25519.pub")) as f: client.me.ssh_keys.add(public_key=f.read(), name="laptop") # then: ssh @ssh. ``` #### `delete` *DELETE /v1/me/ssh-keys/{fingerprint}* ```python client.me.ssh_keys.delete( fingerprint: str, ) -> DeleteResponse ``` Remove an authorized SSH key **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `fingerprint` | `str` | required | OpenSSH SHA256 fingerprint of the key to delete (e.g. `SHA256:abc...`). The base64 hash includes `+` and `/` and the prefix has `:`, so callers MUST URL-encode the value into the path segment. SDKs do this automatically. | **Returns:** `DeleteResponse` ### Snapshots #### `list` *GET /v1/snapshots* ```python client.snapshots.list() -> Snapshot[] ``` List snapshots **Returns:** `Snapshot[]` #### `create` *POST /v1/snapshots* ```python client.snapshots.create( vm_id: str, name: str, ) -> Snapshot ``` Create a snapshot from a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `vm_id` | `str` | required | | | `name` | `str` | — | Snapshot name (trimmed + whitespace-collapsed, max 64 runes; longer values are truncated server-side). Auto-generated as `snapshot-<8-char-vmId-prefix>` if empty. | **Returns:** `Snapshot` #### `retrieve` *GET /v1/snapshots/{id}* ```python client.snapshots.retrieve( id: str, ) -> Snapshot ``` Get a snapshot **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Snapshot ID (UUID). | **Returns:** `Snapshot` #### `update` *PATCH /v1/snapshots/{id}* ```python client.snapshots.update( id: str, name: str, ) -> Snapshot ``` Rename a snapshot **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Snapshot ID (UUID). | | `name` | `str` | — | | **Returns:** `Snapshot` #### `delete` *DELETE /v1/snapshots/{id}* ```python client.snapshots.delete( id: str, ) -> DeleteResponse ``` Delete a snapshot **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Snapshot ID (UUID). | **Returns:** `DeleteResponse` ### Snapshot_imports #### `list` *GET /v1/snapshot-imports* ```python client.snapshot_imports.list() -> SnapshotImportResponse[] ``` List the calling org's snapshot imports **Returns:** `SnapshotImportResponse[]` #### `create` *POST /v1/snapshot-imports* ```python client.snapshot_imports.create( machine_type: MachineType, disk_gi_b: int, name: str, source: SnapshotImportSourceSpec, ) -> SnapshotImportResponse ``` Build a snapshot from a Docker / OCI image or a Dockerfile **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `machine_type` | `MachineType` | — | | | `disk_gi_b` | `int` | — | Disk size for the produced snapshot. Defaults to the machine type's catalog default (typically 10 GiB). | | `name` | `str` | — | Optional human-readable label for the resulting import and snapshot. If omitted, the import id is used. | | `source` | `SnapshotImportSourceSpec` | required | | **Returns:** `SnapshotImportResponse` **Example** ```python # High-level: builds, polls, returns the completed Snapshot. snapshot = await client.build(image_ref="python:3.13-slim") # Dockerfile + context (zipped in-SDK, presigned + PUT, # then snapshot import created): snapshot = await client.build( dockerfile=Path("./Dockerfile").read_text(), context_dir="./my-app", ) ``` #### `presign_context` *POST /v1/snapshot-imports/context-presign* ```python client.snapshot_imports.presign_context( size_bytes: int, ) -> ContextPresignResponse ``` Mint a signed URL for uploading a Dockerfile build-context archive **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `size_bytes` | `int` | — | Planned upload size. The server rejects this request with `400` when it exceeds the platform-wide cap (the same cap is also enforced by the signed URL itself). | **Returns:** `ContextPresignResponse` #### `retrieve` *GET /v1/snapshot-imports/{id}* ```python client.snapshot_imports.retrieve( id: str, ) -> SnapshotImportResponse ``` Get a snapshot import's state **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Snapshot import ID (UUID). | **Returns:** `SnapshotImportResponse` #### `delete` *DELETE /v1/snapshot-imports/{id}* ```python client.snapshot_imports.delete( id: str, ) -> DeleteResponse ``` Delete a terminal snapshot import (cascades to its snapshot) **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Snapshot import ID (UUID). | **Returns:** `DeleteResponse` #### `cancel` *POST /v1/snapshot-imports/{id}/cancel* ```python client.snapshot_imports.cancel( id: str, ) -> SnapshotImportResponse ``` Cancel an in-flight snapshot import **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Snapshot import ID (UUID). | **Returns:** `SnapshotImportResponse` ### Quotas #### `retrieve` *GET /v1/org/quotas* ```python client.quotas.retrieve() -> OrgQuotaUsage ``` Get org quotas and usage **Returns:** `OrgQuotaUsage` ### Volumes #### `list` *GET /v1/volumes* ```python client.volumes.list() -> Volume[] ``` List volumes **Returns:** `Volume[]` #### `create` *POST /v1/volumes* ```python client.volumes.create( name: str, size_gi_b: int, access_mode: "rw" | "ro", ) -> Volume ``` Create a managed volume **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | `str` | required | | | `size_gi_b` | `int` | required | | | `access_mode` | `"rw" | "ro"` | required | | **Returns:** `Volume` #### `retrieve` *GET /v1/volumes/{id}* ```python client.volumes.retrieve( id: str, ) -> Volume ``` Get a volume **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Returns:** `Volume` #### `update` *PATCH /v1/volumes/{id}* ```python client.volumes.update( id: str, name: str, size_gi_b: int, access_mode: "rw" | "ro", ) -> Volume ``` Update a volume's name, sizeGiB (grow / shrink-if-not-overfull), or accessMode **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | | `name` | `str` | — | | | `size_gi_b` | `int` | — | | | `access_mode` | `"rw" | "ro"` | — | | **Returns:** `Volume` #### `delete` *DELETE /v1/volumes/{id}* ```python client.volumes.delete( id: str, ) -> DeleteResponse ``` Delete a volume **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Returns:** `DeleteResponse` #### `list_attachments` *GET /v1/volumes/{id}/attachments* ```python client.volumes.list_attachments( id: str, ) -> VolumeAttachmentItemWithVm[] ``` List VMs currently attached to this volume **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `str` | required | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Returns:** `VolumeAttachmentItemWithVm[]` ## TypeScript SDK ```shell npm install fastvm ``` ### Top-level helpers #### `health` *GET /healthz* ```typescript client.health(): APIPromise ``` Health check **Returns:** `HealthResponse` #### `upload` ```typescript client.upload( vmId: string, localPath: string, remotePath: string, opts?: { fetchTimeoutSec?: number; execTimeoutSec?: number }, ): Promise ``` Copy a local file or directory into the VM. Uses `vms.files.presign` and `vms.files.fetch` under the hood. Directories are tarred on the fly before upload and extracted VM-side after fetch. Streams end-to-end with no intermediate copy to `/tmp` on the client, so multi-GB transfers are bounded by VM disk, not RAM. Directory mode needs the `tar` binary on the client's `PATH` (standard on macOS and Linux; available on modern Windows via bsdtar). **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `vmId` | `string` | required | Target VM id. | | `localPath` | `string` | required | Local file or directory path. | | `remotePath` | `string` | required | Destination path inside the VM. | | `opts.fetchTimeoutSec` | `number` | 600 | Timeout on the VM-side /files/fetch call. | | `opts.execTimeoutSec` | `number` | 600 | Timeout on VM-side tar extraction. | **Returns:** `Promise` **Example** ```typescript await client.upload(vm.id, './config.toml', '/etc/app.toml'); await client.upload(vm.id, './src', '/root/src'); ``` #### `download` ```typescript client.download( vmId: string, remotePath: string, localPath: string, opts?: { fetchTimeoutSec?: number; execTimeoutSec?: number }, ): Promise ``` Copy a file or directory from the VM to the client. Uses `vms.files.presign` plus a VM-side exec to classify the path and stream its contents out. Directories are tarred VM-side and un-tarred on the client, rooted at `./` so upload and download are symmetric. Streams end-to-end with no intermediate copy. Missing paths raise `FileNotFoundError` (Python) or `FileTransferError` with `code: 'ENOENT'` (TypeScript). **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `vmId` | `string` | required | Target VM id. | | `remotePath` | `string` | required | Source path inside the VM. | | `localPath` | `string` | required | Destination path on the client. | | `opts.fetchTimeoutSec` | `number` | 600 | Timeout on VM-side /files/fetch. | | `opts.execTimeoutSec` | `number` | 600 | Timeout on VM-side exec. | **Returns:** `Promise` **Example** ```typescript await client.download(vm.id, '/root/out.log', './out.log'); await client.download(vm.id, '/var/log', './log-backup'); ``` #### `waitForVmReady` ```typescript client.waitForVmReady( vmId: string, opts?: { pollIntervalMs?: number; timeoutMs?: number }, ): Promise ``` Poll `GET /v1/vms/{id}` until the VM reaches `status == "running"` or a terminal failure status. Same polling logic as `vms.launch`; use this when you already have a VM id from `vms.list()` or another flow. **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `vmId` | `string` | required | Target VM id. | | `opts.pollIntervalMs` | `number` | 2000 | Milliseconds between polls. | | `opts.timeoutMs` | `number` | 300000 | Total wait deadline in ms. | **Returns:** `Promise` **Example** ```typescript let vm = await client.vms.retrieve(someId); vm = await client.waitForVmReady(vm.id, { timeoutMs: 120_000 }); ``` ### VMs #### `list` *GET /v1/vms* ```typescript client.vms.list( status: VMStatus, ): APIPromise ``` List VMs **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `status` | `VMStatus` | — | Restrict to VMs with this status. Accepts any value of `VMStatus`; unknown values return an empty list. | **Returns:** `VM[]` #### `launch` *POST /v1/vms* ```typescript client.vms.launch( params: VmLaunchParams, options?: RequestOptions, launchOpts?: { wait?: boolean; pollIntervalMs?: number; timeoutMs?: number }, ): APIPromise ``` Launch a VM and (by default) block until it reaches `status == "running"`. `POST /v1/vms` returns 201 for immediately-running VMs and 202 for queued VMs; the override handles both paths transparently by polling `GET /v1/vms/{id}`. Pass `wait=false` (TS) / `wait=False` (Python) to skip polling and return the raw 201/202 body. Pass `snapshot_id` / `snapshotId` to restore from a snapshot instead of cold-booting. Terminal failure statuses (`error`, `stopped`, `deleting`) raise `VMLaunchError`. Polling-deadline exceeded raises `VMNotReadyError`. **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `params` | `VmLaunchParams` | required | Generated launch params (machineType, snapshotId, name, metadata, firewall). | | `options` | `RequestOptions | undefined` | undefined | Generated per-request options (headers, signal, timeout, etc.). | | `launchOpts.wait` | `boolean` | true | Block until RUNNING. Set false for raw 201/202 behavior. | | `launchOpts.pollIntervalMs` | `number` | 2000 | Milliseconds between polls (±10% jitter applied). | | `launchOpts.timeoutMs` | `number` | 300000 | Total polling deadline in ms. Throws VMNotReadyError on exceed. | **Returns:** `APIPromise` **Example** ```typescript import { FastvmClient } from 'fastvm'; const client = new FastvmClient(); const vm = await client.vms.launch({ machineType: 'c1m2', name: 'dev' }); console.log(vm.id, vm.status); // "running" // Restore from snapshot const fromSnap = await client.vms.launch({ snapshotId: 'snp_...' }); // Skip polling — returns the raw 201/202 body const queued = await client.vms.launch( { machineType: 'c1m2' }, undefined, { wait: false }, ); ``` #### `retrieve` *GET /v1/vms/{id}* ```typescript client.vms.retrieve( id: string, ): APIPromise ``` Get a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | **Returns:** `VM` #### `update` *PATCH /v1/vms/{id}* ```typescript client.vms.update( id: string, name: string, metadata: Metadata, ttl: unknown, ): APIPromise ``` Update a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `name` | `string` | — | | | `metadata` | `Metadata` | — | | | `ttl` | `unknown` | — | | **Returns:** `VM` #### `delete` *DELETE /v1/vms/{id}* ```typescript client.vms.delete( id: string, ): APIPromise ``` Delete a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | **Returns:** `DeleteResponse` #### `pause` *POST /v1/vms/{id}/pause* ```typescript client.vms.pause( id: string, ): APIPromise ``` Pause a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | **Returns:** `VM` #### `resume` *POST /v1/vms/{id}/resume* ```typescript client.vms.resume( id: string, ): APIPromise ``` Resume a paused VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | **Returns:** `VM` #### `refreshTtl` *POST /v1/vms/{id}/ttl/refresh* ```typescript client.vms.refreshTtl( id: string, ): APIPromise ``` Reset the VM's TTL cycle **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | **Returns:** `VM` #### `setFirewall` *PUT /v1/vms/{id}/firewall* ```typescript client.vms.setFirewall( id: string, ingress: IngressPolicy, egress: EgressPolicy, dns: DNSPolicy, ): APIPromise ``` Replace firewall policy **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `ingress` | `IngressPolicy` | — | | | `egress` | `EgressPolicy` | — | | | `dns` | `DNSPolicy` | — | | **Returns:** `VM` #### `patchFirewall` *PATCH /v1/vms/{id}/firewall* ```typescript client.vms.patchFirewall( id: string, ingress: IngressPolicy, egress: EgressPolicy, dns: DNSPolicy, ): APIPromise ``` Patch firewall policy **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `ingress` | `IngressPolicy` | — | | | `egress` | `EgressPolicy` | — | | | `dns` | `DNSPolicy` | — | | **Returns:** `VM` #### `consoleToken` *POST /v1/vms/{id}/console-token* ```typescript client.vms.consoleToken( id: string, ): APIPromise ``` Mint a console token **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | **Returns:** `ConsoleTokenResponse` #### `run` *POST /v1/vms/{id}/exec* ```typescript client.vms.run( id: string, params: VmRunParams, options?: RequestOptions, ): APIPromise ``` Execute a command inside a VM. Same generated method as upstream. TypeScript doesn't silently iterate strings into characters, so no shell-string auto-wrap helper is needed. Pass an argv array directly. **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Target VM id. | | `params` | `VmRunParams` | required | Request body (command: string[], timeoutSec?: number). | | `options` | `RequestOptions | undefined` | undefined | Generated per-request options. | **Returns:** `APIPromise` **Example** ```typescript const result = await client.vms.run(vm.id, { command: ['python3', 'main.py', '--flag'], }); console.log(result.exitCode, result.stdout); ``` #### `stream` ```typescript client.vms.stream( id: string, body: VmRunParams, opts?: StreamOptions, ): AsyncIterable ``` Stream exec output as typed events via `Accept: application/x-ndjson`. Same endpoint as `vms.run` (`POST /v1/vms/{id}/exec`), but the server emits a newline-delimited stream of `ExecEvent` objects instead of a single buffered JSON response. Events are: - `"o"` — stdout chunk (decoded bytes in `data`) - `"e"` — stderr chunk (decoded bytes in `data`) - `"x"` — terminal exit event (`exit_code`, `timed_out`, `duration_ms`) There is no 4 MiB per-stream cap on output. The HTTP connection stays open until the command exits or `timeout_sec` fires server-side. Use this for long-running processes (builds, test runners, live logs) where you need incremental output without buffering the entire result. Shell strings (Python only) are auto-wrapped into `["sh", "-c", ...]` exactly like `vms.run`. **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Target VM id. | | `body` | `VmRunParams` | required | Request body (command: string[], timeoutSec?: number, stdin?: string). | | `opts.timeoutMs` | `number` | undefined (no client-side timeout) | Client-side HTTP abort deadline in milliseconds. | **Returns:** `AsyncIterable` **Example** ```typescript import { FastvmClient, type ExecEvent } from 'fastvm'; const client = new FastvmClient(); for await (const event of client.vms.stream(vm.id, { command: ['make', '-j8'] })) { if (event.type === 'o') process.stdout.write(event.data); else if (event.type === 'e') process.stderr.write(event.data); else if (event.type === 'x') console.log(`exit ${event.exitCode} in ${event.durationMs}ms`); } ``` ### VMs.Services #### `list` *GET /v1/vms/{id}/services* ```typescript client.vms.services.list( id: string, ): APIPromise ``` List service registrations **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | **Returns:** `Service[]` #### `register` *POST /v1/vms/{id}/services* ```typescript client.vms.services.register( id: string, name: string, port: number, h2c: boolean, ): APIPromise ``` Register a service on a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `name` | `string` | required | | | `port` | `number` | required | | | `h2c` | `boolean` | false | Optional. When true, the proxy uses HTTP/2 cleartext to the backend (required for gRPC). Defaults to false (HTTP/1.1). | **Returns:** `Service` #### `update` *PUT /v1/vms/{id}/services/{serviceName}* ```typescript client.vms.services.update( id: string, serviceName: string, port: number, h2c: boolean, ): APIPromise ``` Register or update a service on a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `serviceName` | `string` | required | Service registration name. 1–29 chars, lowercase letters and digits with optional single internal hyphens (no leading, trailing, or consecutive hyphens). Embedded in the public URL as the leftmost label. | | `port` | `number` | required | New TCP port. Same value as the existing entry is a no-op. | | `h2c` | `boolean` | false | Optional. When true, the proxy uses HTTP/2 cleartext to the backend. Same value as the existing entry is a no-op; a different value updates the registered transport. | **Returns:** `Service` #### `delete` *DELETE /v1/vms/{id}/services/{serviceName}* ```typescript client.vms.services.delete( id: string, serviceName: string, ) ``` Deregister a service from a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `serviceName` | `string` | required | Service registration name. 1–29 chars, lowercase letters and digits with optional single internal hyphens (no leading, trailing, or consecutive hyphens). Embedded in the public URL as the leftmost label. | ### VMs.Files #### `presign` *POST /v1/vms/{id}/files/presign* ```typescript client.vms.files.presign( id: string, path: string, ): APIPromise ``` Mint signed URLs for uploading a file to a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `path` | `string` | required | Absolute destination path inside the guest filesystem (where the file will land after `fetchFileToVm`). Used only to scope the staging object key; any value server-side is accepted here. | **Returns:** `FilePresignResponse` **Example** ```typescript // High-level helpers — handle presign + PUT/GET + fetch + (for dirs) tar // for both file and directory transfers automatically. await client.upload(vm.id, './local/file.txt', '/root/file.txt'); await client.upload(vm.id, './local-dir', '/root/remote-dir'); await client.download(vm.id, '/root/out.log', './out.log'); await client.download(vm.id, '/var/log', './log-backup'); // Raw call if you need manual control over the signed-URL flow: const presign = await client.vms.files.presign(vm.id, { path: '/root/file.txt' }); ``` #### `fetch` *POST /v1/vms/{id}/files/fetch* ```typescript client.vms.files.fetch( id: string, url: string, path: string, timeoutSec: number, ): APIPromise ``` Fetch a file into a VM from a presigned URL **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `url` | `string` | required | Must be the `downloadUrl` previously returned by `POST /v1/vms/{id}/files/presign` (URLs from other sources are rejected). | | `path` | `string` | required | Absolute destination path inside the guest filesystem. | | `timeoutSec` | `number` | — | Per-fetch timeout in seconds. | **Returns:** `ExecVMResponse` **Example** ```typescript // You usually don't call this directly — client.upload() composes // presign + PUT + fetch in a single call. Use it when piping an // already-hosted URL (still from /files/presign) into the VM. await client.vms.files.fetch(vm.id, { url: presign.downloadUrl, path: '/root/file.txt', }); ``` ### VMs.Volumes #### `attach` *POST /v1/vms/{id}/volumes* ```typescript client.vms.volumes.attach( id: string, volumeId: string, mountPath: string, readOnly: boolean, ): APIPromise ``` Attach a volume to a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `volumeId` | `string` | required | | | `mountPath` | `string` | required | Absolute path; must start with /mnt/ or /data/. | | `readOnly` | `boolean` | false | | **Returns:** `VolumeAttachmentItem` #### `detach` *DELETE /v1/vms/{id}/volumes/{volumeId}* ```typescript client.vms.volumes.detach( id: string, volumeId: string, ): APIPromise ``` Detach a volume from a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `volumeId` | `string` | required | | **Returns:** `DetachVolumeResponse` ### VMs.Bucket_mounts #### `list` *GET /v1/vms/{id}/bucket-mounts* ```typescript client.vms.bucket_mounts.list( id: string, ): APIPromise ``` List bucket-mounts on a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | **Returns:** `BucketMount[]` #### `attach` *POST /v1/vms/{id}/bucket-mounts* ```typescript client.vms.bucket_mounts.attach( id: string, bucketUri: string, mountPath: string, readOnly: boolean, credentials: BucketMountCredentials, ): APIPromise ``` Attach a customer GCS / S3 bucket to a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `bucketUri` | `string` | required | Customer's GCS or S3 bucket URI. `gs://[/prefix]` or `s3://[/prefix]`. | | `mountPath` | `string` | required | | | `readOnly` | `boolean` | false | | | `credentials` | `BucketMountCredentials` | required | | **Returns:** `BucketMount` #### `retrieve` *GET /v1/vms/{id}/bucket-mounts/{bucketMountId}* ```typescript client.vms.bucket_mounts.retrieve( id: string, bucketMountId: string, ): APIPromise ``` Get a bucket-mount **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `bucketMountId` | `string` | required | BucketMount identifier (e.g. `bm_<22-char-lowercase-hex>`), unique per VM. | **Returns:** `BucketMount` #### `rotate` *PATCH /v1/vms/{id}/bucket-mounts/{bucketMountId}* ```typescript client.vms.bucket_mounts.rotate( id: string, bucketMountId: string, credentials: BucketMountCredentials, ): APIPromise ``` Rotate bucket-mount credentials in-place **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `bucketMountId` | `string` | required | BucketMount identifier (e.g. `bm_<22-char-lowercase-hex>`), unique per VM. | | `credentials` | `BucketMountCredentials` | required | | **Returns:** `BucketMount` #### `delete` *DELETE /v1/vms/{id}/bucket-mounts/{bucketMountId}* ```typescript client.vms.bucket_mounts.delete( id: string, bucketMountId: string, ) ``` Detach and delete a bucket-mount **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | VM ID (UUID). | | `bucketMountId` | `string` | required | BucketMount identifier (e.g. `bm_<22-char-lowercase-hex>`), unique per VM. | ### Me.Ssh_keys #### `list` *GET /v1/me/ssh-keys* ```typescript client.me.ssh_keys.list(): APIPromise ``` List the calling user's authorized SSH keys **Returns:** `SshKeyListResponse` #### `add` *POST /v1/me/ssh-keys* ```typescript client.me.ssh_keys.add( name: string, publicKey: string, ): APIPromise ``` Register an SSH public key for the calling user **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | `string` | — | Optional human label. | | `publicKey` | `string` | required | OpenSSH-format public key (`ssh-ed25519 AAA...`). Comments are stripped. Newlines are rejected. | **Returns:** `SshKey` **Example** ```typescript import { readFileSync } from "node:fs"; await client.me.sshKeys.add({ publicKey: readFileSync(`${process.env.HOME}/.ssh/id_ed25519.pub`, "utf8"), name: "laptop", }); // then: ssh @ssh. ``` #### `delete` *DELETE /v1/me/ssh-keys/{fingerprint}* ```typescript client.me.ssh_keys.delete( fingerprint: string, ): APIPromise ``` Remove an authorized SSH key **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `fingerprint` | `string` | required | OpenSSH SHA256 fingerprint of the key to delete (e.g. `SHA256:abc...`). The base64 hash includes `+` and `/` and the prefix has `:`, so callers MUST URL-encode the value into the path segment. SDKs do this automatically. | **Returns:** `DeleteResponse` ### Snapshots #### `list` *GET /v1/snapshots* ```typescript client.snapshots.list(): APIPromise ``` List snapshots **Returns:** `Snapshot[]` #### `create` *POST /v1/snapshots* ```typescript client.snapshots.create( vmId: string, name: string, ): APIPromise ``` Create a snapshot from a VM **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `vmId` | `string` | required | | | `name` | `string` | — | Snapshot name (trimmed + whitespace-collapsed, max 64 runes; longer values are truncated server-side). Auto-generated as `snapshot-<8-char-vmId-prefix>` if empty. | **Returns:** `Snapshot` #### `retrieve` *GET /v1/snapshots/{id}* ```typescript client.snapshots.retrieve( id: string, ): APIPromise ``` Get a snapshot **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Snapshot ID (UUID). | **Returns:** `Snapshot` #### `update` *PATCH /v1/snapshots/{id}* ```typescript client.snapshots.update( id: string, name: string, ): APIPromise ``` Rename a snapshot **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Snapshot ID (UUID). | | `name` | `string` | — | | **Returns:** `Snapshot` #### `delete` *DELETE /v1/snapshots/{id}* ```typescript client.snapshots.delete( id: string, ): APIPromise ``` Delete a snapshot **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Snapshot ID (UUID). | **Returns:** `DeleteResponse` ### Snapshot_imports #### `list` *GET /v1/snapshot-imports* ```typescript client.snapshot_imports.list(): APIPromise ``` List the calling org's snapshot imports **Returns:** `SnapshotImportResponse[]` #### `create` *POST /v1/snapshot-imports* ```typescript client.snapshot_imports.create( machineType: MachineType, diskGiB: number, name: string, source: SnapshotImportSourceSpec, ): APIPromise ``` Build a snapshot from a Docker / OCI image or a Dockerfile **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `machineType` | `MachineType` | — | | | `diskGiB` | `number` | — | Disk size for the produced snapshot. Defaults to the machine type's catalog default (typically 10 GiB). | | `name` | `string` | — | Optional human-readable label for the resulting import and snapshot. If omitted, the import id is used. | | `source` | `SnapshotImportSourceSpec` | required | | **Returns:** `SnapshotImportResponse` **Example** ```typescript // Raw call (returns 202 immediately; poll for completion). const imp = await client.snapshotImports.create({ machineType: 'c2m4', source: { type: 'image', image: 'python:3.13-slim' }, }); ``` #### `presignContext` *POST /v1/snapshot-imports/context-presign* ```typescript client.snapshot_imports.presignContext( sizeBytes: number, ): APIPromise ``` Mint a signed URL for uploading a Dockerfile build-context archive **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `sizeBytes` | `number` | — | Planned upload size. The server rejects this request with `400` when it exceeds the platform-wide cap (the same cap is also enforced by the signed URL itself). | **Returns:** `ContextPresignResponse` #### `retrieve` *GET /v1/snapshot-imports/{id}* ```typescript client.snapshot_imports.retrieve( id: string, ): APIPromise ``` Get a snapshot import's state **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Snapshot import ID (UUID). | **Returns:** `SnapshotImportResponse` #### `delete` *DELETE /v1/snapshot-imports/{id}* ```typescript client.snapshot_imports.delete( id: string, ): APIPromise ``` Delete a terminal snapshot import (cascades to its snapshot) **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Snapshot import ID (UUID). | **Returns:** `DeleteResponse` #### `cancel` *POST /v1/snapshot-imports/{id}/cancel* ```typescript client.snapshot_imports.cancel( id: string, ): APIPromise ``` Cancel an in-flight snapshot import **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Snapshot import ID (UUID). | **Returns:** `SnapshotImportResponse` ### Quotas #### `retrieve` *GET /v1/org/quotas* ```typescript client.quotas.retrieve(): APIPromise ``` Get org quotas and usage **Returns:** `OrgQuotaUsage` ### Volumes #### `list` *GET /v1/volumes* ```typescript client.volumes.list(): APIPromise ``` List volumes **Returns:** `Volume[]` #### `create` *POST /v1/volumes* ```typescript client.volumes.create( name: string, sizeGiB: number, accessMode: "rw" | "ro", ): APIPromise ``` Create a managed volume **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | `string` | required | | | `sizeGiB` | `number` | required | | | `accessMode` | `"rw" | "ro"` | required | | **Returns:** `Volume` #### `retrieve` *GET /v1/volumes/{id}* ```typescript client.volumes.retrieve( id: string, ): APIPromise ``` Get a volume **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Returns:** `Volume` #### `update` *PATCH /v1/volumes/{id}* ```typescript client.volumes.update( id: string, name: string, sizeGiB: number, accessMode: "rw" | "ro", ): APIPromise ``` Update a volume's name, sizeGiB (grow / shrink-if-not-overfull), or accessMode **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | | `name` | `string` | — | | | `sizeGiB` | `number` | — | | | `accessMode` | `"rw" | "ro"` | — | | **Returns:** `Volume` #### `delete` *DELETE /v1/volumes/{id}* ```typescript client.volumes.delete( id: string, ): APIPromise ``` Delete a volume **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Returns:** `DeleteResponse` #### `listAttachments` *GET /v1/volumes/{id}/attachments* ```typescript client.volumes.listAttachments( id: string, ): APIPromise ``` List VMs currently attached to this volume **Parameters** | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Returns:** `VolumeAttachmentItemWithVm[]` ## REST API Endpoint: `https://api.fastvm.org` ### VMs VM lifecycle #### GET /v1/vms *List VMs* Lists all non-deleted VMs for the authenticated org. Supports metadata-equality filtering; callers pass repeated query parameters of the form `metadata.=` (e.g. `metadata.env=prod&metadata.role=api`). The optional `status` query filter narrows by lifecycle status (e.g. `?status=paused`). Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | `VMStatus` | no | Restrict to VMs with this status. Accepts any value of `VMStatus`; unknown values return an empty list. | **Responses** - `200` — `VM[]`: List of VMs - `401` — `Error`: Missing or invalid credentials - `500` — `Error`: Internal server error #### POST /v1/vms *Launch a VM* Creates a new VM, either from a machineType (fresh boot) or a snapshotId (restore from snapshot). - Returns **201** when the VM is already running in the response. - Returns **202** when the VM is queued; clients must poll `GET /v1/vms/{id}` until status transitions to `running`. Terminal failure statuses are `error` and `stopped`. The SDK's `launch()` helper handles the 201/202 branching and polling automatically. Auth: required (X-API-Key). **Request body:** `CreateVMRequest` **Responses** - `201` — `VMCreateResponse`: VM is already running. The response is a VM object, with two optional warning fields surfacing non-fatal failures: - `snapshotRestoreWarnings`: pre-registered services from the snapshot failed to land on the new VM. The VM itself is good; the user can re-register the listed services manually. - `attachmentWarnings`: one or more inline `volumes` / `bucketMounts` entries failed to attach during the synchronous create handshake. The VM is **not rolled back** — it boots without the failed mounts. The `failedVolumeAttachments` / `failedBucketMountAttachments` arrays carry the per-attachment `statusMessage`; callers that require all-or-nothing semantics must inspect the warning field and decide whether to delete the VM and retry, or call `POST /v1/vms/{id}/{volumes,bucket-mounts}` to retry the failed entries individually. - `202` — `VM`: VM is queued; poll for readiness - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `403` — `Error`: Org quota exceeded - `404` — `Error`: Snapshot or base image not found - `500` — `Error`: Internal server error - `502` — `Error`: Upstream service error - `503` — `Error`: Service temporarily unavailable #### GET /v1/vms/{id} *Get a VM* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Responses** - `200` — `VM`: VM - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error #### PATCH /v1/vms/{id} *Update a VM* Renames a VM and/or replaces its metadata map. At least one of `name` or `metadata` must be provided. Sending `metadata: {}` clears all metadata; omitting `metadata` leaves it unchanged. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Request body:** `UpdateVMRequest` **Responses** - `200` — `VM`: Updated VM - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error #### DELETE /v1/vms/{id} *Delete a VM* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Responses** - `200` — `DeleteResponse`: VM deletion result - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error - `502` — `Error`: Upstream service error #### POST /v1/vms/{id}/pause *Pause a VM* Captures the VM state, frees the worker and all customer-facing quotas, and transitions the VM to `paused`. Idempotent on already-paused VMs (returns 200 with the current state). Synchronous; ~3 s end-to-end. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Responses** - `200` — `VM`: Paused VM (or already paused — idempotent). - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM in a state that cannot be paused. - `500` — `Error`: Internal server error - `502` — `Error`: Upstream service error #### POST /v1/vms/{id}/resume *Resume a paused VM* Restores the VM's prior state, re-acquires quota, and transitions to `running`. Sync-when-fast / async-when-queued: returns 200 if the VM is running inline, or 202 if queued for cluster capacity. Idempotent on already-running. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Responses** - `200` — `VM`: VM running (resumed inline). - `202` — `VM`: Resume queued for capacity. VM is in `resuming`; poll `GET /v1/vms/{id}` or wait for the `vm.resumed` webhook. - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM not in a state that can be resumed. - `429` — `QuotaExceeded`: Org quota exceeded; body indicates the dimension. - `500` — `Error`: Internal server error - `502` — `Error`: Upstream service error #### POST /v1/vms/{id}/ttl/refresh *Reset the VM's TTL cycle* Resets the TTL countdown to a fresh `seconds` budget. From `running`, the deadline moves to `now + seconds*1000`. From `paused`, the remaining-budget is reset to `seconds*1000` and takes effect on next resume. 409 if no TTL is configured. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Responses** - `200` — `VM`: Updated VM with reset TTL cycle. - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM has no TTL configured, OR is in a transitional state. - `500` — `Error`: Internal server error ### Snapshots Snapshot lifecycle #### GET /v1/snapshots *List snapshots* Lists all snapshots for the authenticated org. Supports metadata-equality filtering; callers pass repeated query parameters of the form `metadata.=` (e.g. `metadata.env=prod&metadata.role=api`). Auth: required (X-API-Key). **Responses** - `200` — `Snapshot[]`: List of snapshots - `401` — `Error`: Missing or invalid credentials - `500` — `Error`: Internal server error #### POST /v1/snapshots *Create a snapshot from a VM* Captures a VM's state into a customer-visible snapshot. Supported on `running` and `paused` VMs; returns 201 Created with the new snapshot in both cases. On a paused VM, repeated calls within the same pause cycle are idempotent: the second call returns the same snapshot record without modification. Auth: required (X-API-Key). **Request body:** `CreateSnapshotRequest` **Responses** - `201` — `Snapshot`: Snapshot created - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `403` — `Error`: Snapshot quota exceeded - `404` — `Error`: Source VM not found - `409` — `Error`: Source VM is in a non-snapshottable state (provisioning, pausing, resuming, error, deleting), or the paused VM already has a snapshot with a different name. - `500` — `Error`: Internal server error - `502` — `Error`: Upstream service error #### GET /v1/snapshots/{id} *Get a snapshot* Returns the full Snapshot record for the given ID, scoped to the authenticated org. Used by the SDK's `build()` flow to fetch the completed snapshot after polling reports `completed`. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Snapshot ID (UUID). | **Responses** - `200` — `Snapshot`: Snapshot record - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error #### PATCH /v1/snapshots/{id} *Rename a snapshot* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Snapshot ID (UUID). | **Request body:** `UpdateSnapshotRequest` **Responses** - `200` — `Snapshot`: Updated snapshot - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error #### DELETE /v1/snapshots/{id} *Delete a snapshot* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Snapshot ID (UUID). | **Responses** - `200` — `DeleteResponse`: Snapshot deletion result - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error - `502` — `Error`: Upstream service error ### Firewall VM firewall policy #### PUT /v1/vms/{id}/firewall *Replace firewall policy* Replaces the full firewall policy on a VM. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Request body:** `FirewallPolicy` **Responses** - `200` — `VM`: Updated VM - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error - `502` — `Error`: Upstream service error #### PATCH /v1/vms/{id}/firewall *Patch firewall policy* Updates one or more blocks of the firewall policy. Each top-level block (`ingress`, `egress`, `dns`) is optional; when present, the supplied object **replaces that block wholesale**. Per-rule diffing is not supported — to change a single rule, send the full block with the desired rule list. An empty body (`{}`) is a no-op. Examples: - `{"ingress": {"default": "deny", "rules": []}}` clears all ingress rules and sets the default action. - `{"dns": {"mode": "allow", "domains": ["api.example.com"], "blockBypass": true}}` updates only the DNS block. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Request body:** `PatchFirewallRequest` **Responses** - `200` — `VM`: Updated VM - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error - `502` — `Error`: Upstream service error ### Exec In-VM command execution #### POST /v1/vms/{id}/exec *Execute a command inside a VM* Runs `command` inside the VM. Response shape is determined by the client's `Accept` header: - **`Accept: application/json`** (default, omitted, or `*/*`): buffered `ExecVMResponse` — the server collects all output and returns a single JSON object once the command exits. Per-stream output is capped at 4 MiB; overflow bytes are dropped and signalled via `stdoutTruncated` / `stderrTruncated`. - **`Accept: application/x-ndjson`**: newline-delimited stream of `ExecEvent`s — zero or more `stdout`/`stderr` chunks followed by exactly one terminal `exit` event. Use this for incremental output (long builds, test runners, live logs). No server-side cap. Both modes share the same request body. `timeoutSec` bounds server-side execution; clients should set their own HTTP timeout in addition. 502 responses are transient (the upstream VM host is unreachable or returned an error). The SDK's `run()` helper does NOT auto-retry these by default: exec is **not idempotent**, so if a 502 hides a successful exec a retry may run the command twice. Callers opt in with `max_retries=N` per call. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Request body:** `ExecVMRequest` **Responses** - `200` — `ExecVMResponse`: Command completed. `application/json` (default) returns a single `ExecVMResponse`; `application/x-ndjson` returns an event stream terminated by one `exit` event. - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM is not running - `500` — `Error`: Internal server error - `502` — `Error`: Upstream VM host unreachable or returned an error. Not retried by default (non-idempotent). ### Console Interactive serial console access #### POST /v1/vms/{id}/console-token *Mint a console token* Returns a short-lived token and WebSocket path. Open a WebSocket to `wss://?session=` to attach to the VM's serial console. The WebSocket endpoint itself is intentionally not modeled in this spec because it uses a capability-URL flow (no API key on upgrade) and a custom binary/text protocol. See `src/fastvm/lib/console.py` in the Python SDK for a reference client. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Responses** - `200` — `ConsoleTokenResponse`: Console token - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM is not running - `500` — `Error`: Internal server error ### ssh Per-user authorized SSH key management. Register one pubkey on your user, then `ssh @.ssh.` works for every VM in every org you're a member of. The per-stack SSH gateway uses CA-signed user certificates internally — VMs trust the CA, not your raw pubkey, so the customer-facing pubkey lives only at the gateway. #### GET /v1/me/ssh-keys *List the calling user's authorized SSH keys* Returns every SSH public key registered to the calling user. Keys are personal: registering a key once authorizes `ssh @ssh.` for any VM in any org you are a member of. SSH terminates at the per-stack gateway and is forwarded to the VM over the cluster network; the VM does not need to be publicly IPv4-reachable, and you do not need to know the VM's IPv6 address. Auth: required (X-API-Key). **Responses** - `200` — `SshKeyListResponse`: Authorized keys list - `401` — `Error`: Missing or invalid credentials - `500` — `Error`: Internal server error #### POST /v1/me/ssh-keys *Register an SSH public key for the calling user* Adds one authorized SSH public key to the calling user. The fingerprint is derived server-side and returned. Duplicate fingerprints return 409. Up to 32 keys per user. After this call, `ssh @ssh.` works for any VM you have access to. Each fingerprint is globally unique: registering a public key that another user already has on file returns 409. Auth: required (X-API-Key). **Request body:** `AddSshKeyRequest` **Responses** - `201` — `SshKey`: Key registered - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `409` — `Error`: Key with this fingerprint is already registered (by this user or another user) - `500` — `Error`: Internal server error #### DELETE /v1/me/ssh-keys/{fingerprint} *Remove an authorized SSH key* Deletes one of the calling user's keys by fingerprint. Existing SSH sessions are NOT terminated — the key simply won't authorize new connections after removal. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `fingerprint` | path | `string` | yes | OpenSSH SHA256 fingerprint of the key to delete (e.g. `SHA256:abc...`). The base64 hash includes `+` and `/` and the prefix has `:`, so callers MUST URL-encode the value into the path segment. SDKs do this automatically. | **Responses** - `200` — `DeleteResponse`: Key removed - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error ### Files File upload/download to/from a running VM #### POST /v1/vms/{id}/files/presign *Mint signed URLs for uploading a file to a VM* Returns a pair of short-lived signed URLs targeting a per-VM staging location. Upload to `uploadUrl` with PUT (`Content-Type: application/octet-stream`), then pass `downloadUrl` to `POST /v1/vms/{id}/files/fetch` to have the server pull it into the guest filesystem. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Request body:** `FilePresignRequest` **Responses** - `200` — `FilePresignResponse`: Signed URLs + upload size ceiling - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM is not running - `500` — `Error`: Internal server error - `501` — `Error`: File transfer not configured on this deployment #### POST /v1/vms/{id}/files/fetch *Fetch a file into a VM from a presigned URL* Pulls `url` into the guest at `path`. `url` must be a presigned storage URL previously minted by `POST /v1/vms/{id}/files/presign` (URLs from other sources are rejected). Response mirrors `/v1/vms/{id}/exec`: reports stdout/stderr/exit code of the underlying download+unpack operation. Not idempotent; not retried by default. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Request body:** `FileFetchRequest` **Responses** - `200` — `ExecVMResponse`: Fetch completed (command result) - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM is not running - `413` — `Error`: Object too large for VM disk / insufficient guest disk space - `500` — `Error`: Internal server error - `502` — `Error`: Failed to HEAD the presigned URL ### Quotas Org quotas and usage #### GET /v1/org/quotas *Get org quotas and usage* Auth: required (X-API-Key). **Responses** - `200` — `OrgQuotaUsage`: Quota limits and current usage - `401` — `Error`: Missing or invalid credentials - `500` — `Error`: Internal server error ### Snapshot Imports Build snapshots from a Docker / OCI image reference or a client-uploaded Dockerfile + build context #### GET /v1/snapshot-imports *List the calling org's snapshot imports* Returns every import for the calling org, ordered by `createdAt` descending. Includes pending, in-flight, and terminal rows. Auth: required (X-API-Key). **Responses** - `200` — `SnapshotImportResponse[]`: Array of snapshot imports - `401` — `Error`: Missing or invalid credentials - `500` — `Error`: Internal server error #### POST /v1/snapshot-imports *Build a snapshot from a Docker / OCI image or a Dockerfile* Submits an asynchronous import. `source.type` selects the pipeline: - **`image`** — pull `source.image` (any Docker / OCI ref) and export its rootfs onto the snapshot. Private registries supported via optional `source.registryUsername` / `source.registryPassword`; the registry host is derived from the image reference. - **`dockerfile`** — build the user-supplied Dockerfile + its uploaded context. The context tarball must be uploaded first to the `uploadUrl` returned by `POST /v1/snapshot-imports/context-presign`; pass the returned `contextRef` as `source.contextRef`. Private `FROM` pulls supported via `source.registryUsername` / `source.registryPassword` plus `source.registryHost` (required when credentials are set on this path). Response is `202 Accepted` with an import id; poll `GET /v1/snapshot-imports/{id}` until `status` is one of `succeeded`, `failed`, or `cancelled`. Auth: required (X-API-Key). **Request body:** `CreateSnapshotImportRequest` **Responses** - `202` — `SnapshotImportResponse`: Import accepted; poll `GET /v1/snapshot-imports/{id}` for status - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `403` — `Error`: Org quota exceeded - `500` — `Error`: Internal server error - `501` — `Error`: Snapshot import is not enabled on this cluster (no sandbox base image configured). The image-source path needs a sandbox base; the dockerfile-source path additionally needs `FILE_STAGING_BUCKET` configured for context uploads. - `503` — `Error`: Sandbox base image is configured but not currently available on this stack. Retry after the platform operator publishes a sandbox-base snapshot. #### POST /v1/snapshot-imports/context-presign *Mint a signed URL for uploading a Dockerfile build-context archive* Returns a short-lived signed PUT URL and a one-shot `contextRef`. Zip your Dockerfile + build context, PUT the archive to `uploadUrl` with `Content-Type: application/zip`, then submit a snapshot import with `source.type=dockerfile` and `source.contextRef=`. The ref is consumed by the create call and cannot be reused. Auth: required (X-API-Key). **Request body:** `ContextPresignRequest` **Responses** - `200` — `ContextPresignResponse`: Signed upload URL + opaque context reference - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `500` — `Error`: Internal server error - `501` — `Error`: Build-context uploads are not configured on this stack (no `FILE_STAGING_BUCKET`) or snapshot import itself is disabled. Image-source imports still work in this case. #### GET /v1/snapshot-imports/{id} *Get a snapshot import's state* Returns the current state of an import including its event log. `status` is one of `pending`, `claimed`, `running`, `succeeded`, `failed`, or `cancelled`. On `succeeded`, `snapshotId` references a `ready` snapshot — fetch it via `GET /v1/snapshots/{id}`. On `failed`, `error` carries a user-safe diagnostic. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Snapshot import ID (UUID). | **Responses** - `200` — `SnapshotImportResponse`: Snapshot import state - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error #### DELETE /v1/snapshot-imports/{id} *Delete a terminal snapshot import (cascades to its snapshot)* Removes the import record and, if the import produced a snapshot, the snapshot itself. Cascading the snapshot delete is safe: snapshots are content-addressed in the underlying block store, and any VMs already booted from the snapshot keep running (the delete only removes the pointer used by future `client.restore()` calls). Refuses non-terminal imports with `409 Conflict`; cancel the import first via `POST /v1/snapshot-imports/{id}/cancel`. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Snapshot import ID (UUID). | **Responses** - `200` — `DeleteResponse`: Deletion result - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: Import is still in flight; cancel it first - `500` — `Error`: Internal server error #### POST /v1/snapshot-imports/{id}/cancel *Cancel an in-flight snapshot import* Transitions the import to `cancelled` and best-effort signals the worker to stop the pipeline. Idempotent: re-cancelling a terminal import returns `200` with the current state. Any orphan snapshot produced just before the cancel is best-effort cleaned up. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Snapshot import ID (UUID). | **Responses** - `200` — `SnapshotImportResponse`: Cancellation result (idempotent) - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error ### VM Services Per-VM service registrations exposed via the public 4to6 HTTP proxy #### GET /v1/vms/{id}/services *List service registrations* Returns the services currently registered on this VM, sorted by name. Each service is exposed at `https://--.proxy.` over HTTPS. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Responses** - `200` — `Service[]`: Services - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `500` — `Error`: Internal server error #### POST /v1/vms/{id}/services *Register a service on a VM* Registers an HTTP service on the VM under `name`, listening on `port`. The service immediately becomes addressable at `https://--.proxy.` once the firewall is applied (synchronous). Idempotent: a POST with a name that already exists at the same `(port, h2c)` returns 201 with the existing entry. POST with a name that already exists at a different port OR different `h2c` returns 409 — use PUT to update an existing service. Per-VM cap: currently 16 services per VM (configurable via `MAX_SERVICES_PER_VM` on the scheduler). Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Request body:** `RegisterServiceRequest` **Responses** - `201` — `Service`: Service registered (or idempotent same-`(port, h2c)` re-register) - `400` — `any`: Invalid name, invalid port, or per-VM cap exceeded. The body is a `QuotaExceededError` for the cap case (carries the structured `vm_service_quota_exceeded` reason + numeric count) and an `Error` otherwise. - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: The service name is already registered at a different port or different `h2c` (use PUT to update), or the VM is in `error` state and cannot be modified. - `500` — `Error`: Internal server error #### PUT /v1/vms/{id}/services/{serviceName} *Register or update a service on a VM* Idempotent register-or-update: same name + new port updates the port; same name + same port is a no-op. Returns the resulting entry. Used to change the upstream port for an existing service registration without dropping and re-creating it. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | | `serviceName` | path | `string` | yes | Service registration name. 1–29 chars, lowercase letters and digits with optional single internal hyphens (no leading, trailing, or consecutive hyphens). Embedded in the public URL as the leftmost label. | **Request body:** `UpdateServiceRequest` **Responses** - `200` — `Service`: Service updated (or no-op same-port re-issue) - `400` — `any`: Invalid name or port, or per-VM cap exceeded. The body is a `QuotaExceededError` for the cap case and an `Error` otherwise. - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM is in `error` state and cannot be modified - `500` — `Error`: Internal server error #### DELETE /v1/vms/{id}/services/{serviceName} *Deregister a service from a VM* Idempotent: deleting a service that doesn't exist returns 204. Removes the firewall auto-rule synchronously; the proxy stops routing to the service within seconds (cache invalidation broadcast; 30s TTL is the safety net). Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | | `serviceName` | path | `string` | yes | Service registration name. 1–29 chars, lowercase letters and digits with optional single internal hyphens (no leading, trailing, or consecutive hyphens). Embedded in the public URL as the leftmost label. | **Responses** - `204`: Service deregistered (or already absent) - `400` — `Error`: Invalid service name - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM is in `error` state and cannot be modified - `500` — `Error`: Internal server error ### Health Service health #### GET /healthz *Health check* Returns 200 when the API is reachable. SDK clients call this on startup to warm HTTP/2 connections before the first real request. **Responses** - `200` — `object`: Service is healthy - `500` — `Error`: Internal server error ### Volumes Managed shared-volume lifecycle (POSIX-coherent multi-attach via virtio-fs). #### GET /v1/volumes *List volumes* Auth: required (X-API-Key). **Responses** - `200` — `Volume[]`: List of volumes - `401` — `Error`: Missing or invalid credentials - `500` — `Error`: Internal server error #### POST /v1/volumes *Create a managed volume* Auth: required (X-API-Key). **Request body:** `CreateVolumeRequest` **Responses** - `201` — `Volume`: Volume created - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `403` — `Error`: Org quota exceeded - `409` — `Error`: Name collision - `500` — `Error`: Internal server error - `502` — `Error`: Upstream service error - `503` — `Error`: Service temporarily unavailable #### GET /v1/volumes/{id} *Get a volume* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Responses** - `200` — `Volume`: Volume - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found #### PATCH /v1/volumes/{id} *Update a volume's name, sizeGiB (grow / shrink-if-not-overfull), or accessMode* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Request body:** `UpdateVolumeRequest` **Responses** - `200` — `Volume`: Updated volume - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `403` — `Error`: Org quota exceeded (grow above limit) - `404` — `Error`: Resource not found - `409` — `Error`: Conflict (volume_overfull_for_shrink, volume_in_use_by_attached_vms) - `502` — `Error`: Upstream service error #### DELETE /v1/volumes/{id} *Delete a volume* Returns 200 when the volume transitions to `deleting`. Substrate cleanup is asynchronous; the volume disappears from `GET` after the substrate-cleanup controller completes (typically seconds to minutes for large volumes). Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Responses** - `200` — `DeleteResponse`: Volume marked for deletion - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `object`: Volume currently mounted on running VMs - `500` — `Error`: Internal server error #### GET /v1/volumes/{id}/attachments *List VMs currently attached to this volume* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | Volume identifier (e.g. `vol_<22-char-lowercase-hex>`). | **Responses** - `200` — `VolumeAttachmentItemWithVm[]`: List of attachments - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found #### POST /v1/vms/{id}/volumes *Attach a volume to a VM* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Request body:** `VolumeAttachmentRequest` **Responses** - `201` — `VolumeAttachmentItem`: Volume attached - `400` — `Error`: Invalid request - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM in invalid state, VM in transitional state (`error: vm_transitioning`; retry-able with `Retry-After`), volume not ready, mount-path collision, duplicate-volume, or already-attached. The body's `error` token distinguishes. - `502` — `Error`: Upstream service error - `503` — `Error`: Service temporarily unavailable #### DELETE /v1/vms/{id}/volumes/{volumeId} *Detach a volume from a VM* Returns 200 with `{detached: true}` on the clean path. May include a `warnings` array on the force-teardown path (eject-ack timeout or guest-unresponsive). Returns 502 with `error: guest_umount_busy` when the guest reports EBUSY; the volume STAYS ATTACHED in this case. Resolve by killing in-VM users of the mount and retrying. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | | `volumeId` | path | `string` | yes | | **Responses** - `200` — `DetachVolumeResponse`: Detached (with optional warnings) - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: VM in transitional state (`error: vm_transitioning`; honor `Retry-After`) or terminal state (`error: vm_invalid_state`). - `502` — `Error`: Upstream service error - `503` — `Error`: Service temporarily unavailable ### Bucket Mounts BYO GCS/S3 bucket mounts as a VM sub-resource. #### GET /v1/vms/{id}/bucket-mounts *List bucket-mounts on a VM* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Responses** - `200` — `BucketMount[]`: List - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found #### POST /v1/vms/{id}/bucket-mounts *Attach a customer GCS / S3 bucket to a VM* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | **Request body:** `CreateBucketMountRequest` **Responses** - `201` — `BucketMount`: BucketMount created and mounted - `400` — `Error`: Validation failure (bad URI, malformed credentials, bucket-not-found) - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: Path collision, VM invalid state, VM in transitional state (`error: vm_transitioning`; retry-able with `Retry-After`), or per-VM cap exceeded. The body's `error` token distinguishes. - `502` — `Error`: Upstream service error - `503` — `Error`: Service temporarily unavailable #### GET /v1/vms/{id}/bucket-mounts/{bucketMountId} *Get a bucket-mount* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | | `bucketMountId` | path | `string` | yes | BucketMount identifier (e.g. `bm_<22-char-lowercase-hex>`), unique per VM. | **Responses** - `200` — `BucketMount`: BucketMount - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found #### PATCH /v1/vms/{id}/bucket-mounts/{bucketMountId} *Rotate bucket-mount credentials in-place* Replaces the stored credentials and re-authenticates the FUSE mount on the worker. Brief I/O blip (~50-200 ms typical) during the swap. Returns 502 on the rollback path; flips `mountStatus` to `failed` on full failure. Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | | `bucketMountId` | path | `string` | yes | BucketMount identifier (e.g. `bm_<22-char-lowercase-hex>`), unique per VM. | **Request body:** `UpdateBucketMountRequest` **Responses** - `200` — `BucketMount`: Rotation succeeded - `400` — `Error`: Credentials invalid - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: Concurrent rotation in flight (`error: concurrent_rotation`). `Retry-After` indicates safe wait. - `502` — `Error`: Rotation rolled back or failed - `503` — `Error`: Worker/Redis split-brain (`error: worker_state_inconsistent`): the worker has no record of this BM with no rotation in flight (e.g., post worker-restart). The controller reaper reconciles at lease expiry; clients should NOT retry. #### DELETE /v1/vms/{id}/bucket-mounts/{bucketMountId} *Detach and delete a bucket-mount* Auth: required (X-API-Key). **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | VM ID (UUID). | | `bucketMountId` | path | `string` | yes | BucketMount identifier (e.g. `bm_<22-char-lowercase-hex>`), unique per VM. | **Responses** - `204`: Detached and deleted - `401` — `Error`: Missing or invalid credentials - `404` — `Error`: Resource not found - `409` — `Error`: Conflict. Either a concurrent rotation owns the worker-side slot (`error: concurrent_rotation`) or the VM is in a transitional state (`error: vm_transitioning`). Both are retry-able; honor `Retry-After`. - `500` — `Error`: Internal server error - `502` — `Error`: Guest reported EBUSY on umount. The BucketMount STAYS attached. - `503` — `Error`: Worker unreachable, OR worker/Redis split-brain (`error: worker_state_inconsistent`). In the split-brain case, the controller reaper reconciles at lease expiry; clients should NOT retry. ### Schemas #### `Error` | Field | Type | Required | Description | | --- | --- | --- | --- | | `error` | `string` | yes | Human-readable error message. | #### `QuotaExceededError` Per-VM service quota exceeded. The `error` token is a stable machine-readable code so SDKs can branch on it; `count` is the configured cap at denial time. | Field | Type | Required | Description | | --- | --- | --- | --- | | `error` | `"vm_service_quota_exceeded"` | yes | | | `count` | `integer` | yes | | #### `DeleteResponse` | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | yes | | | `deleted` | `boolean` | yes | | #### `VMStatus` Lifecycle status. Known values: `provisioning`, `running`, `stopped`, `pausing`, `paused`, `resuming`, `deleting`, `error`. Terminal failure statuses are `error` and `stopped`; transitional values (`provisioning`, `pausing`, `resuming`, `deleting`) indicate the VM is in flight. Additional values may be introduced in future server versions; clients should treat unknown values as "in transition" rather than as hard errors. #### `SnapshotStatus` Snapshot lifecycle status. Known values: `creating`, `ready`, `error`. Additional values may be introduced in future server versions. #### `TTL` Per-VM auto-action timer. The cycle ticks down while the VM is `running` and freezes on pause. `seconds` is the original cycle duration; refresh and PATCH-time updates reset to this value. | Field | Type | Required | Description | | --- | --- | --- | --- | | `seconds` | `integer` | yes | Cycle duration. Refresh resets to this value. Capped at 1 year (31536000s); larger values are rejected with 400. | | `action` | `"pause" | "delete"` | yes | Action taken on expiry. `pause` re-arms the cycle for the next running session; `delete` is terminal. | #### `QuotaExceeded` 429 body returned by `/v1/vms/{id}/resume` when the org's quota for one of the listed dimensions would be exceeded. | Field | Type | Required | Description | | --- | --- | --- | --- | | `error` | `string` | yes | | | `dimension` | `"vcpu" | "memory_mib" | "disk_gib" | "snapshot_count"` | yes | | #### `MachineType` Machine size identifier (e.g. `c1m2`, `c2m4`). Controls CPU and memory allocation. Must be supplied on launch unless restoring from a snapshot. #### `VM` | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | yes | | | `name` | `string` | yes | | | `orgId` | `string` | yes | | | `machineName` | `string` | no | | | `sourceName` | `string` | no | Source snapshot or image name (empty on fresh boot). | | `firewall` | `FirewallPolicy` | no | | | `effectiveFirewall` | `any` | no | Read-only composed view: `firewall` (the user policy) unioned with per-service auto-rules from this VM's registered services. Each auto-rule has source CIDR `::/0` and a `description` of the form `auto: proxy service `. The same policy is what the worker firewall actually enforces. Set `firewall` to mutate; this field is computed per-response from `firewall` and the current service registry, never persisted. | | `metadata` | `Metadata` | no | | | `envVars` | `EnvVars` | no | | | `publicIpv6` | `string` | no | | | `cpu` | `integer` | yes | | | `memoryMiB` | `integer` | yes | | | `diskGiB` | `integer` | yes | | | `status` | `VMStatus` | yes | | | `createdAt` | `string` | yes | | | `deletedAt` | `string` | no | | | `ttl` | `any` | no | Optional auto-action timer. Null when no TTL is configured. See `TTL` for semantics. | | `expiresAtMs` | `integer` | no | Absolute timestamp in ms when the TTL fires. Set only while the VM is `running` (the countdown freezes on pause). | | `ttlRemainingMs` | `integer` | no | Remaining cycle budget in ms. Set only while the VM is paused; restored to `expiresAtMs` on resume. | | `pausedAt` | `string` | no | When the VM became paused; null otherwise. | | `volumes` | `VolumeAttachmentItem[]` | no | Currently-attached volumes on this VM. | | `bucketMounts` | `BucketMount[]` | no | Currently-attached bucket-mounts on this VM. | #### `Snapshot` | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | yes | | | `name` | `string` | yes | | | `orgId` | `string` | yes | | | `vmId` | `string` | yes | | | `firewall` | `FirewallPolicy` | no | | | `metadata` | `Metadata` | no | | | `envVars` | `EnvVars` | no | | | `services` | `SnapshotService[]` | no | Captured service registrations from the source VM at snapshot time. | | `volumes` | `SnapshotVolumeAttachment[]` | no | Volume attachments captured at snapshot time. | | `bucketMounts` | `SnapshotBucketMountAttachment[]` | no | BucketMount metadata captured at snapshot time (no credentials). | | `status` | `SnapshotStatus` | yes | | | `createdAt` | `string` | yes | | #### `PolicyAction` Allow/deny verb. Used both as the per-direction default posture and as each rule's action. #### `IngressRuleKind` Ingress rule kind. Only `cidr` is supported — inbound packets don't carry a domain the worker could match on without TLS interception. #### `EgressRuleKind` Egress rule kind. - `cidr`: match by destination IP/CIDR + port/proto. - `fqdn`: match by destination domain (resolved through the in-process DNS resolver) + port/proto. Resolved IPs land in a per-rule dynamic nft set; the chain emits one rule per fqdn rule keyed on (set, proto, port). Port/proto enforcement on fqdn rules is honest — the prior `kind: domain` shape with a shared allow-set silently ignored them. Fqdn values accept an optional leading `*.` wildcard (e.g. `*.example.com`). Bare wildcards and non-leading wildcards are rejected. Wildcards match one-or-more labels left of the suffix and do not match the apex (matches DNS wildcard semantics). #### `DNSMode` Toggles the meaning of `dns.domains`. - `allow`: allowlist — only listed domains can resolve; any other query returns NXDOMAIN. - `deny`: blocklist — listed domains return NXDOMAIN; all other queries resolve through the upstream resolver. Default is `deny` with an empty list, which means "resolve everything" — the safe default that preserves existing behavior when callers omit the `dns` block. #### `IngressRule` | Field | Type | Required | Description | | --- | --- | --- | --- | | `action` | `PolicyAction` | yes | | | `kind` | `IngressRuleKind` | yes | | | `value` | `string` | yes | CIDR (e.g. `::/0`, `10.0.0.0/8`). IPv4 and IPv6 CIDRs are both accepted in the schema; L3 enforcement coverage per family is a worker-side concern. | | `protocol` | `"tcp" | "udp" | "any"` | yes | | | `ports` | `string` | yes | Single port (`443`), inclusive range (`8080-8090`), or `any`. When `protocol` is `any`, `ports` MUST be `any`. | | `description` | `string` | no | | #### `IngressPolicy` | Field | Type | Required | Description | | --- | --- | --- | --- | | `default` | `PolicyAction` | yes | | | `rules` | `IngressRule[]` | no | | #### `EgressRule` | Field | Type | Required | Description | | --- | --- | --- | --- | | `action` | `PolicyAction` | yes | | | `kind` | `EgressRuleKind` | yes | | | `value` | `string` | yes | For `kind: cidr`, an IPv4 or IPv6 CIDR. For `kind: fqdn`, a domain name with optional leading `*.` wildcard. Must be reachable through the `dns` gate — a fqdn value blocked by `dns.mode`/`dns.domains` is rejected at PUT time as a dead rule. | | `protocol` | `"tcp" | "udp" | "any"` | yes | | | `ports` | `string` | yes | Single port (`443`), inclusive range (`8080-8090`), or `any`. When `protocol` is `any`, `ports` MUST be `any`. | | `description` | `string` | no | | #### `EgressPolicy` | Field | Type | Required | Description | | --- | --- | --- | --- | | `default` | `PolicyAction` | yes | | | `rules` | `EgressRule[]` | no | | #### `DNSPolicy` DNS-layer filtering, independent of egress L4 rules. The resolver applies the DNS gate BEFORE L4 enforcement; a domain blocked here returns NXDOMAIN regardless of what egress.rules says about its IPs. All fields are optional — the server defaults `mode` to `deny` when missing, `domains` to `[]`, and `blockBypass` to false (see `normalizeDNSPolicy` in `scheduler/internal/httpapi/firewall.go`). | Field | Type | Required | Description | | --- | --- | --- | --- | | `mode` | `DNSMode` | no | | | `domains` | `string[]` | no | | | `blockBypass` | `boolean` | no | When true, the worker denies DoT (TCP 853) and the known public DoH endpoint IPs at the nft layer so guests cannot sidestep the in-process resolver. Default `false` — turning this on breaks workloads that legitimately reach `1.1.1.1` / `8.8.8.8` / etc. on TCP/443 for non-DoH reasons (e.g. services whose data plane lives on a Cloudflare anycast IP). Operators who enable DNS allowlist mode typically also flip this on explicitly. | #### `FirewallPolicy` Top-level firewall policy with three independent axes. All sub-blocks are optional — the server substitutes the safe default (ingress deny / egress allow / dns mode=deny + empty) for missing blocks. Sending `firewall: null` on VM create is also valid. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ingress` | `IngressPolicy` | no | | | `egress` | `EgressPolicy` | no | | | `dns` | `DNSPolicy` | no | | #### `PatchFirewallRequest` Partial firewall update. Each block (`ingress`, `egress`, `dns`) is optional; when present, the supplied object replaces that block wholesale. To change a single rule, send the full block with the desired rule list. An empty body (`{}`) is a no-op. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ingress` | `IngressPolicy` | no | | | `egress` | `EgressPolicy` | no | | | `dns` | `DNSPolicy` | no | | #### `SnapshotService` Captured (name, port, h2c) tuple for a single service registration on a snapshotted VM. Carried across snapshot/ restore by `POST /v1/vms` (snapshot-restore branch) so the new VM gets the same service registrations the source VM had at snapshot time. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | yes | | | `port` | `integer` | yes | | | `h2c` | `boolean` | no | | #### `SnapshotRestoreWarnings` Reports best-effort failures during the snapshot-restore service-replay step. Only present when restoring from a snapshot AND the post-create bulk service registration failed. The VM is created successfully and usable; the user can manually re-register the listed services with one `POST /v1/vms/{id}/services` per service. Bulk service registration is atomic at Redis (one Lua call either writes all-N entries or zero), so partial state ("5 of 8 registered") is impossible — the response is always either a VM with all services registered or a VM with zero services and the full list returned here. | Field | Type | Required | Description | | --- | --- | --- | --- | | `servicesRegistrationFailed` | `boolean` | yes | Always `true` when this object is present. | | `unregisteredServices` | `SnapshotService[]` | no | Services from the snapshot that did not land on the new VM. Caller can re-register each via `POST /v1/vms/{id}/services`. | | `reason` | `string` | no | Operator-facing diagnostic for the failure. | #### `VMCreateResponse` VM object as returned by `POST /v1/vms`. On snapshot restore, an optional `snapshotRestoreWarnings` field may be present if the captured services failed to re-register on the new VM. Existing SDK callers that don't know about the field see the unchanged VM wire shape (`omitempty` keeps the field absent on cold boots and on warning-free restores). #### `CreateVMRequest` Boot behavior depends on which fields are set: - `snapshotId` set → restore from snapshot (takes precedence over `machineType` if both are sent). - Otherwise → fresh boot. `machineType` selects the size; if omitted or empty, defaults to `c1m2`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | no | User-facing name (trimmed + whitespace-collapsed, max 64 runes after normalization; longer values are truncated server-side). Auto-generated as `vm-<8-char-id-prefix>` if empty. | | `machineType` | `MachineType` | no | | | `snapshotId` | `string` | no | Snapshot ID to restore from. | | `diskGiB` | `integer` | no | Override the default disk size (GiB). | | `firewall` | `FirewallPolicy` | no | | | `metadata` | `Metadata` | no | | | `envVars` | `EnvVars` | no | | | `ttl` | `TTL` | no | | | `volumes` | `VolumeAttachmentRequest[]` | no | Cold-boot inline volume attachments (managed Volume IDs). On snapshot restore, this list authoritatively replaces the captured list. Omit to use the captured list. | | `bucketMounts` | `CreateBucketMountRequest[]` | no | Cold-boot inline bucket-mounts. Same authoritative-replace semantics on snapshot restore. | #### `UpdateVMRequest` At least one of `name`, `metadata`, or `ttl` must be provided. Sending `metadata: {}` clears all metadata; omitting it leaves existing metadata unchanged. Sending `ttl: null` explicitly clears the TTL; sending a `TTL` object replaces it; omitting the field leaves the current TTL unchanged. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | no | | | `metadata` | `Metadata` | no | | | `ttl` | `any` | no | | #### `CreateSnapshotRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `vmId` | `string` | yes | | | `name` | `string` | no | Snapshot name (trimmed + whitespace-collapsed, max 64 runes; longer values are truncated server-side). Auto-generated as `snapshot-<8-char-vmId-prefix>` if empty. | #### `UpdateSnapshotRequest` Rename a snapshot. `name` is optional; if omitted or empty, the server regenerates the auto-name (`snapshot-<8-char-vmId-prefix>`). | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | no | | #### `SnapshotImportSourceSpec` Discriminated source descriptor. `type` selects which other fields are consumed. The opposite-variant fields must be omitted; mixing them is a 400 at the API boundary. | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | `"image" | "dockerfile"` | yes | - `image`: pull an existing Docker / OCI image reference. - `dockerfile`: build a user-supplied Dockerfile against an uploaded build context. | | `image` | `string` | no | OCI image reference (e.g. `ghcr.io/foo/bar:v1`, `nginx:1.27`, `alpine@sha256:…`). Required when `type=image`. | | `platform` | `string` | no | OCI platform selector for multi-arch image indexes, format `/` (e.g. `linux/amd64`). Defaults to `linux/amd64`. Image-variant only. | | `registryUsername` | `string` | no | Optional username for private registry pulls. Applies to both source kinds: `type=image` authenticates the OCI pull, `type=dockerfile` authenticates the `FROM` pulls performed by `buildah` inside the sandbox VM. | | `registryPassword` | `string` | no | Optional password / PAT / OAuth token for private registry pulls. Applies to both source kinds. Held in scheduler process memory between create and dispatch (never persisted) and wiped after the build VM is torn down. | | `registryHost` | `string` | no | Registry hostname the `registryUsername` / `registryPassword` authenticate against (e.g. `docker.io`, `ghcr.io`, `1234.dkr.ecr.us-east-1.amazonaws.com`). **Required** when credentials are set on `type=dockerfile`: the baker keys the auth.json entry against this host. Tolerated but ignored for `type=image` (the host is derived from the image reference). Optional port: e.g. `registry.example.com:5000`. | | `contextRef` | `string` | no | Opaque one-shot token returned by `POST /v1/snapshot-imports/context-presign`. Required when `type=dockerfile`. The platform validates that the referenced upload belongs to the calling org and consumes the token on use. | | `dockerfilePath` | `string` | no | Path to the Dockerfile relative to the context root. Defaults to `Dockerfile`. Must not be absolute and must not contain `..`. | | `buildArgs` | `object` | no | Optional `--build-arg KEY=VALUE` pairs forwarded to the build. Capped at 64 entries, 8 KiB total. | | `target` | `string` | no | Optional multi-stage `--target` selector. Empty means the final stage. | #### `CreateSnapshotImportRequest` Body for `POST /v1/snapshot-imports`. The discriminated `source` is the only image-bearing field; everything else is sizing or labels. | Field | Type | Required | Description | | --- | --- | --- | --- | | `machineType` | `MachineType` | no | | | `diskGiB` | `integer` | no | Disk size for the produced snapshot. Defaults to the machine type's catalog default (typically 10 GiB). | | `name` | `string` | no | Optional human-readable label for the resulting import and snapshot. If omitted, the import id is used. | | `source` | `SnapshotImportSourceSpec` | yes | | #### `SnapshotImportEvent` One entry in an import's append-only event log. Phase + status pairs describe the sub-stages of `running` (preparing → network → pull → export → saving → warming). | Field | Type | Required | Description | | --- | --- | --- | --- | | `phase` | `string` | yes | Pipeline sub-phase. Known values include `preparing`, `network`, `pull` (image source), `fetch_context`, `build` (dockerfile source), `export`, `saving`, `warming`, `done`. | | `status` | `string` | yes | Event status. Known values include `started`, `completed`, `failed`, `skipped`, `cancelled`. | | `timestampMs` | `integer` | yes | Unix-epoch milliseconds. | | `message` | `string` | no | Optional user-safe summary. Never contains credentials or internal paths. | #### `SnapshotImportSourceView` Publicly-rendered source descriptor returned on `GET /v1/snapshot-imports/{id}`. Strips secrets (`registryPassword`, raw context object keys) — only fields safe to echo back to the caller appear here. | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | `"image" | "dockerfile"` | yes | | | `image` | `string` | no | | | `platform` | `string` | no | | | `registryUsername` | `string` | no | | | `registryHost` | `string` | no | Registry hostname for dockerfile-source private builds. Empty for image-source (derived from the image reference, not stored). | | `dockerfilePath` | `string` | no | | | `buildArgs` | `object` | no | | | `target` | `string` | no | | | `contextSizeBytes` | `integer` | no | | #### `SnapshotImportResponse` Current state of a snapshot import. Returned by `POST /v1/snapshot-imports` (initial `pending` state), `GET /v1/snapshot-imports/{id}`, `GET /v1/snapshot-imports` (in the array elements), and `POST /v1/snapshot-imports/{id}/cancel`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | yes | Import id (UUID). | | `name` | `string` | no | | | `source` | `SnapshotImportSourceView` | yes | | | `status` | `string` | yes | Current state. Known values: `pending` (queued, no worker yet), `claimed` (worker assigned, dispatch in flight), `running` (worker executing the pipeline), `succeeded` / `failed` / `cancelled` (terminal). | | `snapshotId` | `string` | no | Set when `status` is `succeeded`. Fetch the corresponding Snapshot record via `GET /v1/snapshots/{id}`. | | `error` | `string` | no | Set when `status` is `failed`. User-safe diagnostic. | | `events` | `SnapshotImportEvent[]` | no | | | `machineName` | `string` | no | | | `cpu` | `integer` | no | | | `memoryMiB` | `integer` | no | | | `diskGiB` | `integer` | no | | | `createdAt` | `string` | yes | | | `startedAt` | `string` | no | | | `updatedAt` | `string` | no | | | `finishedAt` | `string` | no | | #### `ContextPresignRequest` Body for `POST /v1/snapshot-imports/context-presign`. All fields optional; clients that know the upload size up front can supply `sizeBytes` to get an early rejection if the payload would exceed the platform cap. | Field | Type | Required | Description | | --- | --- | --- | --- | | `sizeBytes` | `integer` | no | Planned upload size. The server rejects this request with `400` when it exceeds the platform-wide cap (the same cap is also enforced by the signed URL itself). | #### `ContextPresignResponse` One-shot upload handle for the dockerfile-source flow. | Field | Type | Required | Description | | --- | --- | --- | --- | | `contextRef` | `string` | yes | Opaque token to pass as `source.contextRef` on the subsequent `POST /v1/snapshot-imports`. Single-use; the create call consumes the entry. | | `uploadUrl` | `string` | yes | Short-lived signed PUT URL. Upload the build-context ZIP archive here with `Content-Type: application/zip`. | | `expiresInSec` | `integer` | yes | TTL of `uploadUrl`, in seconds. | | `maxUploadBytes` | `integer` | yes | Server-side cap on upload size. The signed URL also enforces this server-side. | #### `Metadata` Free-form string→string map. Server-enforced limits: up to 256 keys, key length 1–256 bytes, value length ≤4096 bytes, total JSON encoding ≤65536 bytes. #### `EnvVars` Environment variable string→string map injected into the VM at boot. Keys must be 1–256 bytes and match shell-variable name (`[A-Za-z_][A-Za-z0-9_]*`); values may not contain newline, carriage return, or null bytes. Total JSON encoding ≤65536 bytes. #### `ExecVMRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `command` | `string[]` | yes | Argv-style command. First element must be non-empty. For shell strings, wrap as `["sh", "-c", ""]`. | | `timeoutSec` | `integer` | no | Server-side execution timeout in seconds. Must be positive when provided; omit to use the server default. | | `stdin` | `string` | no | Optional base64-encoded stdin blob, written to the child's stdin before the process starts reading much and then closed. Streaming stdin is not supported — pipe from a file inside the guest if you need that shape. | #### `ExecEvent` One event in the NDJSON exec stream returned by `POST /v1/vms/{id}/exec` under `Accept: application/x-ndjson`. Short field names (`t`, `d`, `c`, `to`, `ms`) keep per-chunk overhead small since high-output commands can produce thousands of events per exec. | Field | Type | Required | Description | | --- | --- | --- | --- | | `t` | `"o" | "e" | "x"` | yes | Event type: `o` = stdout chunk, `e` = stderr chunk, `x` = terminal exit event. | | `d` | `string` | no | For `o`/`e`: base64-encoded raw bytes of the chunk. For `x`: optional diagnostic string (e.g. spawn failure) when non-empty. | | `c` | `integer` | no | Exit code. Present on `x` events only. | | `to` | `boolean` | no | True if the command was killed by the timeout. `x` events only. | | `ms` | `integer` | no | Guest-reported duration in milliseconds. `x` events only. | #### `ExecVMResponse` Buffered response shape for `POST /v1/vms/{id}/exec` under `Accept: application/json`. The server collects the streamed events and returns this aggregate once the command exits. Per-stream output is capped at 4 MiB; overflow bytes are dropped and signalled via `stdoutTruncated` / `stderrTruncated`. Streaming clients (`Accept: application/x-ndjson`) receive every byte without a cap. | Field | Type | Required | Description | | --- | --- | --- | --- | | `exitCode` | `integer` | yes | | | `stdout` | `string` | yes | | | `stderr` | `string` | yes | | | `timedOut` | `boolean` | yes | | | `stdoutTruncated` | `boolean` | yes | True if the collector dropped stdout bytes past the 4 MiB cap. | | `stderrTruncated` | `boolean` | yes | True if the collector dropped stderr bytes past the 4 MiB cap. | | `durationMs` | `integer` | yes | | #### `FilePresignRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `path` | `string` | yes | Absolute destination path inside the guest filesystem (where the file will land after `fetchFileToVm`). Used only to scope the staging object key; any value server-side is accepted here. | #### `FilePresignResponse` Pair of signed URLs scoped to the same per-VM staging object. Usable in either direction: either side (client or VM) PUTs bytes to `uploadUrl`, and either side GETs them back via `downloadUrl`. URLs expire after `expiresInSec` seconds and the staging object is auto-deleted after about a day. | Field | Type | Required | Description | | --- | --- | --- | --- | | `uploadUrl` | `string` | yes | Presigned PUT URL for the staging object. Accepts `Content-Type: application/octet-stream`. Used by the client on upload, or by the VM (via an exec'd `curl -T -`) on download. | | `downloadUrl` | `string` | yes | Presigned GET URL for the same staging object. Used by the VM (via `POST /v1/vms/{id}/files/fetch`) on upload, or by the client (via `httpx.stream` / `curl`) on download. | | `expiresInSec` | `integer` | yes | Lifetime of both URLs in seconds. | | `maxUploadBytes` | `integer` | yes | Upper bound on upload size (equals the VM's disk size in bytes). | #### `FileFetchRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | `string` | yes | Must be the `downloadUrl` previously returned by `POST /v1/vms/{id}/files/presign` (URLs from other sources are rejected). | | `path` | `string` | yes | Absolute destination path inside the guest filesystem. | | `timeoutSec` | `integer` | no | Per-fetch timeout in seconds. | #### `ConsoleTokenResponse` | Field | Type | Required | Description | | --- | --- | --- | --- | | `token` | `string` | yes | | | `expiresInSec` | `integer` | yes | | | `websocketPath` | `string` | yes | Relative WebSocket path; combine with your API host as `wss://?session=`. | #### `SshKey` | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | no | Optional human label. | | `publicKey` | `string` | yes | OpenSSH-format public key, of the form ` ` — the optional comment is stripped server-side. Supported types: `ssh-ed25519`, `ssh-rsa`, `ecdsa-sha2-nistp{256,384,521}`, plus FIDO2 hardware-backed variants (`sk-...@openssh.com`). | | `fingerprint` | `string` | yes | OpenSSH SHA256 fingerprint, e.g. `SHA256:abc...`. This is the **identifier** — matches what `ssh-keygen -lf` prints and what your ssh client shows on first connect; pass it back as the `{fingerprint}` path segment to `deleteSshKey`. | | `createdAt` | `string` | yes | | #### `SshKeyListResponse` | Field | Type | Required | Description | | --- | --- | --- | --- | | `keys` | `SshKey[]` | yes | | #### `AddSshKeyRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | no | Optional human label. | | `publicKey` | `string` | yes | OpenSSH-format public key (`ssh-ed25519 AAA...`). Comments are stripped. Newlines are rejected. | #### `OrgQuotaValues` | Field | Type | Required | Description | | --- | --- | --- | --- | | `vcpu` | `integer` | yes | | | `memoryMiB` | `integer` | yes | | | `diskGiB` | `integer` | yes | | | `snapshotCount` | `integer` | yes | | | `volumeCount` | `integer` | yes | | | `volumeGiB` | `integer` | yes | | #### `OrgQuotaUsage` | Field | Type | Required | Description | | --- | --- | --- | --- | | `orgId` | `string` | yes | | | `limits` | `OrgQuotaValues` | yes | | | `usage` | `OrgQuotaValues` | yes | | #### `Service` | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | yes | Service name (1–29 chars). Embedded in the public URL as `--.proxy.`. | | `port` | `integer` | yes | TCP port the service listens on inside the VM. Privileged ports (<1024) are rejected. | | `h2c` | `boolean` | yes | When true, the proxy speaks HTTP/2 cleartext (h2c) to the backend. Required for gRPC and h2c-only apps. When false (default), the proxy uses HTTP/1.1 — covers HTTP/1.1 apps, Server-Sent Events, and WebSocket pass-through. | #### `RegisterServiceRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | yes | | | `port` | `integer` | yes | | | `h2c` | `boolean` | no | Optional. When true, the proxy uses HTTP/2 cleartext to the backend (required for gRPC). Defaults to false (HTTP/1.1). | #### `UpdateServiceRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `port` | `integer` | yes | New TCP port. Same value as the existing entry is a no-op. | | `h2c` | `boolean` | no | Optional. When true, the proxy uses HTTP/2 cleartext to the backend. Same value as the existing entry is a no-op; a different value updates the registered transport. | #### `Volume` | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | yes | | | `name` | `string` | yes | | | `orgId` | `string` | yes | | | `accessMode` | `string` | yes | Access mode. Known values: `rw`, `ro`. Future server versions may introduce additional values. | | `sizeGiB` | `integer` | yes | | | `status` | `string` | yes | Lifecycle status. Known values: - `creating` — the substrate-create saga is in flight. Set by the server briefly between the customer's `POST /v1/volumes` and the worker substrate provisioning; attach attempts are rejected with `VOL_NOT_READY` until the saga commits. Clients polling immediately after create may observe this state. - `ready` — substrate is up; attachable. - `deleting` — cleanup is in progress; not attachable. Future server versions may introduce additional values. | | `pendingSizeGiB` | `integer` | no | When non-zero, a resize saga is in flight; `sizeGiB` is still the pre-resize value and `pendingSizeGiB` is the target. Set briefly between `PATCH /v1/volumes/{id}` and the substrate resize commit. Clients polling immediately after a resize may observe a non-zero value. | | `mountedCount` | `integer` | yes | Number of currently-running VMs with this volume attached (paused VMs are NOT counted). | | `usedGiB` | `integer` | no | Bytes used inside the volume (rounded down to GiB). Fetched on-demand from the substrate; omitted when the substrate is unreachable. | | `createdAt` | `string` | yes | | #### `CreateVolumeRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | yes | | | `sizeGiB` | `integer` | yes | | | `accessMode` | `"rw" | "ro"` | yes | | #### `UpdateVolumeRequest` At least one of `name`, `sizeGiB`, or `accessMode` must be present. `accessMode` requires `mountedCount == 0`. `sizeGiB` shrink requires `usedGiB <= newSizeGiB`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | no | | | `sizeGiB` | `integer` | no | | | `accessMode` | `"rw" | "ro"` | no | | #### `VolumeAttachmentRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `volumeId` | `string` | yes | | | `mountPath` | `string` | yes | Absolute path; must start with /mnt/ or /data/. | | `readOnly` | `boolean` | no | | #### `VolumeAttachmentItem` | Field | Type | Required | Description | | --- | --- | --- | --- | | `volumeId` | `string` | yes | | | `mountPath` | `string` | yes | | | `readOnly` | `boolean` | no | | | `mountStatus` | `string` | yes | Known values: `mounted`, `failed`, `pending`. `pending` appears on attachments to paused VMs (mount happens on resume) and briefly during in-flight hot-attach. | | `statusMessage` | `string` | no | | #### `VolumeAttachmentItemWithVm` #### `DetachVolumeResponse` | Field | Type | Required | Description | | --- | --- | --- | --- | | `detached` | `boolean` | yes | | | `warnings` | `DetachWarning[]` | no | | #### `DetachWarning` | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | `string` | yes | Known values: `ack_timeout`, `guest_unresponsive`. | | `message` | `string` | yes | | #### `BucketMount` | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | yes | | | `vmId` | `string` | yes | | | `bucketUri` | `string` | yes | `gs://...` or `s3://...`; future schemes may be added. | | `mountPath` | `string` | yes | | | `readOnly` | `boolean` | no | | | `mountStatus` | `string` | yes | Known values: `mounted`, `failed`, `pending`. | | `statusMessage` | `string` | no | | | `createdAt` | `string` | yes | | #### `BucketMountCredentials` Customer-provided credentials. Never returned in API responses. Discriminated union: the `type` property selects the per-provider shape so SDKs surface typed per-type values. #### `GcpServiceAccountCredentials` | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | `"gcp-service-account-json"` | yes | | | `value` | `object` | yes | | #### `AwsCredentials` | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | `"aws-credentials"` | yes | | | `value` | `object` | yes | | #### `CreateBucketMountRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `bucketUri` | `string` | yes | Customer's GCS or S3 bucket URI. `gs://[/prefix]` or `s3://[/prefix]`. | | `mountPath` | `string` | yes | | | `readOnly` | `boolean` | no | | | `credentials` | `BucketMountCredentials` | yes | | #### `UpdateBucketMountRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `credentials` | `BucketMountCredentials` | yes | | #### `AttachmentWarnings` | Field | Type | Required | Description | | --- | --- | --- | --- | | `skippedSnapshotVolumes` | `SnapshotVolumeSkip[]` | no | | | `failedVolumeAttachments` | `FailedVolumeAttachment[]` | no | | | `skippedSnapshotBucketMounts` | `SnapshotBucketMountSkip[]` | no | | | `failedBucketMountAttachments` | `FailedBucketMountAttachment[]` | no | | #### `SnapshotVolumeSkip` | Field | Type | Required | Description | | --- | --- | --- | --- | | `volumeId` | `string` | yes | | | `mountPath` | `string` | yes | | | `reason` | `string` | yes | Known values: `deleted`, `deleting`, `cross_org`, `vol_ro_ceiling_after_patch`. | #### `FailedVolumeAttachment` | Field | Type | Required | Description | | --- | --- | --- | --- | | `volumeId` | `string` | yes | | | `mountPath` | `string` | yes | | | `statusMessage` | `string` | yes | | #### `SnapshotBucketMountSkip` | Field | Type | Required | Description | | --- | --- | --- | --- | | `bucketUri` | `string` | yes | | | `mountPath` | `string` | yes | | | `reason` | `string` | yes | Known values: `credentials_invalid`, `bucket_unreachable`, `credentials_unavailable`. | #### `FailedBucketMountAttachment` | Field | Type | Required | Description | | --- | --- | --- | --- | | `bucketUri` | `string` | yes | | | `mountPath` | `string` | yes | | | `statusMessage` | `string` | yes | | #### `SnapshotVolumeAttachment` | Field | Type | Required | Description | | --- | --- | --- | --- | | `volumeId` | `string` | yes | | | `mountPath` | `string` | yes | | | `readOnly` | `boolean` | no | | #### `SnapshotBucketMountAttachment` | Field | Type | Required | Description | | --- | --- | --- | --- | | `bucketUri` | `string` | yes | | | `mountPath` | `string` | yes | | | `readOnly` | `boolean` | no | |