# Isle > Application-ready desktop environments for AI computer-use agents. Isle provides managed sandbox environments running real desktop applications (KiCad and FreeCAD) that AI agents can control via screenshots, mouse, keyboard, and file transfer. Each sandbox includes automatic checkpointing, crash recovery, and application containment. ## Install Install the SDK from PyPI. Requires Python 3.9 or later. The package is `isle-sdk`; the Python import is `isle`. ```bash python -m pip install isle-sdk ``` Set your API key: ```bash export ISLE_API_KEY=isle_... ``` ## Quickstart ```python from isle import Client isle = Client() # uses ISLE_API_KEY env var sandbox = isle.sandboxes.create("kicad", name="Power supply board") sandbox.wait_until_ready() # Take a screenshot image = sandbox.screen.screenshot() # returns PNG bytes # Mouse control sandbox.mouse.click(500, 300) sandbox.mouse.move(200, 400) sandbox.mouse.double_click(500, 300) sandbox.mouse.scroll(0, -500) # Keyboard control sandbox.keyboard.type("100nF") sandbox.keyboard.keypress(["CTRL", "S"]) # File transfer sandbox.files.upload("schematic.kicad_sch") sandbox.files.download("/home/user/work/board.kicad_pcb") # Stop when done (archives the sandbox, billing pauses) sandbox.stop() sandbox.wait_until_stopped() # Resume later sandbox.resume() # Permanently delete after stopping sandbox.stop() sandbox.wait_until_stopped() deletion = sandbox.destroy() tracking_id = deletion["provider_deletion_tracking_id"] # Poll separately only when verified provider erasure is required. erasure = isle.provider_deletions.wait(tracking_id, timeout=300) ``` ## Environments ### KiCad (`kicad`) EDA environment for schematic capture, PCB layout, and electronics design. Supports KiCad native, Gerber, and BOM export formats. ### FreeCAD (`freecad`) CAD environment for 3D modeling, parametric design, and engineering. Supports STEP, IGES, STL, OBJ, and native FreeCAD formats. ## API Reference For direct HTTP integrations, see the REST API Reference section below and https://www.tryisle.com/docs/rest. The following section documents the Python SDK. ### Client ```python from isle import Client isle = Client(api_key="isle_...", base_url="https://www.tryisle.com") ``` - `api_key` (str, optional): API key. Falls back to `ISLE_API_KEY` env var. - `base_url` (str, optional): API base URL. Defaults to `https://www.tryisle.com`. The client is a context manager: ```python with Client() as isle: sandbox = isle.sandboxes.create("kicad") ``` ### Sandbox Management #### `isle.sandboxes.create(environment, *, name=None, retain=True, timeout=360.0, idempotency_key=None) -> Sandbox` Create a new sandbox. - `environment` (str): Environment type (`"kicad"` or `"freecad"`). - `name` (str, optional): Human-readable name. - `retain` (bool): If True, the sandbox can be stopped and resumed. Default True. - `timeout` (float): Provisioning request timeout in seconds. Default 360. - `idempotency_key` (str, optional override): The SDK generates and sends a unique `Idempotency-Key` automatically. Supply this argument to reuse a stable operation key across separate processes. A lost transport response is retried once with the exact same key so it cannot create a second environment. #### `isle.sandboxes.list() -> list[Sandbox]` List all sandboxes. #### `isle.sandboxes.get(sandbox_id) -> Sandbox` Get a sandbox by ID. ### Sandbox A `Sandbox` has these attributes: - `id` (str): Unique sandbox ID. - `name` (str): Human-readable name. - `environment` (str): Environment type. - `status` (str): Current status (`"starting"`, `"running"`, `"resuming"`, `"stopping"`, `"stopped"`, `"error"`). - `stream_url` (str | None): URL to watch the sandbox's screen live. - `retain` (bool): Whether the sandbox persists after stopping. #### `sandbox.wait_until_ready(timeout=120.0, poll=1.0) -> None` Block until the sandbox status is `"running"`. Raises `TimeoutError` if it takes longer than `timeout` seconds. Raises `Exception` if the sandbox enters `"error"` or `"stopped"` state. #### `sandbox.refresh() -> None` Refresh sandbox status and metadata from the API. #### `sandbox.rename(name) -> None` Rename the sandbox. #### `sandbox.stop() -> None` Stop and archive the sandbox. Files persist, billing pauses. Resume later with `sandbox.resume()`. #### `sandbox.wait_until_stopped(timeout=120.0, poll=1.0) -> None` Wait until a stop operation finishes. Raises `TimeoutError` if the deadline expires. #### `sandbox.resume(timeout=120.0, poll=1.0) -> None` Resume a stopped sandbox. Blocks until the sandbox is running again. #### `sandbox.destroy(timeout=120.0) -> dict` Delete a stopped or failed sandbox and its Isle-managed stored data, and request permanent deletion from the compute provider. The returned provider operation fields expose the separately reconciled asynchronous deletion. An accepted deletion returns immediately; use `provider_deletions.wait()` when provider-erasure completion is required. Do not interpret HTTP acceptance, Box absence, or Isle-local cleanup as a completed provider-erasure receipt. #### `isle.provider_deletions.get(tracking_id) -> dict` Return the latest provider-erasure audit status for a deletion tracking ID. The authenticated REST endpoint `GET /api/provider-deletions?scope=active|recent|all&limit=1..100&cursor=...` lists owner-scoped deletion audits. `active` includes pending, failed, and manual-verification records even after Isle-local sandbox data is gone; `recent` includes completed records updated in the last 30 days. Follow the opaque `next_cursor` until it is null when complete pagination is required. #### `isle.provider_deletions.wait(tracking_id, *, timeout=120.0, poll=1.0) -> dict` Poll until provider erasure completes, fails, or requires manual verification. Raises `TimeoutError` if the operation remains pending past the deadline. ### Screen #### `sandbox.screen.screenshot() -> bytes` Capture a screenshot of the sandbox's screen. Returns PNG image bytes. ### Mouse #### `sandbox.mouse.click(x, y, button="left") -> dict` Click at the given coordinates. - `button` (str): `"left"`, `"right"`, or `"middle"`. #### `sandbox.mouse.double_click(x, y, button="left") -> dict` Double-click at the given coordinates. #### `sandbox.mouse.move(x, y) -> dict` Move the cursor to the given coordinates. #### `sandbox.mouse.scroll(x, y) -> dict` Scroll at the given coordinates. Positive `y` scrolls down, negative scrolls up. ### Keyboard #### `sandbox.keyboard.type(text) -> dict` Type a string of text. #### `sandbox.keyboard.keypress(keys) -> dict` Press a key combination. - `keys` (list[str]): Key names, e.g. `["CTRL", "S"]`, `["ALT", "F4"]`, `["ENTER"]`. ### Files #### `sandbox.files.upload(local_path, remote_path=None) -> dict` Upload a local file up to 4 MiB to the sandbox. If `remote_path` is not specified, the file is placed in the sandbox's default upload directory. #### `sandbox.files.download(remote_path, local_path=None) -> str` Download a file up to 4 MiB from the sandbox. If `local_path` is not specified, uses the filename from `remote_path`. Returns the local path where the file was saved. ### Checkpoints and Recovery #### `sandbox.checkpoint() -> dict` Manually create a checkpoint of the current state. Save the project in the desktop application first. On current images the GUI, file API, and monitor share `/home/user/work`. For KiCad, save the project as `/home/user/work/project.kicad_pro`; keep custom libraries, hierarchical sheets, and models inside that workspace. An unsaved project returns HTTP 404 with `code="artifact_not_found"` and the expected `path`. An invalid or changing workspace returns HTTP 422 with `code="artifact_invalid"`. Current KiCad checkpoints are `project.kicad.zip` bundles with a versioned manifest and per-file SHA-256 hashes. They include the saved project, schematic, PCB, and other regular workspace files, excluding locks, local UI preferences, autosaves, backup folders, `.git`, `.cache`, and Isle temporary files. A schematic-only or PCB-only project is supported; a settings-only project is not a complete checkpoint. The workspace and bundle must each fit within 256 MiB. Save all editors before checkpointing; in-memory edits are not captured. Older `.kicad_pro` checkpoints contain only project settings. R2 listings label them `format="legacy-kicad-project", restorable=false`. They remain downloadable subject to normal retention, but are not automatically restored as a complete design. Current bundles use `format="kicad-project-v1", restorable=true`. Recovery restores the captured workspace files and removes later-added regular files from the captured set. Interrupted restores are rolled back before the application starts. #### `sandbox.recover() -> dict` Restore the last safe checkpoint and restart the application. #### `sandbox.restart_app() -> dict` Restart the application without restoring a checkpoint. #### `sandbox.checkpoints(source=None) -> list[dict]` List live checkpoints, or pass `source="r2"` to list persisted checkpoints. #### `sandbox.download_checkpoint(checkpoint_id) -> str` Get a download URL for a specific checkpoint. #### `sandbox.monitor_status() -> dict` Get the application health monitor status. Returns a dict with `status`, `checkpoints` count, and other health info. ### Events and Recording #### `sandbox.events(after=None, limit=200) -> list[dict]` List command trace events (mouse clicks, keypresses, etc.) for the sandbox. - `after` (str, optional): Cursor for pagination. - `limit` (int): Max events to return. Default 200. #### `sandbox.recording_url() -> str` Get the URL for the sandbox's screen recording. When retention is enabled, stop finalizes the recording through the signed root monitor and streams it directly from the environment to durable storage; the application server does not proxy the recording bytes. Recordings are capped at 512 MiB, six hours, or the point where the environment has less than 1 GiB free. Use `sandbox.recording_info()` (or inspect `sandbox.recording_truncated` and `sandbox.recording_truncation_reason` after calling `recording_url`) to distinguish a capped partial recording from a complete one. #### `sandbox.recording_info() -> dict` Return the signed recording URL plus `truncated` and `truncation_reason`. ## Full Example ```python from isle import Client isle = Client() # Create a KiCad environment sandbox = isle.sandboxes.create("kicad", name="Power supply board") sandbox.wait_until_ready() print(f"Sandbox ready: {sandbox.id}") print(f"Stream: {sandbox.stream_url}") # Upload input file sandbox.files.upload("schematic.kicad_sch") # Agent loop: screenshot -> decide -> act image = sandbox.screen.screenshot() # ... your agent processes the image and decides what to do ... sandbox.mouse.click(500, 300) sandbox.keyboard.type("100nF") sandbox.keyboard.keypress(["CTRL", "S"]) # Create a manual checkpoint before a risky operation sandbox.checkpoint() # Download the result sandbox.files.download("/home/user/work/board.kicad_pcb", "board.kicad_pcb") # Stop when done sandbox.stop() sandbox.wait_until_stopped() deletion = sandbox.destroy() tracking_id = deletion["provider_deletion_tracking_id"] erasure = isle.provider_deletions.wait(tracking_id, timeout=300) if not erasure["provider_deletion_complete"]: print(erasure["provider_operation_status"]) ``` ## REST API Reference Public guide: https://www.tryisle.com/docs/rest Base URL: https://www.tryisle.com/api Authentication: `Authorization: Bearer isle_...` on every API request. Create keys in https://www.tryisle.com/api-keys. Requests are owner-scoped. Use JSON bodies with `Content-Type: application/json`, except multipart uploads. Use the canonical www host to avoid redirects. Keep keys in a server/agent process; use your own backend for browser integrations. No Python SDK is needed. Paths below are relative to the API base URL; `{id}` is the Isle sandbox ID. ### HTTP quickstart (curl and jq) ```bash export ISLE_API_KEY='isle_...' export ISLE_BASE_URL='https://www.tryisle.com' # Choose a new operation key once. Reuse it and the exact body after a timeout. export ISLE_CREATE_KEY="desktop-$(date +%s)-$$" curl --fail-with-body --silent --show-error --max-time 360 \ -X POST "$ISLE_BASE_URL/api/sandboxes" \ -H "Authorization: Bearer $ISLE_API_KEY" \ -H 'Content-Type: application/json' \ -H "Idempotency-Key: $ISLE_CREATE_KEY" \ -d '{"environment":"kicad","name":"Power supply board","retain":true}' \ -o sandbox.json SANDBOX_ID=$(jq -er '.id' sandbox.json) export SANDBOX_ID # Poll this endpoint until status is running before input, screenshots, or files. # Use a deadline and backoff; inspect last_error on error/stopped states. curl --fail-with-body --silent --show-error \ "$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID" \ -H "Authorization: Bearer $ISLE_API_KEY" # Once running: curl --fail-with-body --silent --show-error \ "$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/screenshot" \ -H "Authorization: Bearer $ISLE_API_KEY" -o screen.png # Stop when done, then poll GET until status is stopped. curl --fail-with-body --silent --show-error -X DELETE \ "$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID" \ -H "Authorization: Bearer $ISLE_API_KEY" ``` ### Sandboxes and lifecycle - `POST /sandboxes`: required `environment` (`kicad` or `freecad`), optional `name` (trimmed, up to 64 characters, no control characters; omitted/blank chooses an application default), optional boolean `retain` (default true). `Idempotency-Key` is REQUIRED: 1–128 letters, digits, `.`, `_`, `:`, or `-`. Keys are scoped to your account. Reusing a key with different parameters is 409; replaying a deleted environment is 410 while its key record is retained. Returns a sandbox with 201 (new), 200 (replay), or 202 (pending provisioning). A 2xx response does not imply readiness. Preserve the key if a response is lost. - `GET /sandboxes`: array of owned sandboxes, newest created first; no pagination. - `GET /sandboxes/{id}`: one sandbox; poll this endpoint for lifecycle state. - `PATCH /sandboxes/{id}`: `{"name":"New name"}`; nonempty trimmed name, max 64 characters, no control characters. Returns the updated sandbox. - `DELETE /sandboxes/{id}`: stop/archive, NOT permanent deletion. Returns 200 with `{"status":"stopping","stop_job_id":"..."}`, or `{"status":"stopped","already_stopped":true}`. Safe to repeat. No body. - `POST /sandboxes/{id}/resume`: stopped sandbox only; no body. Returns `{"status":"resuming"}` (200 or a pending 202 response). Poll until running. On `resume_outcome="unknown", retry_safe=false`, poll instead of replaying. - `DELETE /sandboxes/{id}/destroy`: stopped/error sandbox only; see deletion below. Sandbox fields: `id`, `name`, `environment`, `status`, `stream_url`, `retain`, `recording_url`, `recording_truncated`, `recording_truncation_reason`, `session_limit_minutes`, `session_ttl_seconds`, `recovery_attempts`, `created_at`, `started_at`, `stopped_at`, `last_error`, `last_error_at`. Timestamps are ISO 8601; unavailable timestamps/URLs/errors may be null. A null session limit means no per-session limit, not unlimited account usage. `recording_url` is a stored reference; use GET /recording for a downloadable URL. States: starting -> running -> stopping -> stopped; resume uses stopped -> resuming -> running. Failures can enter error. Create responses can also contain `provisioning_pending` and `provisioning_requires_manual_intervention`. Compute billing pauses after stop. A client timeout does not cancel creation. ### Screen, mouse, and keyboard (running only) - `GET /sandboxes/{id}/screenshot`: raw PNG bytes (`image/png`), private/no-store. - `POST /sandboxes/{id}/mouse`: JSON `action` = move/click/double_click/scroll; required integer `x`, `y`; optional `button` = left (default)/middle/right. Coordinates are 0–100000. For scroll, y is a signed amount (-100000–100000), positive down, negative up; y=-500 scrolls up five lines. x is the horizontal target and the vertical target is 0. Avoid y=0, which currently scrolls up once. - `POST /sandboxes/{id}/keyboard`: `{"action":"type","text":"100nF"}` (1–10000 characters), or `{"action":"keypress","keys":["CTRL","S"]}`. Case-insensitive keys: letters, digits, F1–F12, ENTER/RETURN, TAB, ESC/ESCAPE, arrows, SPACE, DELETE, HOME, END, PAGEUP, PAGEDOWN. Modifiers: CTRL/CONTROL, SHIFT, ALT, OPTION, CMD/COMMAND, FN. Use one key or unique modifiers then one key; a modifier alone is invalid. Input success includes `ok=true`, `success=true`, `effect="unverifiable"`, `route="global_input"`, optionally delivery/evidence metadata. This reports delivery; inspect a new screenshot to verify application state. An ambiguous 502 includes `action_outcome="unknown", retry_safe=false`. Do not automatically retry input after this response or a transport timeout: it may already have run. ```bash curl --fail-with-body --silent --show-error \ "$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/mouse" \ -H "Authorization: Bearer $ISLE_API_KEY" -H 'Content-Type: application/json' \ -d '{"action":"click","x":500,"y":300,"button":"left"}' curl --fail-with-body --silent --show-error \ "$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/keyboard" \ -H "Authorization: Bearer $ISLE_API_KEY" -H 'Content-Type: application/json' \ -d '{"action":"keypress","keys":["CTRL","S"]}' ``` ### Files (running only) `POST /sandboxes/{id}/files`: multipart with `file` and optional `path` (default `/home/user/work/`), or JSON with `path`, `contents`, and optional `encoding` (`utf8` default or `base64`). Returns `{"path":"..."}`. `GET /sandboxes/{id}/files?path=...`: URL-encoded absolute path; returns raw `application/octet-stream` bytes and an attachment filename. Paths must be beneath `/home/user/work/`, at most 4096 characters, with no control characters, dot/dot-dot segments, or doubled slashes. Only regular files are permitted; symlinks/unsafe boundaries are rejected. Maximum decoded file size is 4 MiB (4194304 bytes). Multipart is preferred near the limit: base64/JSON expansion must also fit the host's 4.5 MiB HTTP request ceiling. ```bash curl --fail-with-body --silent --show-error \ "$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/files" \ -H "Authorization: Bearer $ISLE_API_KEY" \ -F 'file=@schematic.kicad_sch' -F 'path=/home/user/work/schematic.kicad_sch' curl --fail-with-body --silent --show-error --get \ "$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/files" \ -H "Authorization: Bearer $ISLE_API_KEY" \ --data-urlencode 'path=/home/user/work/board.kicad_pcb' -o board.kicad_pcb ``` ### Checkpoints and recovery Save all application editors before checkpointing. KiCad expects `/home/user/work/project.kicad_pro` and its saved schematic/PCB and local assets; FreeCAD expects `/home/user/work/project.FCStd`. See the SDK checkpoint section above for KiCad bundle format, exclusions, 256 MiB limits, and restore semantics. - `GET /sandboxes/{id}/checkpoints`: live array while running, with `id`, `timestamp` (Unix seconds), `artifact_hash`, `safe`. - Add `?source=r2` for persisted checkpoints; stopped environments automatically use storage. Entries use `checkpointId` (not `id`), `lifecycleGeneration`, `filename`, `size`, `safe`, `sha256`, `format`, `restorable`, plus storage metadata. Legacy KiCad settings-only checkpoints have `restorable=false`. - `POST /sandboxes/{id}/checkpoints`, JSON `{"action":"checkpoint"}` or `{}`: local checkpoint metadata plus `persisted` and `persistence_reason`. HTTP 200 with `persisted=false` does NOT confirm durable recovery. Reasons: invalid_checkpoint_metadata, checkpoint_not_safe, artifact_changed_after_checkpoint, storage_failed. A persisted checkpoint uses `persistence_reason=null`. - Same POST with `{"action":"recover"}` returns `{"status":"recovering"}`; `{"action":"restart"}` returns `{"status":"restarting"}` without restoration. - Same POST with `{"action":"download","checkpoint_id":1}` returns `{"url":"..."}` valid for one hour. Use a positive numeric checkpointId from the persisted list. ALL POST checkpoint actions currently require running, including download. Missing/unsaved project: 404 with `code="artifact_not_found"` and expected `path`. Invalid/changing project: 422 with `code="artifact_invalid"`. Save and retry. ### Events, health, recordings, and export - `GET /sandboxes/{id}/events`: retain=true required. Array with `id`, `type`, `data`, `created_at`, oldest first. `limit` defaults 200, capped at 1000; `after` is an exclusive ISO timestamp, not an event ID. - `GET /sandboxes/{id}/errors`: array with `id`, `sandbox_id`, `source`, `message`, `details`, `created_at`, newest first. `limit` defaults 100, capped at 500; `before` is an exclusive ISO timestamp. - `GET /sandboxes/{id}/monitor`: running only. Includes application status, checkpoint counts, recovery_count, last_error, last_error_at. HTTP 200 with `status="monitor_unavailable"` means health could not be read. - `GET /sandboxes/{id}/recording`: retain=true; `{"url":"...","truncated":false, "truncation_reason":null}`. Signed URL valid for one hour. 404 until available. Stop finalizes the recording. Caps: 512 MiB, six hours, or low disk space. - `POST /sandboxes/{id}/recording`: no body; retain=true and stopped required. Confirms existing retention with `key`, `already_persisted=true`, `truncated`, `truncation_reason`. Does not start recording; 404 if none was retained. - `GET /sandboxes/{id}/export`: JSON attachment with `schema_version:2`, `session`, and `steps` (`step`, `timestamp_ms`, `type`, plus `details` or `image_url`). Media links in this export are CDN URLs. retain=false returns empty steps and no recording URL. Export gathers all retained events up to export time. Events/errors have no next-page token. Exclusive timestamps can skip records sharing a timestamp across a page boundary; use /export for a full event trace. Follow signed download links directly without forwarding the Isle API key. Request fresh signed links after expiration; inspect recording truncation metadata. ### Permanent deletion and provider audit `DELETE /sandboxes/{id}/destroy` requires stopped or error. Usually returns 202 with `deletion_pending=true`, `provider_deletion_tracking_id`, `provider_operation_id`, `provider_operation_status`, and deletion state flags. Persist the tracking ID. It may be null when no provider resource ever existed. Replayed destroy requests can return prior state. HTTP acceptance and Isle-local cleanup are separate from completed provider erasure. - `GET /provider-deletions/{trackingId}`: owner-scoped audit with `tracking_id`, `sandbox_id`, `provider_operation_id`, `provider_operation_status`, `provider_http_status`, `provider_deletion_complete`, `provider_deletion_pending`, `provider_deletion_failed`, `requires_manual_verification`, `isle_data_deleted`, `isle_data_deleted_at`, `error`, `requested_at`, `updated_at`, `box_absent_at`, and `completed_at`. - `GET /provider-deletions?scope=all&limit=50`: returns `{"deletions":[...],"next_cursor":"...","scope":"all"}`. `limit` is 1–100, default 50; pass the opaque `next_cursor` as `cursor` until it is null. `scope=active` includes pending, failed, and manual-verification records; `recent` includes completed records updated in 30 days; `all` (default) combines active and recent, not unlimited historical records. Poll while provider_deletion_pending is true. Completion, failure, and manual verification are distinct terminal outcomes. Manual verification means the resource is absent without an erasure receipt. isle_data_deleted separately reports Isle-managed cleanup. Honor Retry-After on pending deletion conflicts. ### REST errors and retries Errors generally use `{"error":"message"}`; optional code/path/environment_id and operation flags provide context. Inspect status and operation state together: an accepted 202 operation can include diagnostic error text. Infrastructure errors can have non-JSON bodies; handle parse failures in your client. - 400: malformed/unknown fields, bad parameters, or wrong lifecycle state. - 401: missing/invalid/revoked key. - 403: account usage/concurrency limit, or denied file access. - 404: unavailable/unowned resource, absent recording/file/checkpoint, unsaved artifact. - 409: idempotency/lifecycle conflict, unsafe file boundary, pending data writes. - 410: deleted environment associated with a retained creation idempotency key. - 413: file or HTTP payload too large. - 422: non-regular file or invalid/changing checkpoint artifact. - 500/502/503: server/upstream/reconciliation failure; a mutation may have run. Retry creation with the same key and parameters. Stop can be repeated safely. Poll with bounded deadlines/backoff and honor Retry-After when present. Recheck state before retrying resume/recovery or other mutations. Never blindly retry input after `action_outcome="unknown", retry_safe=false` or a transport timeout; take a screenshot first so a delivered click/keypress/text is not duplicated.