miosa (Python)
Official Python SDK for MIOSA — the AI cloud platform for sandboxes, computers, deployments, and managed data.
Install
pip install miosa
# or
uv add miosa
# or
poetry add miosa
Requires Python 3.9+. Dependencies: httpx, pydantic v2.
Quickstart
from miosa import Miosa
miosa = Miosa(api_key="msk_live_...")
# Create or resume a persistent sandbox workspace, then work inside it.
sb = miosa.sandboxes.create_agent_workspace(
"my-build",
external_workspace_id="customer-workspace-123",
)
result = sb.exec.run("python --version")
print(result.stdout) # Python 3.12.x
sb.files.write("/workspace/hello.py", "print('hello from miosa')")
out = sb.exec.run("python /workspace/hello.py")
print(out.stdout) # hello from miosa
print(sb.preview.create(port=8000)["url"])
# https://8000-<slug>.sandbox.miosa.ai/
sb.snapshots.create("after-hello-world")
sb.pause()
What's included
| Resource | Description |
|---|---|
miosa.computers |
Firecracker microVMs — full lifecycle, exec, files, services |
miosa.sandboxes |
Lightweight code-execution VMs via the /sandboxes route, with resource read and live resize |
miosa.runtimes |
Sandbox runtime backends (firecracker, soma, auto) and their live gate state |
miosa.deployments |
Versioned production releases with rollback |
miosa.databases |
Managed Postgres / Redis lifecycle |
miosa.storage |
S3-compatible object storage |
miosa.volumes |
Persistent block storage |
miosa.api_keys |
Programmatic API key management |
miosa.usage |
Usage reports per workspace, per external tenant |
miosa.settings |
Workspace config, branding, BYOK provider keys |
miosa.webhooks |
Outgoing tenant webhooks — CRUD, test, delivery history |
miosa.open_computers |
BYOC host management — register your own machines |
miosa.devices |
Unified device facade for sandbox/computer list, exec, files, previews |
miosa.connectors |
Provider credentials and short-lived runtime tokens |
miosa.runtime_env |
Inherited tenant/workspace/project runtime environment variables |
miosa.environments |
Machine templates — repositories, variables, tools and credentials a new sandbox or computer inherits |
miosa.deployment_environments |
Deployment-environment inventory (/deployment-environments) |
miosa.snapshot_history |
Account-wide snapshot history, file browser and fork |
miosa.named_snapshots |
Named (pinned) snapshots and their deploy/fork |
miosa.commands |
The synchronous commands endpoint on sandboxes and computers |
miosa.agent_steering |
Steer the agent attached to a machine (prompts / stop) |
miosa.bill_to |
Which organization a create bills |
miosa.member_caps |
Per-member usage and concurrency caps |
miosa.agent_settings |
Agents tab — harnesses, models, credentials, settings |
miosa.agent_signins |
Sign in a workspace's own Claude / ChatGPT / Kimi / Mistral account |
miosa.agent_accounts |
Connected agent accounts |
miosa.agent_triggers |
Agent triggers — schedule, webhook, event, forge, email |
miosa.agent_chats |
A chat's harness context use and compaction |
miosa.connections |
One read-model over models, apps and tool servers |
miosa.ai_gateway |
AI Gateway policies, budget, limits, settings and request log |
miosa.account |
whoami — the credential's user, org, workspace and scopes |
miosa.workflows |
Visual workflows — CRUD, publish, run, versions, runs and steps |
miosa.service_accounts |
Service accounts and their API keys |
miosa.sse_ticket |
Short-lived SSE tickets |
miosa.event_subscriptions |
Platform event subscriptions and delivery replay |
miosa.builder_apps |
Builder apps — data, automations, approvals, publish |
miosa.ws |
WebSocket surfaces — sandbox terminal, TCP tunnels, SSH tunnel |
miosa.ssh |
SSH kit — ssh-info, certificates, config snippet, ready command |
miosa.computer_use |
Claude computer-use toolset adapter and the agent loop |
miosa.helpers |
Eleven Boat-style helpers: waits, streams, read/write/exec, stop and remove |
miosa.sandbox_batches |
Batch sandbox create, items, cancel and the batch event stream |
miosa.operations |
Long-running operations — get, events, cancel |
miosa.notifications |
Notifications — list, unread count, mark read, stream |
miosa.alerts |
Alerts — list and resolve |
miosa.activity |
Tenant activity feed |
Computers and Desktop Access
computer is the user-facing product. miosa-desktop is the desktop computer
template/profile. The canonical fast desktop profile is:
computer = miosa.computers.create(
name="agent-desktop",
template_type="miosa-desktop",
size="small", # 2 vCPU / 4 GB
)
Authenticated platform desktop links do not need a viewer password. Raw
external viewer links such as *.computer.miosa.ai/desktop/index.html are
password-gated; rotate the password when you need to share that raw link:
status = computer.viewer_password()
rotated = computer.rotate_viewer_password()
print(rotated["viewer_password"])
Connect vs Egress
Use Connect when your product needs to manage provider credentials for agents, sandboxes, computers, or deployments. Connect owns the product-level contract: connectors, installations, project links, inherited defaults, short-lived tokens, white-label attribution, and runtime bindings.
Use Egress when you need the low-level security boundary: encrypted secret
storage, outbound host allowlists, placeholder token exchange, and audit logs.
Egress is the enforcement layer under Connect. Most white-label apps should
call miosa.connectors and runtime binding helpers first; only call
miosa.secrets, miosa.network, or miosa.audit when you are building
security/admin controls directly.
import os
connector = miosa.connectors.create(
"anthropic",
name="workspace-claude",
scope="workspace",
external_workspace_id="clinic-iq",
value=os.environ["ANTHROPIC_API_KEY"],
)
# Runtime-level materialization: the sandbox sees ANTHROPIC_API_KEY, but your
# app does not get the raw provider key back from MIOSA.
miosa.connectors.materialize_defaults(
workspace_id="workspace-id",
resource_type="sandbox",
resource_id=sb.id,
target="agent",
external_workspace_id="clinic-iq",
)
Agent workspaces
For AI builders, code assistants, and white-label agent products, the sandbox
is the development machine. Do not run builds on the user's laptop and upload
the result as the primary flow. Create or resume a stable sandbox workspace,
write files under /workspace, run package installs/tests/builds inside the
sandbox, expose previews from the sandbox, and publish from that sandbox.
sb = miosa.sandboxes.get_or_create(
"acme-marketing-page",
template_id="miosa-sandbox",
persistent=True,
timeout_sec=86_400,
idle_timeout_sec=1800,
snapshot_expiration_days=30,
keep_last_snapshots=1,
external_workspace_id="acme",
external_user_id="jane",
wait_until_ready=True,
)
sb.files.write_files([
{"path": "/workspace/package.json", "data": '{"scripts":{"dev":"vite --host 0.0.0.0"}}'},
{"path": "/workspace/index.html", "data": '<main id="app"></main>'},
])
for event in sb.exec_stream("cd /workspace && npm install && npm run dev -- --port 3000"):
print(event)
preview = sb.preview.create(port=3000)
print(preview["url"])
For platform agents that plan work outside the sandbox and then hand execution to a runtime inside the sandbox/computer, use the unified device API:
miosa.agent_runtime_profiles.create(
workspace_id="clinic-workspace",
name="ClinicIQ Claude Code",
runtime="claude-code",
is_default=True,
connectors=[
"anthropic/cliniciq",
{
"uid": "refero",
"type": "mcp",
"managed": True,
"server_url": "https://api.refero.design/mcp",
},
],
env={"ANTHROPIC_API_KEY": "miosa-managed:anthropic/cliniciq"},
metadata={"model": "claude-opus-4.8", "white_label_client": "cliniciq"},
)
miosa.devices.write_file(
sb.id,
"/workspace/PLAN.md",
content="# Build plan\nUse the customer profile and export /workspace/out.",
)
build = miosa.devices.exec(
sb.id,
"npm install && npm run build",
cwd="/workspace",
timeout_ms=600_000,
)
artifact = miosa.devices.read_file(sb.id, "/workspace/out/report.pdf")
create_agent_workspace() is a convenience wrapper around get_or_create()
with builder defaults: a 24-hour activity cap, thirty-minute idle
snapshot/pause, readiness waiting, 30-day snapshot-retention metadata,
keep-last-snapshot metadata, and metadata that marks the sandbox as an agent
workspace.
That mirrors the Vercel-style persistent-sandbox model without keeping CPU
running forever: activity extends the running session, idle workspaces are
checkpointed and paused, and later sessions resume from the checkpoint. Use
extend(86_400) before long builds, pause() when the user is done for now,
resume() for the next session, snapshots.create() for manual checkpoints,
and destroy() only when the workspace should be permanently deleted.
Browser automation with CDP
A sandbox browser speaks the Chrome DevTools Protocol. Mint a scoped, expiring,
revocable grant with sandboxes.browser_cdp_url(id) (the async client mirrors
it as await client.sandboxes.browser_cdp_url(id)). Pass ttl_seconds
(60-604800) to request a lifetime:
grant = miosa.sandboxes.browser_cdp_url(sb.id, ttl_seconds=3600)
print(grant.cdp_host_http_url) # https://cdp-<id8>-<token>.sandbox.miosa.ai
SandboxBrowserCdp carries every endpoint a CDP client needs:
| Field | What it is |
|---|---|
cdp_url |
wss:// CDP endpoint for header/session auth |
cdp_ws_url |
wss:// CDP endpoint carrying the grant token in the path |
cdp_http_url |
https:// CDP endpoint carrying the grant token in the path |
cdp_host |
Header-auth CDP host (cdp-<id8>-<token>.sandbox.miosa.ai) |
cdp_host_http_url |
https:// host form; answers /json, /json/version, /json/list, /json/new with webSocketDebuggerUrl rewritten to the same host's /devtools/* sockets |
cdp_host_ws_url |
wss:// host form; serves the /devtools/<browser|page>/<id> sockets |
cdp_token_id / cdp_expires_at / epoch |
Grant id, ISO-8601 expiry and generation |
cdp_debugger_address |
Optional host:port for Selenium's debuggerAddress. This endpoint does not return it - it appears on the browser-start payload - so it may be absent |
The SDK only mints the grant - it never starts or proxies a browser. Run a
CDP-speaking browser inside the sandbox, then choose whichever endpoint your
client understands. The path-token URLs (cdp_ws_url, cdp_http_url) are for
clients that cannot set headers; cdp_url is the header-auth wss:// form.
Point Selenium at the debugger address. Use the cdp_debugger_address from the
browser-start payload when present, or 127.0.0.1:9222 when you forward the
port with the MIOSA CLI:
# Selenium - 127.0.0.1:9222 after `miosa sandboxes forward <id> 9222`, or the
# cdp_debugger_address supplied by the browser-start payload.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver = webdriver.Chrome(options=options)
driver.get("https://example.com")
Point chromedp (Go) at the header-auth host as its base URL:
// chromedp
allocCtx, cancel := chromedp.NewRemoteAllocator(ctx, grant.cdp_host_http_url)
defer cancel()
browserCtx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
Agent SDK
The Python SDK includes a small agent loop plus provider adapters and a MIOSA tool catalogue. This is the quickest path when you want an LLM to operate MIOSA computers directly.
from miosa.agent import Agent, groq_provider
agent = Agent(
provider=groq_provider(
api_key="gsk_...",
model="moonshotai/kimi-k2-instruct-0905",
),
miosa_api_key="msk_live_...",
)
result = agent.run(
"Create a sandbox workspace, write /workspace/hello.py, run it, show me "
"the output, snapshot it, then pause the sandbox.",
max_iterations=10,
)
print(result.final_text)
Built-in agent tools expose the computer primitive in plain terms:
| Tool | Does |
|---|---|
create_sandbox |
Create/resume a persistent code sandbox workspace for shell/Python/Node/files. |
create_computer |
Boot a general MIOSA computer for GUI, services, previews, files. |
list_computers |
List active computers, sandboxes, and desktops. |
get_computer |
Fetch status, template, size, and public URL. |
exec / exec_python |
Run bash or Python inside the computer. |
read_file / write_file / list_files |
Work with files inside the computer. |
preview_url |
Get the public HTTPS URL for a service port. |
pause_sandbox / resume_sandbox |
Stop/resume a persistent workspace without deleting files. |
extend_sandbox |
Extend the activity timeout before long installs, builds, or agent tasks. |
snapshot_sandbox |
Create a checkpoint after dependency install or a good edit. |
deploy_sandbox |
Publish to normal MIOSA Deploy. |
deploy_docker |
Publish to the workspace App Engine appliance. |
destroy_computer |
Release compute resources when done. |
Provider factories include openai_provider, groq_provider,
deepseek_provider, openrouter_provider, together_provider,
fireworks_provider, mistral_provider, cerebras_provider,
perplexity_provider, xai_provider, ollama_provider, lm_studio_provider,
and openai_compatible_provider for custom routers.
Sandbox-first builder examples live in examples/:
See BUILDER_GUIDE.md for the full “build your own Lovable with MIOSA sandboxes” product pattern.
| Example | What it builds |
|---|---|
agent_sandbox_website_builder.py |
A Lovable-style website builder flow. |
agent_sandbox_app_builder.py |
A small app builder with a dev server and preview. |
agent_sandbox_artifact_builder.py |
Markdown/doc artifacts in /workspace/artifacts. |
agent_sandbox_slide_deck_builder.py |
A deck workspace under /workspace/deck. |
Set GROQ_API_KEY and MIOSA_API_KEY, then run one of the examples. They keep
the sandbox alive with miosa_tool_options={"allow_destroy": False} so your app
can show previews, inspect generated files, or publish later.
The agent should treat a MIOSA sandbox like the actual development machine:
write source files under /workspace, run package managers/tests/builds inside
the sandbox, start dev servers inside the sandbox, stream logs/tool results back
to your UI, and only publish after a sandbox preview passes a smoke check.
File operations
# Write / read
computer.files.write_file("/workspace/config.json", '{"key": "value"}')
text = computer.files.read_file("/workspace/config.json")
# Upload / download
computer.files.upload("/local/report.pdf", "/workspace/report.pdf")
raw_bytes = computer.files.download("/workspace/report.pdf")
# List / stat / mkdir / rename / chmod
entries = computer.files.list("/workspace")
stat = computer.files.stat("/workspace/config.json")
computer.files.mkdir("/workspace/output", recursive=True)
computer.files.rename("/workspace/old.txt", "/workspace/new.txt")
computer.files.chmod("/workspace/run.sh", 0o755)
Services (background processes)
svc = computer.services.create(
name="api",
command="uvicorn app:main --port 8080",
working_dir="/workspace",
env={"DEBUG": "1"},
port=8080,
)
for log in computer.services.logs(svc.id, follow=True):
print(log.stream, log.line)
computer.services.restart(svc.id)
computer.services.delete(svc.id)
Publish from sandbox
Sandbox previews are mutable builder URLs. Publishing creates a durable
deployment version with a stable url / public_url.
sb = miosa.sandboxes.create(name="clinic-intake", template_id="miosa-sandbox")
sb.files.write_file("/workspace/index.html", "<h1>Clinic intake</h1>")
result = sb.deploy(
name="clinic-intake",
output_path="/workspace",
entrypoint="index.html",
)
print(result["url"])
For full-stack/dynamic apps:
result = sb.deploy(
name="clinic-intake-api",
output_path="/workspace",
build_command="npm run build",
run_command="npm start",
port=3000,
health_check_path="/",
resources={
"database": {"auto": True, "engine": "postgresql", "size": "xs"},
"storage": {"auto": True},
},
domain="intake.apps.cliniciq.com",
)
If result["state"] == "building" for a dynamic app, poll
miosa.deployments.get(result["deployment_id"]) until it is running. Always
display the server-returned url / public_url; do not hardcode
preview.miosa.app, api.miosa.app, or <tenant>.miosa.app.
To publish the exact snapshot that passed QA instead of whatever the editable
sandbox holds right now, use deploy_snapshot. It forks the snapshot into a
temporary release sandbox, deploys that fork, and destroys it again, so the
source sandbox is never mutated:
snapshot = sb.snapshots.create("qa-approved")
result = sb.deploy_snapshot(
snapshot["id"],
name="clinic-intake",
output_path="/workspace",
entrypoint="index.html",
)
print(result["source_snapshot_id"], result["release_sandbox_id"])
The result always carries source_snapshot_id and release_sandbox_id for
provenance. Pass cleanup=False to keep the release sandbox for inspection.
If the release sandbox could not be destroyed, the deployment still succeeds and
result["release_cleanup_error"] explains why; when the deploy itself fails and
the release sandbox survives, the raised error carries the same detail so the
leftover sandbox can be cleaned up by id.
For workspace App Engine, publish from the same sandbox but choose the App Engine target:
result = sb.deploy_docker(
name="lead-magnet",
path="/workspace",
build_command="npm run build",
run_command="npm start",
port=3000,
)
print(result.get("url") or result.get("public_url"))
App Engine runs app containers inside the workspace's dedicated MIOSA App Engine appliance VM. It is separate from normal MIOSA dynamic runtime VMs and is useful when a workspace needs many small apps, funnels, lead magnets, APIs, or client sites.
Desktop control
screenshot = computer.screenshot() # PNG bytes
computer.click(640, 400)
computer.double_click(640, 400)
computer.type("hello world")
computer.key("Return")
computer.scroll("down", 3)
computer.drag(100, 100, 400, 400)
cursor = computer.cursor()
windows = computer.windows()
computer.launch("firefox")
Async usage
import asyncio
from miosa import AsyncMiosa
async def main():
async with AsyncMiosa(api_key="msk_live_...") as miosa:
computer = await miosa.computers.create(
name="async-build", template_type="miosa-sandbox"
)
await computer.files.write_file("/workspace/app.py", "print('async!')")
result = await computer.exec.run("python /workspace/app.py")
print(result.stdout)
await computer.destroy()
asyncio.run(main())
White-label / multi-tenant
Tag resources with external_workspace_id and external_user_id to attribute usage to your downstream customers:
computer = miosa.computers.create(
name="customer-build",
template_type="miosa-sandbox",
metadata={
"external_workspace_id": "dental-office-123",
"external_user_id": "dr-smith-456",
},
)
White-label URL layers are separate:
sandbox preview: https://<port>-<slug>.sandbox.<preview-domain>
durable deployment: https://<slug>.<deployment-domain>
custom domain: https://app.customer.com
Tenant preview-domain management is available through miosa.tenant. Tenant
deployment-domain routing exists server-side; clients should consume deployment
public_url instead of reconstructing it.
Runs
Use an idempotency key when a caller may retry run creation. The same key prevents a retry from creating duplicate work.
run = miosa.runs.run(
sandbox_id="sbx_abc123",
instruction="Build the ClinicIQ report",
idempotency_key="clinic-iq-request-123",
)
events = miosa.runs.events(run["id"], limit=200)
for event in events:
print(event["type"], event.get("metadata", {}))
# Continue from the last durable event without replaying earlier events.
new_events = miosa.runs.events(run["id"], after_id=events[-1]["id"])
The async client exposes the same contract through await client.runs.run(...) and await client.runs.events(...).
Environments
An environment is the template a new sandbox or computer inherits: which repositories are cloned, which variables and secret files are injected, and which of your credentials the machine may use. The same environments apply to sandboxes and computers.
# Create an environment and give it variables, a repo and a tool.
env = miosa.environments.create(
"prod",
variables={"STRIPE_KEY": "sk_live_..."},
repositories=[{"repo": "octocat/hello-world", "setup_script": "npm ci"}],
cli_tools={"gh": "latest"},
)
# Start a machine in it, with a one-off setup script and a create-time wait.
sb = miosa.sandboxes.create(
"miosa-sandbox",
environment="prod",
setup_file="#!/bin/sh\nnpm ci\n",
wait=45, # wait up to 45s for ready; ?wait= on the create request itself
)
# One atomic save -> exactly one new version.
miosa.environments.update(
"prod",
variables_set={"REGION": "us-east-1"},
variables_unset=["OLD"],
pass_sandbox_credentials=True,
)
# Move pinned machines to the latest version.
miosa.environments.upgrade("prod")
# What a machine would receive, lowest layer first.
miosa.environments.effective("prod")
miosa.environments.billing() reports the account new machines bill, and
miosa.deployment_environments.list() is the deployment-environment inventory
(previously the deprecated data key on GET /environments).
Resources and live resize
sandbox.resources() reads the sandbox's vCPU and memory and what the host can
change on the running sandbox right now, without changing anything.
sandbox.resize(cpu_count=..., memory_mb=...) changes them, sending only the
fields you pass. Both are also available on client.sandboxes by id, sync and
async.
read = sandbox.resources()
print(read["resources"], read["capabilities"]["live_cpu"])
out = sandbox.resize(cpu_count=4)
if out.get("restarted"):
... # 202: the sandbox was recreated; follow the new id
next_sbx = client.sandboxes.get(out["new_sandbox"])
elif out["applied_live"]:
... # 200: applied on the running sandbox
else:
... # 200: recorded; a restart applies it
resize needs at least one of cpu_count or memory_mb (otherwise it raises
ValueError). Only vCPU changes live today, only on a running Firecracker
sandbox, up to the count it booted with, and only on hosts with the cpu
controller enabled. Memory never applies live and answers requires_restart;
SOMA answers RESIZE_UNSUPPORTED. A refusal raises a MiosaError whose .code
is RESIZE_REQUIRES_RESTART, RESIZE_UNSUPPORTED, PLAN_CAP_EXCEEDED, ... and
whose .body carries the server's fields.
Runtime selection
Every sandbox create runs on a runtime backend. firecracker is the full
microVM and the default for every size and template; soma is the fastest
start for supported Linux sandbox images; auto lets the platform pick the
best runtime for the request. Pass runtime= to miosa.sandboxes.create and
the SDK sends it as the runtime_profile create field (firecracker maps to
standard on the wire). Omit it to let the server default apply.
# Read the live catalog: which runtimes exist, and whether the gate is open.
for rt in miosa.runtimes.list():
print(rt["id"], rt["available"], rt["unavailable_reason"])
# firecracker True None
# soma False GLOBAL_GATE_CLOSED
# auto True None
# Ask for a runtime by name. "firecracker" -> runtime_profile "standard".
sb = miosa.sandboxes.create("miosa-sandbox", runtime="firecracker")
# Let the platform choose.
sb = miosa.sandboxes.create("miosa-sandbox", runtime="auto")
miosa.runtimes.list() returns each runtime's capabilities (browser,
network egress, persistence, pause, gpu, and the max_shape it accepts) along
with available and unavailable_reason - the real gate state, so a caller
can fall back before asking for a runtime that is closed. The async client
exposes the same await miosa.runtimes.list().
Snapshots
# Account-wide history, newest first, searchable.
history = miosa.snapshot_history.list(q="builder", limit=50)
# Browse a snapshot's disk image without the machine.
tree = miosa.snapshot_history.tree("f3c1", path="/home/user")
blob = miosa.snapshot_history.file("f3c1", path="/home/user/notes.txt")
# Fork a snapshot into a new, independent machine.
fork = miosa.snapshot_history.fork("f3c1", idempotency_key="k1")
# Pin a snapshot under a name that outlives the machine.
miosa.named_snapshots.create("web-stack", sandbox_id="9a2e")
miosa.named_snapshots.deploy("web-stack") # fork it into a new machine
Commands and agent steering
# The commands endpoint: synchronous, with cwd, stdin and a duration.
result = miosa.commands.run("sbx_abc123", "bash ./setup.sh", cwd="my-repo")
print(result["stdout"], result["exit_code"], result["duration_ms"])
# Steer the agent run attached to a machine.
miosa.agent_steering.prompt("sbx_abc123", "fix the failing test")
miosa.agent_steering.stop("sbx_abc123")
A machine that is not ready refuses a command with a retryable 409 →
MachineStartingError; poll it using e.retry_after_ms.
Agents, connections and bill-to
# The Agents tab: harnesses, model catalog, shared credentials.
harnesses = miosa.agent_settings.harnesses()
miosa.agent_settings.put_credential("openrouter", api_key="sk-or-...")
miosa.agent_settings.update_harness("claude-code", default_model="claude-sonnet-5-5")
# One read over model providers, apps and tool servers.
miosa.connections.list(family="models", usable_by="agents")
# Connect agent accounts for your workspace, or for one of your end users.
# Pass external_user_id (your id for them) and the connection is theirs alone.
# No call returns a secret: a status, a redacted preview and the account email and plan.
miosa.connections.set_key("anthropic", key=os.environ["ANTHROPIC_API_KEY"], workspace_id=ws)
signin = miosa.connections.connect_subscription("claude", external_user_id="cus_123")
print(signin.verification_url) # open in the user's browser
status = signin.submit_code(pasted_code) # Claude: the page shows a code
# ChatGPT and Kimi are device codes: show signin.user_code, then signin.wait()
signin = miosa.connections.resume_subscription(signin_id, "claude", external_user_id="cus_123")
miosa.connections.get("claude", external_user_id="cus_123") # connected? scope? inherited?
miosa.connections.test("anthropic", external_user_id="cus_123") # does the key work?
miosa.connections.delete("claude", external_user_id="cus_123") # exactly this scope
miosa.connections.tool_keys.set("APIFY_TOKEN", value, external_user_id="cus_123")
miosa.harnesses.list(external_user_id="cus_123") # which harnesses can this customer run, and with what
# Who pays for a create.
miosa.bill_to.update("acme")
limits = miosa.bill_to.limits()
# Triggers start runs on a schedule, a webhook, an event, Forge or email.
miosa.agent_triggers.create(
"agent_1",
name="Every Monday",
kind="schedule",
instruction="Summarize last week's tickets.",
config={"cron": "0 9 * * 1", "timezone": "America/Chicago"},
)
# A chat's harness context and compaction.
miosa.agent_chats.context("chat_1")
miosa.agent_chats.compact("chat_1", focus="keep the API notes")
Build a multi-tenant platform on MIOSA
Running a platform on MIOSA (the way ClinicIQ does) means every request belongs
to one of your customers. The pieces below are the whole story; a runnable
end-to-end version is examples/multi_tenant_platform.py.
One key, many organizations
A platform API key is issued for one organization. To act for another, send
X-MIOSA-Tenant — set it once on the client, or per tenant with with_tenant:
platform = Miosa(unbranded=True) # no SDK branding in the headers
acme = platform.with_tenant("tenant_acme") # every call now carries the header
Unbranded matters when your customers see your logs, not ours: unbranded=True
blanks the User-Agent, and user_agent="acme-platform/1.0" sets your own.
Identity: stable references, readable names
Never mint a workspace slug per request. POST /workspaces resolves on
external_workspace_id before it inserts, so re-provisioning is idempotent:
ws = platform.workspaces.create(name="@alex", external_workspace_id="cust_1041")
if ws.reused:
print("existing workspace", ws.id) # 200 reuse, nothing inserted
A live workspace that already carries the requested name raises
WorkspaceNameExistsError (409), carrying the existing workspace. Send
allow_duplicate_name=True when a second one is intended. Rename with
workspaces.update(ws.id, name="@alex-b"); the id and slug never change.
Sandbox and computer creates take the same attribution
(external_workspace_id, external_user_id, external_project_id), and
metadata and tags round-trip, so you can list your own customers' machines:
platform.sandboxes.list(external_user_id="cust_1041")
client.sandboxes.list(tags=["user:cust_1041"])
Scoped end-user tokens
Hand your customers a token that does one thing for one user, for an hour:
token = platform.tokens.create_scoped(
user_id="cust_1041",
workspace_id=ws.id,
expires_in_seconds=3600,
scopes=["sandboxes:read", "sandboxes:write", "sandboxes:exec"],
)
platform.tokens.list_scoped() # audit what is outstanding
platform.tokens.delete_scoped(token["id"])
Quotas bound what that user can spend:
platform.quotas.set("cust_1041", max_sandboxes=10, max_concurrent=2,
max_storage_gb=50, max_credit_cents=5000)
platform.quotas.get("cust_1041") # includes the effective policy
platform.quotas.delete("cust_1041")
Isolation: environments with nothing of yours in them
safe_for_third_parties (or no_env=True on a create) guarantees a machine
receives none of the owner's credentials, whatever the toggles say. Egress is
capped with the network policy and the allowlist:
env = platform.environments.create(
"enduser-alex", workspace_id=ws.id, safe_for_third_parties=True
)
sandbox = platform.sandboxes.create("miosa-sandbox", environment=env["id"],
workspace_id=ws.id, no_env=True)
sandbox.network.allow(host="api.stripe.com")
sandbox.network.lockdown(policy_id="pol_1") # audit_only -> enforce
Previews, shares and the desktop
preview = sandbox.previews.create(port=3000, label="app") # https://3000-<slug>...
share = sandbox.previews.share(preview["id"], ttl_seconds=3600) # no key needed
platform.tenant.preview_domain.set("preview.acme.com") # your own domain
platform.tenant.preview_domain.verify() # returns the DNS records
platform.custom_domains.create(domain="app.acme.com", deployment_id="dep_1")
computer.embed() # iframe the desktop
computer.stream_token() # short-lived desktop stream
Who pays, and what it costs
A create bills the organization it runs in. Choose it per request without touching anything:
platform.sandboxes.create(..., bill_to="acme") # or the X-Miosa-Bill-To header
platform.bill_to.update("acme") # account-wide default
platform.bill_to.limits() # before you start: credits, caps, reasons
platform.member_caps.update("tenant_acme", user_id, usage_cap_cents=500)
platform.usage.get(group_by="workspace_id", period="30d") # usage for billing
platform.usage.get(external_user_id="cust_1041") # one customer's usage
Computer use
Drive a desktop from an agent loop, or hand the desktop to Claude as a tool.
from miosa import Miosa, MiosaComputerToolset, computer_use_loop
with Miosa() as miosa, miosa.computers.create(name="cu", template_type="miosa-desktop") as computer:
computer.wait_until_ready(timeout=60)
computer.wait_for_desktop()
# One dispatcher over the 17 desktop actions, with a typed result.
computer.act("left_click", {"coordinate": [512, 384]})
computer.act("type", {"text": "hello"})
state = computer.look(include_tree=True, include_image=True)
if (node := state.find("Sign in")):
computer.act("left_click", {"coordinate": [node.x, node.y]})
# Give the same computer to Claude as a toolset.
toolset = MiosaComputerToolset(computer, confirm=lambda call: True)
print(toolset.tools) # [{"type": "computer_toolset_20260801"}]
result = toolset.execute_batch(response.content)
# In a batch, the first failure marks that result is_error and every later one
# returns "Not executed: an earlier computer action in this turn failed."
# Or run the loop in-process against your own model client.
result = computer_use_loop(
computer, toolset=toolset, client=anthropic_client, model="claude-sonnet-5",
instruction="Log in and screenshot the dashboard.",
max_iterations=20, on_step=lambda step: print(step.action, step.ok),
)
print(result.final_text, len(result.steps))
confirm is mandatory when type, key or hold_key are enabled, matching
Anthropic's constructor rule; disable a member through configs to drop it.
MiosaComputerToolsetLegacy covers computer_20251124 / 20250124 /
20241022 for older models.
Shedding load with fail_fast
from miosa import NoReadyMachineError
try:
sbx = miosa.sandboxes.create(size="large", fail_fast=True)
except NoReadyMachineError:
sbx = miosa.sandboxes.create(size="small") # fall back, no queueing
fail_fast answers now instead of queueing, and does not consume a start
limit. It is not retried, and the error is always non-retryable.
The helper module
miosa.helpers is one import for the loops you would otherwise write twice:
wait_until_ready, wait_until_idle, wait_for_desktop, wait_for_prompt,
stream_events, stream_prompt, stream_command, stop_and_remove,
read_text, write_text, exec_command (each with an _async twin where it
applies).
from miosa import Miosa
from miosa.helpers import exec_command, stop_and_remove, wait_until_ready
wait_until_ready(miosa, computer.id, timeout_ms=60_000)
print(exec_command(miosa, computer.id, "uname -a").stdout)
stop_and_remove(miosa, computer.id)
SSH your customers can actually use
A sandbox's sshd is reached through the authenticated WebSocket tunnel, so the
SDK assembles the whole path. client.ssh.ready() reads ssh-info, signs a
short-lived certificate for your key and returns a runnable command:
details = client.ssh.ready(
sandbox.id, public_key, ttl_seconds=900, identity_file="~/.ssh/id_ed25519"
)
print(details["command"])
# ssh -o ProxyCommand=python3 -m miosa.ssh_tunnel %h -i ~/.ssh/id_ed25519 root@<sandbox id>
print(details["config"]) # a ~/.ssh/config block
print(details["known_hosts"]) # the known_hosts entry
The tunnel URL is host-derived, so a white-label platform sees its own host in
every snippet. For a non-expiring grant use client.ssh.install_key(...).
The ProxyCommand is real: export MIOSA_API_KEY (and MIOSA_BASE_URL on your
own domain) and ssh works as-is. python -m miosa.ssh_tunnel <sandbox id> is
the bridge. From Python, open_ssh_tunnel_socket(sandbox.id) returns a
blocking socket for paramiko or asyncssh; see examples/ssh_from_python.py.
OpenComputers hosts get the same treatment with
open_computers.hosts.ssh_keys(host_id) / add_ssh_key(host_id, key).
API keys, every option
api_keys.create() forwards whatever the control plane accepts, so an option
added server-side works without an SDK change:
key = client.api_keys.create(
name="acme-backend",
description="server key for acme",
metadata={"team": "platform"},
expires_at="2026-12-31T00:00:00Z",
scopes=["sandboxes:read", "sandboxes:write"],
workspace_id=ws.id, # or project_id=
ip_allowlist=["203.0.113.0/24"],
rotation_overlap_days=7,
preset="read_only",
)
client.api_keys.list() # last_used_at and per-key usage come back
client.api_keys.rotate(key["id"])
client.api_keys.update(key["id"], scopes=["sandboxes:read"])
api_keys.presets(), service_accounts and the end-user tokens.create_scoped
complete the surface.
Webhooks you can trust
Create the subscription, then verify every delivery:
from miosa import verify_webhook_signature
hook = platform.webhooks.create(url="https://acme.com/hooks/miosa",
events=["run.finished", "deployment.released"])
secret = hook["secret"]
# in your receiver, with the RAW body:
if not verify_webhook_signature(raw_body, request.headers["X-MIOSA-Signature"], secret):
abort(401)
platform.event_subscriptions.deliveries(subscription_id)
platform.event_subscriptions.replay_delivery(webhook_id, delivery_id)
End-user apps on deployments
dep = platform.deployments.create(name="acme-app", source={"type": "git", "url": repo})
platform.deployments.publish(dep.id)
platform.project_auth.enable(project_id) # your customers log into the app
Errors you will actually catch
WorkspaceNameExistsError, TeamMemberCapReachedError, MemberLimitReachedError,
BillToLockedError, OwnCredentialsRequiredError, MachineStartingError and the
rest carry the server's code, details and retry hints. See
Error handling.
Error handling
from miosa import (
AuthenticationError, NotFoundError, RateLimitError, MiosaError
)
try:
computer = miosa.computers.get("cmp_doesnt_exist")
except NotFoundError:
print("Computer not found")
except RateLimitError as e:
print(f"Rate limited; retry after {e.retry_after}s")
except AuthenticationError:
print("Check your API key")
except MiosaError as e:
print(f"API error {e.status}: {e.message}")
Exception hierarchy: MiosaError > AuthenticationError (401), InsufficientCreditsError (402), PermissionError (403), NotFoundError (404), ValidationError (422), RateLimitError (429), ServerError (5xx), ConnectionError, TimeoutError.
Every error carries the server's stable code and any details, plus retry
hints: retryable (bool) and retry_after_ms (int, or the 429 retry_after).
The sprint codes have typed subclasses, so a caller can catch exactly what it
means:
from miosa import (
CreditRequiredError, # 402 needing credit (a named snapshot over the free allowance)
MachineStartingError, # 409 SANDBOX_STARTING / COMPUTER_STARTING (retry)
OwnCredentialsRequiredError,# 422 the harness has nothing of yours to run on
RunSessionNotResumableError,# 409 continue_from_run_id cannot resume
SnapshotInUseError, # 409 a snapshot still has dependents
)
try:
miosa.commands.run("sbx_abc123", "echo hi")
except MachineStartingError as e:
time.sleep((e.retry_after_ms or 2000) / 1000)
The environments, snapshots, bill-to, agents and triggers codes each have a
typed class (EnvironmentNotFoundError, EnvironmentMemoryWithheldError,
BillToLockedError, TeamMemberCapReachedError, UnknownHarnessError,
TriggerNotFoundError, ...); see miosa.errors for the full list.
Pagination
List endpoints page with limit plus offset or a cursor. miosa.pagination
drives that loop:
from miosa import collect, paginate
for snapshot in paginate(
lambda **p: client.snapshot_history.list(**p), items_key="data", limit=100
):
print(snapshot["id"])
all_runs = collect(lambda **p: client.runs.list(**p), items_key="data")
iter_cursor_pages follows an opaque cursor; iter_pages_async and
iter_cursor_pages_async are the async twins.
Retries
A 429 or 5xx on an idempotent request (GET, HEAD, OPTIONS, PUT,
DELETE) is retried with exponential backoff, honoring Retry-After.
A POST or PATCH is retried only when it carries an Idempotency-Key, so a
retried create dedups server-side instead of running twice. sandboxes.create()
generates one for you; pass idempotency_key= to override, or add the header
yourself for another write.
Configuration
| Option | Env var | Default |
|---|---|---|
api_key |
MIOSA_API_KEY |
— |
base_url |
MIOSA_BASE_URL |
https://api.miosa.ai/api/v1 |
timeout |
— | 30s |
max_retries |
— | 3 |
Links
Phase 1-4 methods (v1.1.0)
Preview tokens and share URLs
# Mint a scoped preview token (e.g. for iframe embed)
token = sb.preview_token(expires_in=3600, scope="read")
print(token["url"]) # https://...?mt=mp_<token>
# Create a public share link (no API key required)
share = sb.share.create(expires_in=7200)
print(share["share_url"])
File tree and batch write
tree = sb.files.tree("/workspace", depth=2)
sb.files.write_many([
{"path": "/workspace/app.py", "content": "print('hello')"},
{"path": "/workspace/cfg.json", "content": b"{}"},
])
Tenant events stream and quotas
for event in client.events.stream(types=["sandbox.*"]):
print(event["_event_type"], event)
client.quotas.set("usr_abc", max_sandboxes=10, max_concurrent=3)
Webhook signature verification
from miosa.resources.webhooks import Webhooks
ok = Webhooks.verify_signature(request.body, request.headers["X-Miosa-Signature"], secret)
License
MIT
Metadata
Release files for miosa 2.16.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| miosa-2.16.5.tar.gz | 543.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| miosa-2.16.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 922.2 kB
Release files / miosa-2.16.5.tar.gz
| Download URL | miosa-2.16.5.tar.gz |
|---|---|
| Size | 543.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0ba09723f1aff9a632adc2ae72d80b34fc53a2ef8fdfa29143de54650500de38
|
|
BLAKE2b-256 checksum How to use checksums |
f5653160561844f3ddc4f725aa8ac1da575b323f122346aa5eae9701d89cf711
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Release files / miosa-2.16.5-py3-none-any.whl
| Download URL | miosa-2.16.5-py3-none-any.whl |
|---|---|
| Size | 378.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
452372d52802051f344d1af11cad02c5f0157987641b7b843f40b1f97de638ee
|
|
BLAKE2b-256 checksum How to use checksums |
5e81e367a8d8937d99f45d3cf0cca1c07bf225cb64c79cdd778a42c6ebe86b76
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Security for miosa 2.16.5
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.