The official Python SDK for MIOSA. Supports Python 3.9+ and provides both synchronous (Miosa) and asynchronous (AsyncMiosa) clients.
Install
What’s new in 1.1.0
| Area | What shipped |
|---|---|
| Sandbox lifecycle | create_sandbox with slug, external_user_id, external_project_id; sandbox.update(); sandbox.preview_token() |
| Tenant preview domain | client.tenant.preview_domain.get/set/verify/delete() |
| Tenant branding | client.tenant.branding.get/set/delete() |
| Webhooks | client.webhooks.create/list/get/delete/test() |
| Files advanced | sandbox.files.tree(), sandbox.files.write_files(), sandbox.files.watch_dir() |
| Sandbox env | sandbox.env.list/set/delete() |
| Processes | sandbox.processes.start/list/stop/logs() |
| Templates + fork | client.sandbox_templates.*, client.sandboxes.fork() |
| Usage / quotas | client.usage.get(), client.quotas.get/set/delete() |
| Audit log | client.audit_log.list() |
| Tenant events (SSE) | client.events.stream(tenant_wide=True) |
| Sharing | sandbox.share.create/list/revoke() |
First request
from miosa import Miosa
client = Miosa(api_key="msk_live_...")
# Create a sandbox, run a command, destroy it
sandbox = client.sandboxes.create(image="miosa-sandbox")
result = sandbox.run("python3 -c 'print(1 + 1)'")
print(result.stdout) # "2\n"
sandbox.destroy() Configure
| Option | Env var | Default | Description |
|---|---|---|---|
api_key | MIOSA_API_KEY | - | Workspace key (msk_live_*) |
base_url | MIOSA_BASE_URL | https://api.miosa.ai/api/v1 | API endpoint |
timeout | - | 30.0 | Per-request timeout in seconds |
max_retries | - | 3 | Automatic retries on 429 / 5xx |
from miosa import Miosa
# Reads MIOSA_API_KEY from environment
client = Miosa()
# Explicit configuration
client = Miosa(
api_key="msk_live_...",
base_url="https://api.miosa.ai/api/v1",
timeout=60.0,
max_retries=3,
) Both clients support the context manager protocol:
with Miosa() as client:
computers = client.computers.list() Authentication
The API key is sent as Authorization: Bearer <key> on every request. Keys must start with msk_.
from miosa import Miosa
import os
# From environment variable (recommended for production)
client = Miosa(api_key=os.environ["MIOSA_API_KEY"])
# Custom default headers (advanced - for proxy setups)
client = Miosa(
api_key="msk_live_...",
default_headers={"X-Tenant-ID": "acme-corp"},
) Async vs sync
Both clients expose an identical surface; AsyncMiosa methods are coroutines:
# Sync
from miosa import Miosa
client = Miosa()
sandbox = client.sandboxes.create(image="miosa-sandbox")
# Async
import asyncio
from miosa import AsyncMiosa
async def main():
async with AsyncMiosa(api_key="msk_live_...") as client:
sandbox = await client.sandboxes.create(image="miosa-sandbox")
result = await sandbox.run("echo hi")
print(result.stdout)
asyncio.run(main()) Error handling
from miosa import (
Miosa,
MiosaError,
AuthenticationError,
NotFoundError,
PermissionError,
RateLimitError,
ValidationError,
InsufficientCreditsError,
ServerError,
)
client = Miosa()
try:
computer = client.computers.get("nonexistent-id")
except NotFoundError:
print("Computer not found")
except AuthenticationError:
print("Invalid API key")
except PermissionError:
print("Access denied")
except RateLimitError as e:
print(f"Rate limited - retry after {e.retry_after}s")
except ValidationError as e:
print(f"Validation failed: {e.message}")
except InsufficientCreditsError:
print("Top up credits at miosa.ai/billing")
except ServerError as e:
print(f"Server error {e.status_code}")
except MiosaError as e:
print(f"API error {e.status_code}: {e.message}") Streaming events
sandbox.events.stream() opens an SSE connection and yields SandboxEvent objects. Use sandbox.events.astream() in async code:
# Sync
for event in sandbox.events.stream():
print(event.type, event.data)
if event.type in ("build.completed", "build.failed", "build.timed_out"):
break
# Async (AsyncMiosa - AsyncSandboxEvents.stream() is an async generator)
async for event in sandbox.events.stream():
print(event.get("type"), event.get("data"))
if event.get("type") in ("build.completed", "build.failed", "build.timed_out"):
break Phase 1-5 API Reference
Sandbox lifecycle (Phase 1)
client.sandboxes - sandbox handle returned by create()
# Create - full signature
sandbox = client.sandboxes.create(
image="miosa-sandbox", # template image
name="my-sandbox", # optional display name
slug="acme-backend", # optional URL-safe identifier
external_user_id="usr_123", # white-label user attribution
external_project_id="proj_456", # white-label project attribution
external_workspace_id="ws_789", # white-label workspace attribution
metadata={"env": "staging"}, # arbitrary key-value pairs
timeout_sec=300, # sandbox lifetime in seconds
always_on=False, # keep alive past idle timeout
vcpus=1,
memory_mb=512,
idempotency_key="uuid-v4", # safe for retries
)
sandbox = client.sandboxes.get(sandbox_id)
sandboxes = client.sandboxes.list(state="running")
client.sandboxes.destroy(sandbox_id)
# Update mutable fields on an existing sandbox
sandbox.update({
"name": "updated-name",
"slug": "new-slug",
"timeout_sec": 600,
"always_on": True,
"metadata": {"version": "2"},
})
# Mint a short-lived preview token
token_info = sandbox.preview_token(expires_in=3600)
# Returns: {"token": "mp_...", "url": "https://...", "expires_at": "..."}
# Fork from an existing snapshot or running sandbox
new_sandbox = client.sandboxes.fork(
sandbox_id,
snapshot_id="snap_abc", # optional
name="forked-sandbox",
) | Parameter | Type | Description |
|---|---|---|
name | str | Display name |
slug | str | URL-safe identifier |
external_user_id | str | White-label user attribution |
external_project_id | str | White-label project attribution |
external_workspace_id | str | White-label workspace attribution |
metadata | dict | Arbitrary key-value pairs |
timeout_sec | int | Lifetime in seconds |
always_on | bool | Stay alive past idle timeout |
idempotency_key | str | Deduplication key for retries |
Tenant preview domain (Phase 1)
client.tenant.preview_domain
# Get current custom preview domain
info = client.tenant.preview_domain.get()
# Returns: {"domain": "preview.acme.com", "verified_at": "...", "cname_target": "..."}
# Set custom preview domain
client.tenant.preview_domain.set("preview.acme.com")
# Trigger DNS verification
result = client.tenant.preview_domain.verify()
# Returns: {"verified": true, "target": "...", "records": [...]}
# Remove custom domain
client.tenant.preview_domain.delete() Tenant branding (Phase 1)
client.tenant.branding
# Get current branding
branding = client.tenant.branding.get()
# Set branding (all keys optional)
client.tenant.branding.set({
"product_name": "Acme AI",
"logo_url": "https://cdn.acme.com/logo.png",
"support_url": "https://acme.com/support",
"support_email": "help@acme.com",
"primary_color": "#ff6600",
"background_color": "#ffffff",
})
# Reset to platform defaults
client.tenant.branding.delete() | Key | Description |
|---|---|
product_name | White-label product name shown in UI |
logo_url | URL to your logo image |
support_url | Support page URL |
support_email | Support email address |
primary_color | Hex color for primary UI accents |
background_color | Hex color for desktop background |
Webhooks (Phase 1)
client.webhooks
# Create a webhook
wh = client.webhooks.create(
url="https://example.com/webhooks/miosa",
events=["sandbox.created", "sandbox.ready", "sandbox.destroyed"],
)
# List, get, delete
whs = client.webhooks.list()
wh = client.webhooks.get(wh_id)
client.webhooks.delete(wh_id)
# Send a test event to verify the endpoint is reachable
client.webhooks.test(wh_id) Supported events: sandbox.created, sandbox.ready, sandbox.destroyed, sandbox.error, computer.started, computer.stopped, computer.error
Files advanced (Phase 2)
sandbox.files
# Recursive directory tree
tree = sandbox.files.tree("/workspace", recursive=True)
# Batch file write
sandbox.files.write_files([
FileWriteItem(path="/workspace/app.py", data="print('hello')"),
FileWriteItem(path="/workspace/config.json", data=b'{"debug": true}'),
])
# Watch for file-system changes (SSE iterator)
for event in sandbox.files.watch_dir("/workspace"):
print(event.event, event.path) # "modified", "/workspace/app.py"
# event.event: "created" | "modified" | "deleted" | "renamed"
# event.old_path: set when event == "renamed" Sandbox environment variables (Phase 2)
sandbox.env
# List all env vars
vars = sandbox.env.list()
# Set a variable
sandbox.env.set("DATABASE_URL", "postgres://...")
# Delete a variable
sandbox.env.delete("DATABASE_URL") Processes (Phase 2)
sandbox.processes
# Start a long-running background process
proc = sandbox.processes.start("npm run dev", env={"NODE_ENV": "development"})
# Returns ProcessInfo with pid, status, started_at
# List all running processes
procs = sandbox.processes.list()
# Stop a process (SIGTERM then SIGKILL)
sandbox.processes.stop(proc.pid)
# Get tail of process output
logs = sandbox.processes.logs(proc.pid, tail=100) Usage and quotas (Phase 3)
client.usage - client.quotas
# Usage rollup - filter by user or group by dimension
report = client.usage.get(
external_user_id="usr_123",
group_by="external_user_id", # or "external_project_id", "workspace_id"
period="30d", # or pass start=/end= ISO timestamps
)
# Returns: {"period_start": "...", "period_end": "...", "results": [...]}
# Current period summary
summary = client.usage.current()
# Set per-user quotas
client.quotas.set(
"usr_123",
max_sandboxes=5,
max_concurrent=2,
max_storage_gb=10,
max_credit_cents=10000, # $100.00
)
# Get current quotas + live usage for a user
quota = client.quotas.get("usr_123")
# Revert to tenant default
client.quotas.delete("usr_123") Audit log (Phase 3)
client.audit_log
page = 1
while True:
result = client.audit_log.list(page=page, per_page=50)
for entry in result.items:
print(entry.action, entry.created_at)
if not result.has_next:
break
page += 1 Tenant-wide event stream (Phase 3)
# SSE stream of all events across the tenant (requires admin scope)
for event in client.events.stream(tenant_wide=True):
print(event.type, event.data) Sharing (Phase 4)
sandbox.share
# Create a public share URL (no API key required to access)
share = sandbox.share.create(expires_in=3600)
# Returns: {"share_id": "ms_...", "share_url": "https://...", "expires_at": "...", "scope": "read"}
# List active shares
shares = sandbox.share.list()
# Revoke a share
sandbox.share.revoke(share["share_id"]) Resource Reference
Full method signatures across all 19 resource groups (185 methods total). Every method on AsyncMiosa is a coroutine with the same signature - prefix calls with await.
Sandboxes
client.sandboxes - client.sandbox_templates
# Sandbox lifecycle
sandbox = client.sandboxes.create(
image="miosa-sandbox",
name="my-sandbox", # optional display name
vcpus=1,
memory_mb=512,
timeout_seconds=300,
idempotency_key="uuid-v4", # optional
)
sandbox = client.sandboxes.get(sandbox_id)
sandboxes = client.sandboxes.list(page=1, per_page=20)
sandboxes = client.sandboxes.list(status="running") # filter: status, name
sandboxes = client.sandboxes.list(name="my-dev")
client.sandboxes.destroy(sandbox_id)
# Execution
result = client.sandboxes.exec(sandbox_id, cmd=["bash", "-c", "echo hi"])
result = sandbox.run("npm install", opts={"cwd": "/app", "timeout_sec": 120})
result = sandbox.exec(["python3", "main.py"])
# Output fields: result.stdout, result.stderr, result.exit_code
# Files (on sandbox handle)
sandbox.files.write("/app/main.py", content="print('hello')")
sandbox.files.write("/app/data.bin", content=b" ")
content = sandbox.files.read("/app/main.py") # → str
data = sandbox.files.read("/app/data.bin") # → bytes
entries = sandbox.files.list("/app") # → list[FileEntry]
sandbox.files.delete("/app/old.txt")
sandbox.files.mkdir("/app/subdir")
sandbox.files.move("/app/old.txt", "/app/new.txt")
# Events (SSE)
for event in sandbox.events.stream():
print(event.type, event.data)
if event.type in ("build.completed", "build.failed", "build.timed_out"):
break
# Templates
templates = client.sandbox_templates.list()
template = client.sandbox_templates.get(template_id)
template = client.sandbox_templates.create(
name="my-template",
image="miosa-sandbox",
description="optional",
)
client.sandbox_templates.delete(template_id) Computers
client.computers
# CRUD
computer = client.computers.create(
name="runner-1",
template_type="miosa-desktop",
size="small", # "xs" | "small" | "medium" | "large" | "xl" - default: "small"
selected_apps=["vscode", "chrome"],
workspace_id="ws-uuid", # assign to workspace (optional)
external_workspace_id="org_abc", # your own workspace ref (optional)
external_project_id="proj_xyz", # your own project ref (optional)
idempotency_key="uuid-v4",
)
computer = client.computers.get(computer_id)
result = client.computers.list(workspace_id="ws-uuid") # also accepts: status="running", name="dev"
client.computers.update(computer_id, agent_session_id="session-uuid")
client.computers.delete(computer_id)
# Lifecycle
client.computers.start(computer_id)
client.computers.stop(computer_id)
client.computers.restart(computer_id)
clone = client.computers.clone(computer_id)
client.computers.resize(computer_id, size="medium")
client.computers.move(computer_id, region="us-east-ny")
# Access tokens
vnc = client.computers.vnc_credentials(computer_id)
tok = client.computers.stream_token(computer_id)
urls = client.computers.urls(computer_id)
apps = client.computers.list_apps(computer_id)
# Auto-stop
cfg = client.computers.get_auto_stop(computer_id)
client.computers.update_auto_stop(computer_id, auto_stop_seconds=3600)
# Metrics
metrics = client.computers.metrics(computer_id, window="1h")
# Env vars
env_vars = client.computers.env.list(computer_id)
client.computers.env.create(computer_id, name="DATABASE_URL", value="postgres://...")
client.computers.env.update(computer_id, name="DATABASE_URL", value="postgres://new")
client.computers.env.delete(computer_id, name="DATABASE_URL")
# Port exposure
ports = client.computers.ports.list(computer_id)
client.computers.ports.create(computer_id, port=3000, visibility="public")
client.computers.ports.update(computer_id, port=3000, visibility="private")
client.computers.ports.delete(computer_id, port=3000) Desktop
client.desktop - desktop control for a running computer. All coordinates are pixels (0,0 = top-left).
# Screenshots
png_bytes = client.desktop.screenshot(computer_id)
png_bytes = client.desktop.screenshot_region(computer_id, x=0, y=0, width=800, height=600)
# Mouse
client.desktop.click(computer_id, x=500, y=300, button="left") # button: "left"|"right"|"middle"
client.desktop.double_click(computer_id, x=500, y=300)
client.desktop.move(computer_id, x=200, y=400)
client.desktop.drag(computer_id, start_x=100, start_y=200, end_x=400, end_y=500)
client.desktop.scroll(computer_id, x=500, y=300, direction="down", amount=3)
# Keyboard
client.desktop.type(computer_id, text="Hello, World!")
client.desktop.key(computer_id, key="ctrl+c")
client.desktop.hotkey(computer_id, keys=["ctrl", "shift", "t"])
# Clipboard
text = client.desktop.get_clipboard(computer_id)
client.desktop.set_clipboard(computer_id, text="Hello, World!")
# Wallpaper (white-label branding)
client.desktop.set_wallpaper(computer_id, url="https://cdn.example.com/bg.png")
# Accessibility tree (AT-SPI)
tree = client.desktop.accessibility_tree(computer_id) Workspaces
client.workspaces
workspaces = client.workspaces.list()
workspace = client.workspaces.create(name="staging", description="Staging environment")
workspace = client.workspaces.get(workspace_id)
workspace = client.workspaces.update(workspace_id, name="staging-us-east")
client.workspaces.delete(workspace_id)
# Observability
stats = client.workspaces.stats(workspace_id)
usage = client.workspaces.usage(workspace_id, window="7d", granularity="day") Tenant / Settings / Regions
client.tenant - client.settings - client.regions
# Tenant plan + counters
plan = client.tenant.current()
# Settings
settings = client.settings.get()
client.settings.update(default_computer_size="medium", auto_stop_default=3600)
# Regions
regions = client.regions.list()
region = client.regions.get(region_slug) Dashboard / Analytics / Audit Log / Usage
client.dashboard - client.analytics - client.audit_log - client.usage
# Dashboard summary
summary = client.dashboard.get()
# Analytics
stats = client.analytics.computers(window="7d")
stats = client.analytics.usage(window="30d")
# Audit log (paginated)
page = 1
while True:
result = client.audit_log.list(page=page, per_page=50)
for entry in result.items:
print(entry.action, entry.created_at)
if not result.has_next:
break
page += 1
# Credit & compute usage
usage = client.usage.credits(window="30d")
usage = client.usage.compute(window="30d") Webhooks
client.webhooks
wh = client.webhooks.create(url="https://example.com/hook", events=["sandbox.created", "sandbox.ready"])
wh = client.webhooks.get(wh_id)
whs = client.webhooks.list()
client.webhooks.update(wh_id, events=["sandbox.created", "sandbox.destroyed"])
client.webhooks.delete(wh_id)
client.webhooks.test(wh_id) API Keys
client.api_keys
key = client.api_keys.create(name="ci-runner", scopes=["computers:read", "computers:write"])
key = client.api_keys.get(key_id)
keys = client.api_keys.list()
client.api_keys.revoke(key_id) Models / Completions / Embeddings
client.models - client.completions - client.embeddings
# Completions (hosted LLM)
resp = client.completions.create(
model="qwen3:32b",
messages=[{"role": "user", "content": "Hello"}],
max_tokens=512,
stream=False,
)
# Streaming completions
for chunk in client.completions.stream(model="qwen3:32b", messages=[...]):
print(chunk.delta, end="", flush=True)
# Embeddings
result = client.embeddings.create(model="nomic-embed-text", input="The quick brown fox")
vectors = result.embeddings # list[list[float]] White-label integrator flow
This is the marquee pattern for platforms building on MIOSA: create sandboxes on behalf of end users, scope by project, and expose previews without sharing API keys.
import os
from miosa import Miosa
client = Miosa(api_key=os.environ["MIOSA_API_KEY"])
# 1. Configure tenant branding once
client.tenant.branding.set({
"product_name": "Acme AI",
"logo_url": "https://cdn.acme.com/logo.png",
"primary_color": "#ff6600",
})
# 2. Optionally set a custom preview domain so previews appear on your domain
client.tenant.preview_domain.set("preview.acme.com")
client.tenant.preview_domain.verify()
# 3. Register a webhook to track lifecycle events
client.webhooks.create(
url="https://api.acme.com/webhooks/miosa",
events=["sandbox.created", "sandbox.ready", "sandbox.destroyed"],
)
# 4. Set per-user quotas before provisioning
client.quotas.set(
"usr_dr_smith",
max_sandboxes=3,
max_concurrent=1,
)
# 5. Create a sandbox attributed to a specific user + project
sandbox = client.sandboxes.create(
image="miosa-sandbox",
name="smile-dental",
external_user_id="usr_dr_smith",
external_project_id="proj_landing_page",
external_workspace_id="ws_smile_dental",
metadata={"plan": "pro"},
)
# 6. Write files and run a build
sandbox.files.write_files([
FileWriteItem(path="/workspace/index.html", data="<h1>Hello</h1>"),
])
result = sandbox.run("echo build done")
# 7. Mint a preview token for the end user (no API key needed on their end)
token = sandbox.preview_token(expires_in=3600)
preview_url = token["url"] # share this with the end user
# 8. Create a public share link
share = sandbox.share.create(expires_in=86400)
public_url = share["share_url"]
# 9. Query usage per project
report = client.usage.get(
external_user_id="usr_dr_smith",
period="30d",
)
# 10. Clean up
sandbox.destroy() Common patterns
Workspace-scoped creation
from miosa import Miosa
client = Miosa()
# Create a workspace for a customer
ws = client.workspaces.create(name="acme-staging")
# All computers here are isolated inside acme-staging
computer = client.computers.create(
name="dev-box",
template_type="miosa-desktop",
size="small",
workspace_id=ws.id,
external_workspace_id="org_acme",
external_project_id="proj_api",
) Idempotency key
import uuid
sandbox = client.sandboxes.create(
image="miosa-sandbox",
idempotency_key=str(uuid.uuid4()),
) Pagination
page = 1
while True:
result = client.audit_log.list(page=page, per_page=50)
for entry in result.items:
print(entry.action, entry.created_at)
if not result.has_next:
break
page += 1 Webhook verification
import hmac, hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)