# MIOSA Documentation - Full Text Generated: 2026-10-26T08:13:15.797Z Canonical origin: https://miosa.ai Worker fallback origin: https://miosa.roberto-c49.workers.dev This file is generated from src/routes/docs. It is intended for AI agents, crawlers, search indexers, and customers who need a single plain-text copy of the public documentation. ## Authoritative Agent Contract Guide: https://miosa.ai/docs/guides/agent-operating-guide - Run `miosa capabilities --json`, `miosa app inspect`, and `miosa app plan` before reconstructing a workflow from old examples. - Resolve one exact organization, workspace, application, and environment before mutating resources. - Reuse the linked persistent sandbox unless it is proven terminal. Do not replace resources because an identifier or route was not resolved. - Use `miosa app preview` to create and test an immutable candidate release. Preview never promotes production. - Verify and promote one exact release ID. Never promote an inferred latest release. - Call a release live only when the active release, running artifact digest, route contract, placement, data bindings, and required configuration agree. - Recover interrupted operations with the recorded operation ID and idempotency key instead of starting duplicates. - Use URLs returned by MIOSA. Do not construct preview, deployment, or custom-domain hostnames. --- # MIOSA Documentation URL: https://miosa.ai/docs Fallback URL: https://miosa.roberto-c49.workers.dev/docs Source: src/routes/docs/+page.md Description: Build, preview, deploy, prove, and operate AI-generated applications on MIOSA sandboxes, computers, Docker Host, and white-label domains. --- # Agent Development Kit (ADK) URL: https://miosa.ai/docs/adk Fallback URL: https://miosa.roberto-c49.workers.dev/docs/adk Source: src/routes/docs/adk/+page.md Description: Build LLM agents that use MIOSA sandboxes, desktops, and deploys with the tool-call loop already wired. TypeScript ADK first; Python next. # Agent Development Kit (ADK) The MIOSA ADK lets you build production-ready LLM agents that natively use MIOSA primitives - sandboxes, full desktop environments, and one-click deploys - with the tool-call loop already wired. If `@miosa/sdk` is "call MIOSA APIs from code", `@miosa/adk` is **"build an AGENT that uses MIOSA"**. ```bash npm install @miosa/adk @anthropic-ai/sdk ``` --- ## 30-second example ```typescript const agent = new Agent({ provider: anthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY!, model: "claude-opus-4-7", }), miosaApiKey: process.env.MIOSA_API_KEY!, }); const result = await agent.run({ prompt: "Create a MIOSA sandbox, write a Python hello-world, run it, then destroy the sandbox.", maxIterations: 10, }); console.log(result.finalText); ``` That's it. The agent boots a sandbox, writes the file, runs it, prints output, destroys the sandbox, returns the final response. No tool-loop boilerplate. --- ## ADK vs SDK The raw TypeScript SDK around MIOSA's REST API. You call methods: ```typescript const computer = await client.computers.create({ name: "x" }); await computer.exec.bash("echo hello"); ``` Use this when you're **writing application code** that talks to MIOSA. Adds the agent loop on top. You give it a provider + prompt: ```typescript const agent = new Agent({ provider, miosaApiKey }); await agent.run({ prompt: "build a flask app and run it" }); ``` Use this when you're **building an agent** that should use MIOSA. --- ## Built-in MIOSA tools When you pass `miosaApiKey`, the agent gets these tools out of the box: | Tool | Does | |------|------| | `create_sandbox` | Boot a Debian 12 microVM (Python + Node). | | `create_desktop` | Start a full desktop environment. | | `list_sandboxes` | List your active computers. | | `get_sandbox` | Fetch one sandbox by id (status / IP). | | `destroy_sandbox` | Delete a computer. | | `exec` | Run a bash command. | | `exec_python` | Run a Python snippet inline. | | `read_file` | Read a file from the VM. | | `write_file` | Write a file inside the VM. | | `list_files` | List a directory inside the VM. | Get an API key at , or run `npx -y @miosa/cli mcp install` to wire one via browser-based auth. --- ## Custom tools Compose your own tool catalogue alongside (or instead of) the MIOSA tools: ```typescript Agent, anthropicProvider, miosaTools, type Tool, } from "@miosa/adk"; const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }); const sendSlack: Tool = { name: "send_slack", description: "Post a message to the #builds Slack channel.", inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"], }, async execute(args) { const text = String(args["text"] ?? ""); // your Slack call here return `Sent: ${text}`; }, }; const agent = new Agent({ provider: anthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }), tools: [...miosaTools({ client: miosa }), sendSlack], }); ``` --- ## Streaming step events Drive a live UI as the agent runs: ```typescript const result = await agent.run({ prompt: "...", onStep: (step) => { console.log(`[turn ${step.index}] ${step.text}`); for (const call of step.toolCalls) { console.log( ` → ${call.name}(${JSON.stringify(call.input)}) → ${call.output.slice(0, 80)}`, ); } }, }); ``` The `AgentStep` shape: ```typescript interface AgentStep { index: number; text: string; toolCalls: Array<{ id: string; name: string; input: Record; output: string; isError: boolean; }>; usage?: { inputTokens: number; outputTokens: number }; } ``` --- ## Providers `@miosa/adk` ships with native tool-call providers for hosted, marketplace, and local OpenAI-compatible models, plus Anthropic and Gemini-specific adapters. The same provider interface also works for custom internal routers. ```typescript const provider = anthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY!, model: "claude-opus-4-7", // default maxTokens: 4096, // default }); ``` Uses `@anthropic-ai/sdk` Messages API with native tool-use. Supports any Claude model that supports tool calling. ```typescript const provider = surplusProvider({ apiKey: process.env.SURPLUS_API_KEY!, model: "claude-opus-4.8", }); ``` Surplus uses the OpenAI-compatible chat completions contract. In platform runtime profiles, prefer routing through MIOSA's Intelligence API/model route so keys can be metered, audited, revoked, and used as fallback fuel for Claude Code, Codex, Hermes, OSA, or custom harnesses inside MIOSA sandboxes. ```typescript const provider = miosaRouteProvider({ miosaApiKey: process.env.MIOSA_API_KEY!, model: "optimal", }); ``` Use this when the agent should call MIOSA's Intelligence API rather than a third-party provider directly. As model routes ship, the model can become a route id such as `route:cheap-claude-code`; MIOSA then owns provider selection, fallbacks, metering, and secret handling. ```typescript const openai = openAIProvider({ apiKey: process.env.OPENAI_API_KEY!, model: "gpt-5", }); const groq = groqProvider({ apiKey: process.env.GROQ_API_KEY!, model: "moonshotai/kimi-k2-instruct-0905", }); const internal = openAICompatibleProvider({ name: "internal-router", baseUrl: "https://llm.example.com/v1/chat/completions", apiKey: process.env.INTERNAL_LLM_KEY, model: "best-agent-model", }); ``` The bundled OpenAI-compatible provider covers OpenAI, Groq, DeepSeek, OpenRouter, Together, Fireworks, Mistral, Cerebras, Perplexity, xAI, Surplus, MIOSA Intelligence, Ollama, LM Studio, and custom routers. Implement the `Provider` interface directly when an endpoint needs a custom tool or streaming protocol. --- ## Termination `agent.run()` returns when: - The model emits an `end_turn` with no further tool calls (normal completion) - A `stop_sequence` is hit - `maxIterations` is reached (default 10) - safety guard against runaway loops - The provider throws (returned as `stopReason: "error"`) ```typescript const result = await agent.run({ prompt: "...", maxIterations: 20 }); if (result.stopReason === "max_iterations") { console.warn("Agent didn't finish in 20 turns"); } ``` --- ## Quick comparison | | `@miosa/adk` | Anthropic Claude Agent SDK | OpenAI Agents SDK | Google ADK | |--|--|--|--|--| | Tool-call loop | ✓ | ✓ | ✓ | ✓ | | MIOSA tools built-in | **✓** | - | - | - | | Anthropic provider | ✓ | ✓ | - | - | | OpenAI-compatible providers | ✓ | - | ✓ | - | | Sub-agents | (roadmap) | ✓ | ✓ | ✓ | | Memory | (roadmap) | ✓ | (basic) | ✓ | | Cost | free | free | free | free | The ADK is intentionally small. If you want MIOSA-as-execution-substrate with a clean agent loop, you're in the right place. --- ## What's coming next - **Python ADK** (`pip install miosa-adk`) - same surface, Python ergonomics. - **Managed model routes** - route ADK and in-sandbox harness calls through MIOSA Intelligence API. - **Sub-agents** - spawn child agents with scoped tool subsets. - **Memory** - built-in conversation memory with TTL + summarization. - **Streaming output** - token-by-token via provider.streamStep. Open an issue at if there's something you need sooner. --- # Agent Studio URL: https://miosa.ai/docs/agents/agent-studio Fallback URL: https://miosa.roberto-c49.workers.dev/docs/agents/agent-studio Source: src/routes/docs/agents/agent-studio/+page.md Description: Chat with an agent in a dedicated workspace, with one warm sandbox per chat and honest status and errors. Agent Studio is the chat-first way to use a saved agent. The chat is the main surface, and the canvas folds into a slim strip you can reopen. ## One sandbox per chat A chat gets one warm sandbox, and follow-ups reuse it. A paused sandbox is woken with its files. Only a destroyed sandbox is replaced, and the chat says so. ## Honest status and errors The status line shows what is happening: starting the sandbox, starting the agent, thinking. A failed message shows the platform's own message, its code, and one hint, shown once on the turn. ## The workspace panel A workspace panel sits beside the chat, with the same tabs as Optimal: - Preview - Files - Code - Terminal - Machine - Logs It opens on the chat's first activity and shows the chat's own sandbox. ## A clean conversation Harness wording such as Overdrive warnings and the stdin banner never reaches the chat. Overdrive is one quiet chip beside the composer with a tooltip. Tool rows stay inside the chat: a long argument can no longer widen it, and commands, arguments, and JSON output are readable. Past chats are kept per agent in both Agent Studio and the agent page. ## Related - [Chat with an agent on a machine](/docs/agents/chat-on-a-machine/): the chat on a computer or sandbox page. - [Configure an agent](/docs/guides/configure-agent/): instructions, model, tools, and permissions. --- # Chat with an agent on a machine URL: https://miosa.ai/docs/agents/chat-on-a-machine Fallback URL: https://miosa.roberto-c49.workers.dev/docs/agents/chat-on-a-machine Source: src/routes/docs/agents/chat-on-a-machine/+page.md Description: Open a computer, or the Agent section of a sandbox, and chat with an agent bound to that machine. Opening a computer, or the Agent section of a sandbox, opens one chat bound to that machine. The workspace panel sits beside it. ## Pick the brain in the chat Before the first message you choose what powers the chat: - **Optimal** runs a machine-bound chat on wallet credits. - **A harness** runs on your own account: Claude Code, Codex, OSA, pi, OpenCode, Prime Agent, Kimi Code, or Mistral Vibe. - **A saved agent** runs with its own instructions, tools, and permissions. The chat is bound to the machine you opened, so there is no "where should you build?" step. The brain is remembered per person, per workspace, and per machine. It never changes the brain you picked on the [Optimal](/docs/optimal/) page. ## While it works - Switching the brain stops the run, keeps the old thread above a divider, and starts a new thread. - A Connect card opens when your account is not connected. - Approvals appear inline, and you can approve or deny. - **Stop** cancels the run. - **Take over** hands control to you, and **Hand back** returns it. ## The panel follows the machine The workspace panel opens on Desktop for a computer and on Terminal for a sandbox, then Preview once a URL exists. It restores the layout you had on the Optimal page when you leave. A machine that belongs to another workspace never gets a chat, a chat id, or a run. ## Related - [Agent Studio](/docs/agents/agent-studio/): the chat-first agent workspace. - [Build on your machines](/docs/optimal/machines/): the panel and its tabs. --- # Agent confinement URL: https://miosa.ai/docs/agents/confinement Fallback URL: https://miosa.roberto-c49.workers.dev/docs/agents/confinement Source: src/routes/docs/agents/confinement/+page.md Description: An agent runs inside its machine and cannot use MIOSA's own platform key unless an Environment grants it a scoped one. An agent runs inside its machine and stays there. It cannot use MIOSA's own platform key to reach the rest of the platform. ## The platform key is blanked Every sandbox boots with a platform key and its ids in the environment, so a plain command can call MIOSA. Harness runs do not get it. When MIOSA starts a harness, it blanks the platform key and the ids at launch, and unsets them in the run script. So Claude Code, Codex, OSA, pi, OpenCode, Prime Agent, Kimi Code, Mistral Vibe, and Hermes Agent cannot borrow MIOSA's key. ## Grant a scoped key with an Environment When you want an agent to reach MIOSA on purpose, an [Environment](/docs/cli/environments/) can grant it a scoped machine key. That key is bound to the machine, not to your account. ## What is kept - A value the run itself supplies is kept. - Environment grants are not touched. ## Related - [Isolation and permissions](/docs/optimal/isolation/): what a hosted chat can touch. - [Runtime environment](/docs/guides/runtime-env/): variables by scope and target. --- # Harnesses and auth URL: https://miosa.ai/docs/agents/harnesses Fallback URL: https://miosa.roberto-c49.workers.dev/docs/agents/harnesses Source: src/routes/docs/agents/harnesses/+page.md Description: The agents MIOSA can run on your machine, how each one authenticates, and how MIOSA gets each one ready. A harness is a coding agent MIOSA runs on a machine, on your own account. MIOSA never falls back to its own keys. ## The harnesses | Harness | `runner` | |---|---| | Claude Code | `claude-code` | | Codex | `codex` | | OSA | `osa` | | pi | `pi` | | OpenCode | `opencode` | | Prime Agent | `prime` | | Kimi Code | `kimi` | | Mistral Vibe | `mistral` | | Hermes Agent | `hermes` | There is also `custom` for a gateway model, which runs on MIOSA's AI Gateway. ## How MIOSA gets them ready Runs are fast because a machine rarely installs anything at run time. | Ready | Harnesses | |---|---| | Prebaked in the machine image | Claude Code, Codex, OSA | | Installed on first use | pi, OpenCode, Prime Agent, Kimi Code, Mistral Vibe | | Add-on, installed at create | Hermes Agent | Hermes Agent is a large install, about 2.5 GB, so it is added at create and cached per host. ## Which account powers which harness A Connection you make once is reused by every harness that accepts it. Connect a ChatGPT plan once, and Codex, pi, OpenCode, Prime Agent, and Hermes Agent all use it. | Harness | Plans | API keys | |---|---|---| | Claude Code | Claude plan | Anthropic, OpenRouter, DeepSeek; Amazon Bedrock | | Codex | ChatGPT plan | OpenAI, DeepSeek; Amazon Bedrock | | OSA | | Ollama Cloud; Anthropic, OpenAI, OpenRouter | | pi | Claude plan, ChatGPT plan, Mistral Vibe key | Anthropic, OpenAI, OpenRouter, Mistral, LLM Gateway | | OpenCode | ChatGPT plan, Mistral Vibe key | Anthropic, OpenAI, OpenRouter, Mistral, LLM Gateway | | Prime Agent | Claude plan, ChatGPT plan, Mistral Vibe key | Anthropic, OpenAI, OpenRouter, Mistral, LLM Gateway | | Kimi Code | Kimi Code plan | Moonshot | | Mistral Vibe | Mistral Vibe key | Mistral | | Hermes Agent | ChatGPT plan, Claude plan (Max with extra usage) | Anthropic, OpenAI, OpenRouter | Two caveats to know: - **OpenCode cannot use a Claude plan.** Anthropic does not allow it, and OpenCode removed the plugins that did it. - **Hermes Agent on a Claude plan needs a Claude Max plan with extra usage credits.** A Claude Pro plan is refused. ## Hermes Agent Hermes Agent is a first-class runner in the run API: send `runner: "hermes"` to `POST /api/v1/runs`. It is not in the CLI `--harness` list yet, so use the API or the console until it is. MIOSA runs the platform's pinned Hermes version, and never installs a newer one. A Hermes run without an account returns `OWN_CREDENTIALS_REQUIRED`, the same as any other harness. ## The harness list as an API `GET /api/v1/agents/harnesses` returns each harness with: - `supported_auth`: the accounts that can power it, in the order the run path tries them. Each has `id`, `label`, `kind`, `vendor`, `injection`, `model`, `source`, and `note`. - `also_supports`: what the agent's own CLI takes that MIOSA does not connect yet. - `unsupported_auth`: what a person may expect to work but the vendor refuses, with `reason` and `source`. Each model carries `served_by`, the account ids that can run it. Refresh tokens never reach a machine. The control plane is the single refresh owner, and hands the machine a short-lived access token that outlives the run. ## Related - [Quickstart](/docs/quickstart/): run an agent in a sandbox. - [Agents, chats and runs](/docs/cli/agents/): the CLI surface. - [Chat with an agent on a machine](/docs/agents/chat-on-a-machine/): pick a harness from the chat. --- # Interactive API Reference - MIOSA Docs URL: https://miosa.ai/docs/api Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api Source: src/routes/docs/api/+page.svelte Description: Browse and try the allowlisted MIOSA Sandbox V1 API interactively. Use the written endpoint guides for the wider platform API. Interactive API reference This interactive reference covers the allowlisted Sandbox V1 OpenAPI surface. Use the written API reference for deployments, databases, computers, and the wider platform contract. {#if !mounted} {/if} --- # API Reference URL: https://miosa.ai/docs/api-reference Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference Source: src/routes/docs/api-reference/+page.md Description: REST API reference for the MIOSA platform - base URL, auth, request/response envelopes, and endpoint guides. Want to browse and try endpoints without writing code? The Interactive Sandbox API Reference renders the allowlisted public Sandbox V1 OpenAPI contract with an in-browser request runner. Use the endpoint guides below for the wider platform API. Every MIOSA API endpoint is REST over HTTPS, returns JSON, and follows a consistent envelope format. The SDKs wrap these endpoints - you can use them directly when you need low-level control or want to integrate from a language without an official SDK. ## Base URL ``` https://api.miosa.ai/api/v1 ``` ## Authentication All requests require a Bearer token: ```http Authorization: Bearer msk_u_... ``` MIOSA API keys (`msk_*`) and short-lived JWT tokens are both accepted. Keys are scoped to an organization and its workspaces/projects. See [API Keys](/docs/platform/api-keys/) for scope details. ## Request format - `Content-Type: application/json` for all request bodies. - File uploads use `multipart/form-data`. - All field names are **snake_case**. - Mutation requests (`POST`, `PUT`, `PATCH`, `DELETE`) should include an `Idempotency-Key` header to enable safe retries: ```http POST /api/v1/sandboxes Authorization: Bearer msk_u_... Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 Content-Type: application/json { "template": "miosa-sandbox", "workspace_slug": "acme-corp", "project_slug": "lead-magnet", "external_workspace_id": "acct_123", "external_user_id": "user-456", "external_project_id": "project_789" } ``` Use a UUID v4 as the idempotency key. Repeating the same key within 24 hours returns the original response without re-executing the mutation. The key also keeps a retry safe: an idempotent create whose placement outcome is uncertain answers a retryable `503 SANDBOX_PLACEMENT_RECONCILING` with `Retry-After` instead of a terminal `409`, so retrying with the same key resolves to the same sandbox. ## Response envelope Single-resource responses: ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "my-sandbox", "status": "running", "created_at": "2026-05-17T10:00:00Z" } } ``` List responses include a pagination object: ```json { "data": [ { "id": "...", "name": "..." } ], "page": { "page": 1, "page_size": 20, "total": 150 } } ``` Pagination parameters accepted by all list endpoints: | Parameter | Type | Default | Max | |---|---|---|---| | `page` | integer | 1 | - | | `page_size` | integer | 20 | 100 | ## Error envelope All errors follow a single shape: ```json { "error": { "code": "sandbox_not_found", "message": "No sandbox with id sbx_abc exists in this workspace.", "request_id": "req_01jv..." } } ``` Include the `request_id` when contacting support. ### HTTP status codes | Status | Meaning | |---|---| | 200 | Success | | 201 | Created | | 202 | Accepted - async operation started | | 400 | Bad request - missing or invalid parameters | | 401 | Unauthorized - invalid or missing token | | 403 | Forbidden - valid token, insufficient scope | | 404 | Not found | | 409 | Conflict - invalid state transition (e.g. starting a running computer) | | 413 | Payload too large - file upload exceeds 10 MB | | 422 | Unprocessable entity - validation error | | 429 | Rate limited | | 500 | Internal server error | | 502 | Bad gateway - command service unreachable | ## Rate limits Rate limits apply per API key, tenant or organization, and endpoint family. The current self-service defaults are 600 requests per minute for user keys and 30,000 requests per minute for platform keys. Specific endpoint families and tenant policies can impose lower limits. Treat `X-RateLimit-*` and `Retry-After` response headers as authoritative. ## IDs and timestamps Canonical resource IDs returned by the current API are UUIDs. Some older SDK examples and compatibility surfaces may still show human-readable aliases. Pass the exact ID returned by the API instead of constructing or prefixing an identifier. All timestamps are ISO 8601 in UTC: `2026-05-17T10:00:00Z`. ## Ownership fields Create endpoints for resources accept the same ownership selectors: | Field | Use | |---|---| | `workspace_id` | Existing MIOSA workspace UUID | | `workspace_slug` | Existing or auto-created workspace slug | | `workspace_name` | Display name used when auto-creating a workspace | | `project_id` | Existing MIOSA project UUID | | `project_slug` | Existing or auto-created project slug | | `project_name` | Display name used when auto-creating a project | | `external_workspace_id` | Your customer/account/workspace ID | | `external_user_id` | Your end-user ID | | `external_project_id` | Your project/app/document ID | Responses include `workspace_id` and `project_id` when the resource is owned by a workspace/project. See [Ownership and Attribution](/docs/platform/attribution/). --- ## Compute Create, list, get, stop, and delete sandbox environments. The core compute primitive. [Open →](/docs/api-reference/sandboxes/) Dispatch prompts into sandboxes or computers through OSA, Codex, Claude Code, Pi, Hermes, or a custom runtime. [Open →](/docs/api-reference/agent-runs/) Configure reusable agent runtimes, tools, connector grants, policy, and non-secret env defaults. [Open →](/docs/api-reference/agent-runtime-profiles/) Run bash commands and Python code inside a running computer or sandbox. [Open →](/docs/api-reference/exec/) Server-sent events stream for long-running commands - line-by-line stdout/stderr. [Open →](/docs/api-reference/streaming-exec/) Read, write, stat, mkdir, rename, copy, and chmod files inside a computer. [Open →](/docs/api-reference/files/) Directory listings, multi-file operations, and archive upload/download. [Open →](/docs/api-reference/filesystem/) Create, restore, and delete point-in-time snapshots of a computer's disk state. [Open →](/docs/api-reference/snapshots/) ## Deploy Manage static and server deployments - create, list, get, and delete. [Open →](/docs/api-reference/deployments/) Immutable deployment versions. Each publish creates a new version. [Open →](/docs/api-reference/versions/) Promote a version to a named release (production, staging, etc.). [Open →](/docs/api-reference/releases/) Register, verify, and manage custom domains on deployments. [Open →](/docs/api-reference/custom-domains/) ## Computers Full lifecycle for desktop VM computers - create, start, stop, restart, resize. [Open →](/docs/api-reference/computers/) Desktop control actions: screenshot, click, type, key, scroll, drag, hotkey, windows, accessibility tree. [Open →](/docs/api-reference/desktop/) Register your own hardware as a MIOSA computer (BYOC). One command, any machine. [Open →](/docs/api-reference/open-computers/) ## Platform Customer/client workspaces inside an organization. [Open →](/docs/api-reference/workspaces/) Apps, websites, documents, and workflows inside a workspace. [Open →](/docs/api-reference/projects/) Managed background services attached to a computer (databases, queues, etc.). [Open →](/docs/api-reference/services/) Provision managed databases and operate lifecycle, backups, restores, branches, replicas, credentials, logs, and metrics. [Open →](/docs/api-reference/databases/) Inbound and outbound network rules for a computer. [Open →](/docs/api-reference/network-policy/) Lifecycle events emitted by computers and sandboxes - for webhooks and audit logs. [Open →](/docs/api-reference/events/) Read credit balance, list usage transactions, and top up. [Open →](/docs/api-reference/credits/) List available regions and their current capacity. [Open →](/docs/api-reference/regions/) SDK coverage varies by language and release. Use the current SDK page for your language to confirm a helper exists. You can always call the documented REST endpoint directly. --- # Agent Run Groups URL: https://miosa.ai/docs/api-reference/agent-run-groups Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/agent-run-groups Source: src/routes/docs/api-reference/agent-run-groups/+page.md Description: Create durable multi-agent orchestration groups, dispatch run manifests, inspect counts, and cancel grouped work. # Agent Run Groups Agent Run Groups are the durable control-plane object for multi-agent fanout. Use a group when one user request creates many related Agent Runs across sandboxes, computers, or custom runtimes. Base path: `/api/v1/agent-run-groups` Use synchronous dispatch for small fanout where you want child run creation in the response. Use `async: true` for queued fanout: MIOSA stores each manifest entry, enforces the group's concurrency limit while workers claim entries, and returns immediately with queued entry records. ## Endpoints | Method | Path | Description | |---|---|---| | `GET` | `/api/v1/agent-run-groups` | List groups for the tenant | | `POST` | `/api/v1/agent-run-groups` | Create a group | | `GET` | `/api/v1/agent-run-groups/{id}` | Fetch one group | | `POST` | `/api/v1/agent-run-groups/{id}/dispatch` | Dispatch a bounded manifest of child Agent Runs | | `GET` | `/api/v1/agent-run-groups/{id}/events` | Replay or stream durable events from every child run in the group | | `POST` | `/api/v1/agent-run-groups/{id}/cancel` | Cancel the group and running children | ## Create ```http POST /api/v1/agent-run-groups Authorization: Bearer msk_u_... Content-Type: application/json ``` ```json { "name": "lumen-browser-checks", "workspace_id": "ws_123", "project_id": "proj_123", "concurrency_limit": 25, "expected_runs": 100, "metadata": { "user_task_id": "task_123" } } ``` ## Dispatch Dispatch accepts a `runs` array. Each entry uses the same fields as [Agent Runs](/docs/api-reference/agent-runs/), plus optional `orchestration_role` and `parent_agent_run_id`. ```http POST /api/v1/agent-run-groups/{id}/dispatch Authorization: Bearer msk_u_... Content-Type: application/json ``` ```json { "runs": [ { "sandbox_id": "sbx_code_1", "provider": "claude-code", "cwd": "/workspace", "prompt": "Implement the pricing page and export /workspace/report.html", "orchestration_role": "coder", "metadata": { "artifact_paths": ["/workspace/report.html"] } }, { "computer_id": "cmp_browser_1", "provider": "osa", "prompt": "Open the preview, test signup, and capture screenshots.", "orchestration_role": "browser-qa" } ], "async": true } ``` Set `async` to `true` when the group should queue entries and return before the child Agent Runs are created. MIOSA creates one durable entry per manifest item, then dispatcher workers claim entries under the group's `concurrency_limit`. Current dispatch batches are capped at `100` entries per request. For larger fleets, shard logical work into multiple groups or dispatch batches from your backend. ## Response ```json { "data": { "group": { "id": "grp_123", "name": "lumen-browser-checks", "status": "running", "concurrency_limit": 25, "expected_runs": 100, "counts": { "total": 2, "running": 0, "succeeded": 2, "failed": 0, "canceled": 0 }, "entry_counts": { "total": 2, "queued": 0, "running": 0, "succeeded": 2, "failed": 0, "canceled": 0 } }, "results": [ { "index": 0, "ok": true, "run": { "id": "run_123", "agent_run_group_id": "grp_123", "orchestration_role": "coder", "status": "succeeded" } } ], "entries": [] } } ``` Async dispatch returns `entries` immediately and usually returns an empty `results` array: ```json { "data": { "group": { "id": "grp_123", "status": "running", "concurrency_limit": 25, "entry_counts": { "total": 2, "queued": 2, "running": 0, "succeeded": 0, "failed": 0, "canceled": 0 } }, "results": [], "entries": [ { "id": "agre_123", "agent_run_group_id": "grp_123", "index": 0, "status": "queued", "attempts": 0 } ] } } ``` ## Events Use group events when your UI needs one stream for an entire multi-agent task. MIOSA records child Agent Run events and replays them through the group in sequence order. This lets your product render progress from the coder, tester, browser QA agent, and deployment verifier without opening one stream per child run. Replay existing events as JSON: ```http GET /api/v1/agent-run-groups/{id}/events Authorization: Bearer msk_u_... ``` Stream existing events plus live events with SSE: ```http GET /api/v1/agent-run-groups/{id}/events?stream=true Accept: text/event-stream Authorization: Bearer msk_u_... ``` Event payloads include the group id, child run id, event sequence, event type, message, payload, and timestamp: ```json { "data": [ { "id": "ev_123", "agent_run_group_id": "grp_123", "agent_run_id": "run_123", "sequence": 4, "type": "command_finished", "message": "Command completed", "payload": { "exit_code": 0 }, "created_at": "2026-06-15T21:20:00Z" } ] } ``` Terminal group states close the SSE response after replay. Running groups keep the stream open and emit heartbeat comments while waiting for new child events. ## SDK ```typescript const group = await miosa.agentRunGroups.create({ name: "lumen-browser-checks", workspaceId: "ws_123", concurrencyLimit: 25, expectedRuns: 100, }); const result = await miosa.agentRunGroups.dispatch( group.id, [ { sandboxId: "sbx_code_1", provider: "claude-code", cwd: "/workspace", prompt: "Implement the pricing page and export /workspace/report.html", orchestrationRole: "coder", metadata: { artifact_paths: ["/workspace/report.html"] }, }, ], { async: true }, ); const withRuns = await miosa.agentRunGroups.get(group.id, { includeRuns: true }); const events = await miosa.agentRunGroups.events(group.id); for await (const event of miosa.agentRunGroups.streamEvents(group.id)) { console.log(event.agent_run_id, event.type, event.message); } ``` ```python group = client.agent_run_groups.create( name="lumen-browser-checks", workspace_id="ws_123", concurrency_limit=25, expected_runs=100, ) result = client.agent_run_groups.dispatch( group["id"], [ { "sandbox_id": "sbx_code_1", "provider": "claude-code", "cwd": "/workspace", "prompt": "Implement the pricing page and export /workspace/report.html", "orchestration_role": "coder", "metadata": {"artifact_paths": ["/workspace/report.html"]}, } ], async_=True, ) with_runs = client.agent_run_groups.get(group["id"], include_runs=True) events = client.agent_run_groups.events(group["id"]) for event in client.agent_run_groups.stream_events(group["id"]): print(event["agent_run_id"], event["type"], event.get("message")) ``` ## CLI ```bash miosa api POST /run-groups -d '{ "name": "lumen-browser-checks", "workspace_id": "ws_123", "concurrency_limit": 25, "expected_runs": 100 }' miosa api POST /run-groups/grp_123/dispatch -d '{ "runs": [ { "sandbox_id": "sbx_code_1", "provider": "claude-code", "prompt": "Build and export report", "metadata": {"artifact_paths": ["/workspace/report.html"]} } ], "async": true }' miosa api GET /run-groups/grp_123?include=runs miosa api GET /run-groups/grp_123/activity miosa api GET "/run-groups/grp_123/activity?stream=true&limit=100" miosa api POST /run-groups/grp_123/cancel ``` ## Next Dispatch one prompt into a sandbox or computer and collect events and artifacts. Design the product backend that assigns devices, streams progress, and stores outputs. Configure inherited environment variables for agents and deployments. --- # Agent Runs URL: https://miosa.ai/docs/api-reference/agent-runs Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/agent-runs Source: src/routes/docs/api-reference/agent-runs/+page.md Description: Dispatch prompts into MIOSA devices with a stable run response shape for agent work. # Agent Runs Agent Runs are the prompt-dispatch interface for MIOSA sandboxes and computers. Use them when your product needs to send a natural-language task into a device, run it through OSA, Codex, Claude Code, Pi, Hermes, or a custom runtime, and receive a stable run response your UI can render. This is the API behind: - `miosa agent run --sandbox ...` - `miosa agent run --computer ...` - `miosa prompt` - `client.agentRuns.run(...)` in the TypeScript SDK - `client.agent_runs.run(...)` in the Python SDK - `miosa run files ` - `miosa run download ` Use raw exec when you already know the command. Use Agent Runs when the user prompt should be handled by an agent runtime inside a MIOSA device. The run response preserves prompt, provider, target, output, error, metadata, and timestamps for your product UI and audit trail. ## Flow ## Create a run ```http POST /api/v1/agent-runs Authorization: Bearer msk_u_... Content-Type: application/json ``` `POST /agent-runs` is live for sandbox and computer targets. Use sandbox targets for code, files, tests, builds, artifacts, and preview servers. Use computer targets when the task needs a browser, desktop, screenshots, or interactive UI state. For customer-owned OpenComputers hosts, use `POST /opencomputers/hosts/{id}/agent/dispatch`. ```json { "target_kind": "sandbox", "target_id": "41026070-9bb0-4d62-90b4-8ceeb0a131b6", "provider": "claude-code", "prompt": "Update the landing page, run tests, and leave the preview running.", "cwd": "/workspace", "timeout": 1800, "metadata": { "workspace_id": "lumen", "task_id": "task_123" } } ``` ### Request fields | Field | Type | Required | Description | |---|---:|---:|---| | `prompt` | string | yes | Natural-language task to run. | | `target_id` | UUID/string | yes | Sandbox or computer id. | | `target_kind` | `sandbox` / `computer` | recommended | Target family. Defaults are inferred when using `sandbox_id` or `computer_id`. | | `sandbox_id` | UUID/string | no | Shortcut for `target_id` with sandbox target. | | `computer_id` | UUID/string | no | Shortcut for `target_id` with computer target. | | `provider` | string | no | `osa`, `codex`, `claude`, `claude-code`, `pi`, `hermes`, or `custom`. | | `command` | string | required for `custom` | Runtime command for custom providers. | | `model` | string | no | Provider-specific model override. | | `cwd` | string | no | Working directory, usually `/workspace`. | | `timeout` | integer | no | Run timeout in seconds. | | `env` | object | no | Non-secret run env overrides. Profile env is applied first. | | `agent_runtime_profile_id` | UUID/string | no | Force a specific runtime profile for this run. Alias: `agent_profile_id`. | | `agent_run_group_id` | UUID/string | no | Attach this run to an Agent Run Group. | | `parent_agent_run_id` | UUID/string | no | Link this run to a parent/orchestrator run. | | `orchestration_role` | string | no | Product role such as `coder`, `browser-qa`, `tester`, or `artifact-generator`. | | `skip_agent_runtime_profile` | boolean | no | If `true`, skip tenant/workspace default runtime profile resolution. | | `metadata` | object | no | Product ids, UI labels, task ids, or trace metadata. | ## Response ```json { "data": { "id": "b1772e2f-89d1-4b59-9aa7-147eed4c902a", "agent_run_group_id": "grp_123", "parent_agent_run_id": null, "orchestration_role": "coder", "target_kind": "sandbox", "target_id": "41026070-9bb0-4d62-90b4-8ceeb0a131b6", "provider": "claude-code", "prompt": "Update the landing page, run tests, and leave the preview running.", "status": "succeeded", "output": "Updated app/page.tsx and started preview on port 3000.", "stderr": "", "exit_code": 0, "metadata": { "workspace_id": "lumen", "task_id": "task_123" }, "started_at": "2026-06-14T10:30:00Z", "finished_at": "2026-06-14T10:31:42Z", "created_at": "2026-06-14T10:30:00Z", "updated_at": "2026-06-14T10:31:42Z" } } ``` ### Status values | Status | Meaning | |---|---| | `running` | Reserved for async run history. | | `succeeded` | The runtime completed successfully. | | `failed` | The runtime returned a non-zero exit or backend error. | | `canceled` | The run was canceled. For sandbox process-backed runs, MIOSA also stops the recorded sandbox process. | ## Artifacts Agent runs can record generated files as artifacts. MIOSA captures declared artifact bytes into managed storage when possible, then falls back to the live target filesystem for older or unpersisted artifacts. That matters for product UX: if `persisted` is `true`, your users can download the file even after the sandbox or computer session has stopped. If `persisted` is `false`, keep the target running until the file has been downloaded, exported, or published. Declare artifact paths in the request body: ```json { "sandbox_id": "41026070-9bb0-4d62-90b4-8ceeb0a131b6", "provider": "claude-code", "prompt": "Create /workspace/report.html", "metadata": { "artifact_paths": ["/workspace/report.html"] } } ``` List artifacts: ```http GET /api/v1/agent-runs/{run_id}/artifacts Authorization: Bearer msk_u_... ``` Download one artifact: ```http GET /api/v1/agent-runs/{run_id}/artifacts/{artifact_id}/download Authorization: Bearer msk_u_... ``` Use `?disposition=inline` when embedding an HTML/PDF/image artifact in your own preview UI. The default response is an attachment. Artifact list responses also include short-lived `signed_download_url` values when the backend can mint them. Use these URLs for browser-facing downloads in your own UI; they do not require exposing the user's API key. Artifact records include persistence metadata: | Field | Meaning | |---|---| | `status` | Artifact lifecycle status such as `created`, `available`, or `failed`. | | `sha256` | Content hash when MIOSA captured bytes. | | `persisted` | `true` when the bytes are stored outside the live runtime. | | `storage_backend` | Storage backend label, for example `s3` or `local`. | | `persisted_at` | Timestamp when MIOSA captured the bytes. | MIOSA does not expose internal storage keys or bucket URLs in the public API. Use the download endpoint or signed download URL instead. ## Events Agent Run events are durable. Use them to build timelines, progress feeds, audit trails, and orchestration UIs. | Event | Meaning | |---|---| | `created` | Run record was created. | | `command_started` | MIOSA started executing the runtime command. | | `process_started` | Sandbox process-backed run started and recorded a sandbox process id. | | `command_finished` | Runtime command finished and the run was marked succeeded or failed. | | `artifacts_recorded` | Declared artifacts were captured or recorded. | | `failed` | Run failed before normal command completion. | | `canceled` | Run was canceled. | List durable events: ```http GET /api/v1/agent-runs/{run_id}/events Authorization: Bearer msk_u_... ``` Stream events with SSE: ```http GET /api/v1/agent-runs/{run_id}/events?stream=true Accept: text/event-stream Authorization: Bearer msk_u_... ``` The stream sends existing durable events first, then live PubSub events. Terminal events such as `command_finished`, `failed`, and `canceled` close the stream. ## Cancellation ```http POST /api/v1/agent-runs/{run_id}/cancel Authorization: Bearer msk_u_... ``` Cancellation is best-effort by target type: | Target | Behavior | |---|---| | Sandbox process-backed run | MIOSA stops the recorded sandbox process, marks the run `canceled`, and emits `canceled`. | | Older synchronous sandbox path | MIOSA marks the run `canceled`; the command may already have completed by the time cancellation is received. | | Computer/BYOC target | MIOSA marks the run `canceled`; computer runtime interruption depends on the target agent implementation. | For sandbox process-backed runs, the run metadata includes: ```json { "agent_process": { "target_kind": "sandbox", "target_id": "41026070-9bb0-4d62-90b4-8ceeb0a131b6", "process_id": "proc_123", "pid": 123, "started_at": "2026-06-15T16:45:00Z" } } ``` ## Run history Use the response from `POST /agent-runs` for the immediate task result. For long-running browser or desktop conversations, keep using the Computer session APIs alongside Agent Runs so your UI can show both the interactive session and the per-prompt run result. ## SDK examples ```typescript const client = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }); const run = await client.agentRuns.run({ sandboxId: "41026070-9bb0-4d62-90b4-8ceeb0a131b6", targetKind: "sandbox", agentRunGroupId: "grp_123", orchestrationRole: "coder", agentRuntimeProfileId: "arp_123", provider: "claude-code", cwd: "/workspace", timeout: 1800, prompt: "Build the dashboard, run tests, and keep the preview live.", metadata: { artifact_paths: ["/workspace/report.html"], }, }); console.log(run.id, run.status, run.output); const artifacts = await client.agentRuns.artifacts(run.id); const report = await client.agentRuns.downloadArtifact(run.id, artifacts[0].id); ``` ```python from miosa import Miosa client = Miosa() run = client.agent_runs.run( sandbox_id="41026070-9bb0-4d62-90b4-8ceeb0a131b6", target_kind="sandbox", agent_run_group_id="grp_123", orchestration_role="coder", agent_runtime_profile_id="arp_123", provider="claude-code", cwd="/workspace", timeout=1800, prompt="Build the dashboard, run tests, and keep the preview live.", metadata={"artifact_paths": ["/workspace/report.html"]}, ) print(run["id"], run["status"], run.get("output", "")) artifacts = client.agent_runs.artifacts(run["id"]) report = client.agent_runs.download_artifact(run["id"], artifacts[0]["id"]) ``` ## CLI Use `miosa prompt` when choosing the target the run executes on: ```bash miosa prompt "Build the dashboard, run tests, and keep the preview live." \ --sandbox 41026070-9bb0-4d62-90b4-8ceeb0a131b6 \ --harness claude-code \ --max-time 1800 \ --json miosa prompt "Open the CRM and verify the lead import" \ --computer desktop-prod-1 \ --harness osa \ --json miosa api POST /opencomputers/hosts/host_abc123/agent/dispatch \ -d '{"task": "Audit the local browser workflow"}' miosa run files --json miosa run download --output ./report.html ``` Use `miosa agent run` when you have a saved agent to run with the standard harness: ```bash miosa agent run builder \ --sandbox 41026070-9bb0-4d62-90b4-8ceeb0a131b6 \ --json \ "Build the dashboard, run tests, and keep the preview live." ``` Use the API-level `command` field with `provider: "custom"` when you bring your own runtime harness. The CLI exposes the built-in providers first. ## Product guidance Render the run status, provider, target, prompt, output, and request id in your task stream so users can see what the agent did. The agent should write files and run commands inside the sandbox, not build locally and upload a finished result afterward. Store your workspace, user, project, artifact, or task ids in metadata so you can connect MIOSA runs to your product records. Store your own dispatch id in `metadata` when sending work from queues. The queryable run store will use the same response shape when retry-safe history lands. ## See also - [Agent orchestration](/docs/guides/agent-orchestration/) - [Backend orchestration](/docs/integrations/backend-orchestration/) - [Sandboxes API](/docs/api-reference/sandboxes/) - [Computers API](/docs/api-reference/computers/) - [CLI](/docs/cli/) --- # Agent Runtime Profiles URL: https://miosa.ai/docs/api-reference/agent-runtime-profiles Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/agent-runtime-profiles Source: src/routes/docs/api-reference/agent-runtime-profiles/+page.md Description: Configure reusable agent runtime defaults for sandboxes, computers, and custom agent sessions. # Agent Runtime Profiles Agent Runtime Profiles define how MIOSA should prepare an agent runtime for a tenant, workspace, or project. Use profiles to standardize OSA, Codex, Claude Code, Pi, Hermes, or custom runtimes across sandboxes and computers without passing the same model, tools, connectors, policy, and environment on every run. Base path: `/api/v1/agent-runtime-profiles` Profiles configure runtime shape. Agent Runs still provide the prompt, target, timeout, and task-specific metadata. ## Endpoints | Method | Path | Description | |---|---|---| | `GET` | `/api/v1/agent-runtime-profiles` | List profiles for the tenant | | `POST` | `/api/v1/agent-runtime-profiles` | Create a profile | | `GET` | `/api/v1/agent-runtime-profiles/{id}` | Fetch one profile | | `PUT` | `/api/v1/agent-runtime-profiles/{id}` | Update a profile | | `DELETE` | `/api/v1/agent-runtime-profiles/{id}` | Delete a profile | List can be filtered with `workspace_id` or `project_id`. ## Create ```http POST /api/v1/agent-runtime-profiles Authorization: Bearer msk_u_... Content-Type: application/json ``` ```json { "name": "lumen-claude", "runtime": "claude-code", "workspace_id": "ws_123", "project_id": "proj_123", "description": "Default Lumen coding and browser QA runtime", "applies_to": { "resources": ["sandbox", "computer"] }, "tools": ["filesystem", "shell", "browser", "artifacts"], "connectors": ["anthropic/lumen", "refero/managed"], "env": { "ANTHROPIC_MODEL": "claude-sonnet-4.6" }, "policy": { "approval_required_for": ["deploy", "external_write"], "max_child_runs": 25 }, "metadata": { "owner": "platform" }, "is_default": true } ``` ## Fields | Field | Description | |---|---| | `name` | Human-readable profile name. Unique within the relevant tenant/workspace/project scope. | | `runtime` | One of `osa`, `codex`, `claude`, `claude-code`, `pi`, `hermes`, or `custom`. | | `workspace_id` | Optional workspace scope. | | `project_id` | Optional project scope. | | `description` | Operator-facing explanation. | | `applies_to` | Resource selector, commonly `{ "resources": ["sandbox", "computer"] }`. | | `tools` | Tool ids the runtime should expect, such as `filesystem`, `shell`, `browser`, `artifacts`, or `deployments`. | | `connectors` | Connector UIDs to bind when the target runtime is created. | | `env` | Non-secret runtime defaults. Put provider keys in runtime env/secrets, not here. | | `policy` | Product policy such as approvals, max child runs, or external-write rules. | | `metadata` | Product-specific labels and ids. | | `is_default` | Whether MIOSA should resolve this profile by default for the scope and resource family. | ## Resolution Agent Runs can pass `agent_runtime_profile_id` explicitly. If they do not, MIOSA resolves defaults by scope and target family: ```text explicit run profile -> project default -> workspace default -> tenant default -> no profile ``` The profile can be applied when creating or running sandboxes, computers, and OpenComputer/BYOC agent sessions. Request-level `env` values override non-secret profile env values for that single run. ## CLI Agent Runtime Profiles have no dedicated `miosa` subcommand; drive them with `miosa api` against the `/agent-runtime-profiles` endpoint. ```bash miosa api GET /agent-runtime-profiles?workspace_id=ws_123 miosa api POST /agent-runtime-profiles -d '{ "name": "lumen-claude", "runtime": "claude-code", "workspace_id": "ws_123", "project_id": "proj_123", "tools": ["filesystem", "shell", "browser", "artifacts"], "connectors": ["anthropic/lumen", "refero/managed"], "env": {"ANTHROPIC_MODEL": "claude-sonnet-4.6"}, "policy": {"approval_required_for": ["deploy", "external_write"]}, "is_default": true }' miosa api PUT /agent-runtime-profiles/arp_123 -d '{"runtime": "codex"}' miosa api DELETE /agent-runtime-profiles/arp_123 ``` ## Next Store inherited environment variables for tenant, workspace, project, and target scopes. Dispatch prompts into a sandbox, computer, or custom runtime. Use runtime profiles with groups, events, artifacts, and multi-agent fanout. --- # Audit Log API URL: https://miosa.ai/docs/api-reference/audit-log Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/audit-log Source: src/routes/docs/api-reference/audit-log/+page.md Description: Retrieve a cursor-paginated audit log of all actions taken within your tenant. The audit log records all significant actions performed within your tenant - resource creation and deletion, API key usage, settings changes, and more. Results are cursor-paginated and ordered newest-first. Base path: `/api/v1/audit-log` Audit events are immutable. There is no write API. The log is append-only. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/audit-log` | Fetch recent audit events | --- ## List Audit Events **`GET /api/v1/audit-log`** ### Auth ``` Authorization: Bearer msk_... ``` ### Query Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `limit` | integer | No | `50` | Events per page (max `100`) | | `after` | string | No | - | Cursor from previous response's `next_cursor` | | `type` | string | No | - | Filter by event type prefix (e.g. `sandbox.`) | | `actor_id` | string | No | - | Filter by actor user ID | | `resource_type` | string | No | - | Filter by resource type (e.g. `sandbox`) | | `resource_id` | string | No | - | Filter by resource ID | ### Response - `200 OK` ```json { "data": [ { "id": "evt_01hwxyz...", "type": "sandbox.created", "actor": { "type": "api_key", "id": "user_42" }, "resource": { "type": "sandbox", "id": "sbx_abc123" }, "ts": "2026-05-26T10:14:23Z", "metadata": { "image": "debian-12-sandbox-v8", "workspace_id": "ws_01hwxyz..." } } ], "next_cursor": "MjAyNi0wNS0yNlQxMDoxNDoyM1o" } ``` | Field | Type | Description | |-------|------|-------------| | `data[].id` | string | Unique event ID | | `data[].type` | string | Dot-namespaced event type | | `data[].actor.type` | string | `"user"` or `"api_key"` | | `data[].actor.id` | string | User ID or `external_user_id` for API key actors | | `data[].resource.type` | string | Resource type (e.g. `sandbox`, `computer`, `webhook`) | | `data[].resource.id` | string | Resource ID | | `data[].ts` | string | ISO 8601 timestamp | | `data[].metadata` | object | Event-specific detail fields | | `next_cursor` | string | Opaque cursor; omitted when no more pages | ### Common Event Types | Type | Trigger | |------|---------| | `sandbox.created` | Sandbox provisioned | | `sandbox.destroyed` | Sandbox deleted | | `sandbox.exec` | `POST /exec` called | | `computer.created` | Computer provisioned | | `computer.stopped` | Computer stopped | | `computer.deleted` | Computer deleted | | `snapshot.created` | Snapshot initiated | | `api_key.created` | API key minted | | `api_key.deleted` | API key revoked | | `tenant.preview_domain.set` | Preview domain configured | | `webhook.created` | Outgoing webhook created | | `quota.upserted` | Per-user quota set | | `settings.updated` | Tenant settings changed | Use the `type` filter with a prefix to narrow events (e.g. `type=sandbox.` returns all sandbox events). --- ## Pagination Audit events are returned newest-first. To page forward: 1. Issue the initial request without `after`. 2. If `next_cursor` is present, re-issue with `after={next_cursor}`. 3. Stop when `next_cursor` is absent. Cursors are opaque base64url strings encoding a timestamp. Do not parse or construct them. --- ## Errors | Status | Code | Cause | |--------|------|-------| | 400 | - | Invalid cursor format | --- ## Examples ```bash # First page curl "https://api.miosa.ai/api/v1/audit-log?limit=50&type=sandbox." \ -H "Authorization: Bearer msk_u_..." # Next page curl "https://api.miosa.ai/api/v1/audit-log?limit=50&after=MjAyNi0wNS0yNlQxMDoxNDoyM1o" \ -H "Authorization: Bearer msk_u_..." # Filter by resource curl "https://api.miosa.ai/api/v1/audit-log?resource_type=computer&resource_id=cmp_xyz789" \ -H "Authorization: Bearer msk_u_..." ``` ```python client = miosa.Miosa() # Page through all sandbox events in the last page cursor = None while True: page = client.audit_log.list( type="sandbox.", limit=100, after=cursor, ) for event in page.data: print(event.type, event.resource.id, event.ts) if not page.next_cursor: break cursor = page.next_cursor ``` ```typescript const client = new Miosa(); let cursor: string | undefined; do { const page = await client.auditLog.list({ type: 'sandbox.', limit: 100, after: cursor, }); for (const event of page.data) { console.log(event.type, event.resource.id, event.ts); } cursor = page.nextCursor; } while (cursor); ``` --- # Cloud and BYOC API URL: https://miosa.ai/docs/api-reference/cloud Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/cloud Source: src/routes/docs/api-reference/cloud/+page.md Description: Organization-scoped AWS and GCP accounts, regions, pools, nodes, preflight, and host provisioning. Base path: `/api/v1/cloud` All resources belong to the active organization, which is returned as both `organization_id` and the compatibility field `tenant_id`. Cloud reads require `cloud:read` and mutations require `cloud:write`. Organization members can read inventory. Organization owners and admins can mutate it. Workspace-scoped credentials cannot mutate organization cloud capacity. AWS and GCP BYOC are gated private previews. Provider connection and preflight do not make workload placement selectable. Each exact organization, region, provider, and workload combination needs a current verification receipt from a real accepted host. ## Accounts - `GET /cloud/accounts` lists organization cloud accounts. - `POST /cloud/accounts` creates an AWS or GCP customer BYOC trust boundary. - `POST /cloud/accounts/{id}/aws-role` attaches an AWS AssumeRole ARN and generated external ID. - `POST /cloud/accounts/{id}/gcp-workload-identity` attaches the GCP project, customer actuator service account, and separate least-privilege worker service account without storing a service-account key. ## Regions - `GET /cloud/regions?cloud_account_id={id}` lists configured regions. - `POST /cloud/regions` records provider region, zone, network, host image, and exact immutable artifact configuration. - `PATCH /cloud/regions/{id}` updates that configuration and clears stale preflight readiness. Every launchable region must provide an HTTPS manifest URI, lowercase SHA-256 digest, version bound into the URI, and positive artifact generation. ## Pools - `GET /cloud/pools?cloud_region_id={id}` lists desired host pools. - `POST /cloud/pools` creates a pool configuration. - `PATCH /cloud/pools/{id}` updates organization capacity and placement guardrails. - `POST /cloud/pools/{pool_id}/provision` explicitly launches between 1 and 50 hosts. ```json { "count": 1 } ``` Provisioning returns one result per requested host. A successful launch starts in `provider_pending` and is not schedulable until host acceptance passes. ## Nodes - `GET /cloud/nodes?cloud_pool_id={id}` lists provider and acceptance state for a pool. Important node states include `provider_pending`, `acceptance_running`, `active`, `draining`, `terminated`, and `failed`. Only `active` nodes with a durable organization cell and accepted host session are eligible for placement. ## Preflight - `GET /cloud/preflights` lists account and region preflight evidence. - `POST /cloud/preflights` runs server-side account or region verification and records immutable evidence. Preflight and host acceptance are different gates. Preflight validates AWS or GCP identity, real provider API access, zone and machine availability, configured network and image references, and the immutable artifact contract before launch. Host acceptance validates the actual launched machine and promoted MIOSA runtime before placement. Sandbox, Computer, and Deployment placement are certified separately because one working runtime does not prove another workload contract. ## SDK ```typescript const result = await miosa.cloud.provisionPool(poolId, 1); const nodes = await miosa.cloud.listNodes({ cloudPoolId: poolId }); ``` ```python result = client.cloud.provision_pool(pool_id, count=1) nodes = client.cloud.list_nodes(cloud_pool_id=pool_id) ``` Sandbox creation does not take a pool ID. An active BYOC organization is automatically and exclusively placed into its configured scheduling cell. --- # Computers API URL: https://miosa.ai/docs/api-reference/computers Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/computers Source: src/routes/docs/api-reference/computers/+page.md Description: API reference for creating, managing, and controlling MIOSA computers - full lifecycle, desktop control, files, metrics, and env vars. Computers are persistent desktop VMs with a Linux desktop, streamed desktop access, and APIs for exec, files, apps, metrics, and desktop control. Base path: `/api/v1/computers`. Verbs supported: **GET** (list/get/config), **POST** (create/start/stop/restart/resize), **PATCH** (update), **PUT** (update), **DELETE** (delete). Selected mutation endpoints (sandbox templates and migration) accept an `Idempotency-Key` header. Use a UUID v4. Repeating the same key within 24 hours returns the original response without re-executing the mutation. Rate limits vary by key, tenant policy, and endpoint family. Exceeding the limit returns HTTP 429 with a `Retry-After` header; honor it when you back off. --- ## Create a Computer **`POST /api/v1/computers`** Creates a new computer and begins provisioning. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Display name | | `template_type` | string | Yes | Template (`"miosa-desktop"`) | | `size` | string | No | `"small"` (4GB/1CPU), `"medium"` (8GB/2CPU), `"large"` (16GB/4CPU). Default: `"small"` | | `selected_apps` | string[] | No | Applications to install after boot | | `workspace_id` | UUID | No | Workspace to assign this computer to. Defaults to the tenant's default workspace. | | `workspace_slug` | string | No | Existing or auto-created workspace slug. | | `workspace_name` | string | No | Workspace display name if auto-created. | | `project_id` | UUID | No | Project to assign this computer to. Defaults to the workspace default project. | | `project_slug` | string | No | Existing or auto-created project slug. | | `project_name` | string | No | Project display name if auto-created. | | `external_workspace_id` | string | No | Your own customer/account/workspace ID. | | `external_user_id` | string | No | Your own end-user ID. | | `external_project_id` | string | No | Your own project/app/document ID. | | `agent_runtime_profile_id` | UUID/string | No | Force a specific agent runtime profile for this computer. Alias: `agent_profile_id`. | | `skip_agent_runtime_profile` | boolean | No | If `true`, do not apply the tenant/workspace default runtime profile. | If a default agent runtime profile exists for the workspace or tenant and it applies to computers, MIOSA applies it automatically. Profile env vars are stored in the encrypted computer env store and synced into the VM after OSA is healthy. Profile connectors are bound through MIOSA egress as opaque `miosa-tok-*` placeholder tokens. ### Response - `201 Created` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "tenant_id": "...", "owner_user_id": "...", "name": "my-computer", "slug": "my-computer", "template_type": "miosa-desktop", "size": "small", "vcpus": 1, "memory_mb": 4096, "status": "provisioning", "vm_id": null, "workspace_id": "660e8400-e29b-41d4-a716-446655440001", "project_id": "770e8400-e29b-41d4-a716-446655440002", "external_workspace_id": null, "external_user_id": null, "external_project_id": null, "sandbox_url": "https://my-computer.sandbox.miosa.ai", "desktop_url": "https://my-computer.computer.miosa.ai/desktop/index.html", "selected_apps": [], "settings": {}, "ai_config": {}, "agent_session_id": null, "agent_status": null, "resolution": "1280x720", "auto_stop": 0, "created_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } ``` `sandbox_url` is a legacy preview field on the Computer response. Use `desktop_url` for the desktop stream and the `/computers/{id}/urls` endpoint for short-lived desktop and terminal URLs. ### Errors | Status | Error | Cause | |--------|-------|-------| | 400 | `name is required` | Missing name or template_type | | 402 | `INSUFFICIENT_CREDITS` | Not enough credits to create | | 404 | `AGENT_RUNTIME_PROFILE_NOT_FOUND` | Explicit profile ID does not exist in this tenant | | 422 | `TENANT_RESOLUTION_FAILED` | Cannot determine tenant for user | | 422 | `AGENT_RUNTIME_PROFILE_NOT_APPLICABLE` | Explicit profile does not apply to computers | ```bash curl -X POST https://api.miosa.ai/api/v1/computers \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "my-computer", "template_type": "miosa-desktop", "size": "small", "workspace_slug": "acme-corp", "workspace_name": "Acme Corp", "project_slug": "records-portal", "project_name": "Records Portal", "external_workspace_id": "acct_123", "external_user_id": "user-456", "external_project_id": "project_789", "agent_runtime_profile_id": "9f4b3d8e-0000-0000-0000-000000000000" }' ``` --- ## List Computers **`GET /api/v1/computers`** Returns all computers belonging to the authenticated user's tenant. ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `workspace_id` | UUID | Filter computers to a specific workspace. Omit to return computers across all workspaces. | | `project_id` | UUID | Filter computers to a specific project. | | `external_workspace_id` | string | Filter by your customer/account/workspace ID. | | `external_user_id` | string | Filter by your end-user ID. | | `external_project_id` | string | Filter by your project/app/document ID. | ### Response - `200 OK` ```json { "computers": [ { "id": "...", "name": "my-computer", "status": "running", "size": "small", "created_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash # All computers curl https://api.miosa.ai/api/v1/computers \ -H "Authorization: Bearer $MIOSA_API_KEY" # Filtered to a workspace curl "https://api.miosa.ai/api/v1/computers?workspace_id=660e8400-e29b-41d4-a716-446655440001" \ -H "Authorization: Bearer $MIOSA_API_KEY" # Filtered to a project curl "https://api.miosa.ai/api/v1/computers?project_id=770e8400-e29b-41d4-a716-446655440002" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get a Computer **`GET /api/v1/computers/{id}`** ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Computer ID | ### Response - `200 OK` Full computer object (same as create response, with current status). ### Errors | Status | Error | Cause | |--------|-------|-------| | 400 | `invalid computer id` | Not a valid UUID | | 404 | `computer not found` | Does not exist or belongs to different tenant | ```bash curl https://api.miosa.ai/api/v1/computers/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update a Computer **`PATCH /api/v1/computers/{id}`** Currently supports updating `agent_session_id` only. ### Request Body | Field | Type | Description | |-------|------|-------------| | `agent_session_id` | string | Link an AI agent session | ### Response - `200 OK` Updated computer object. ```bash curl -X PATCH https://api.miosa.ai/api/v1/computers/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"agent_session_id": "session-uuid"}' ``` --- ## Delete a Computer **`DELETE /api/v1/computers/{id}`** Permanently destroys the VM and removes the computer record. ### Response - `202 Accepted` Returns the computer object; its `state` reflects the destroy in progress. ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `not a member of this computer` | No access to this computer | | 404 | `computer not found` | Does not exist | ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Start a Computer **`POST /api/v1/computers/{id}/start`** Resumes a stopped or paused computer. ### Response - `200 OK` Updated computer object with `status: "running"`. ### Errors | Status | Error | Cause | |--------|-------|-------| | 402 | `INSUFFICIENT_CREDITS` | Not enough credits | | 409 | `computer cannot be started from its current status` | Not in a startable state | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/start \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Stop a Computer **`POST /api/v1/computers/{id}/stop`** Pauses a running computer. Can be restarted later. ### Response - `200 OK` Updated computer object with `status: "stopped"`. ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `computer cannot be stopped from its current status` | Not running | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/stop \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Restart a Computer **`POST /api/v1/computers/{id}/restart`** Stops and immediately restarts a running computer. ### Response - `200 OK` Updated computer object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `computer must be running to restart` | Not running | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/restart \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Auto-Stop Configuration **`GET /api/v1/computers/{id}/auto-stop`** ### Response - `200 OK` ```json { "auto_stop_seconds": 3600, "enabled": true } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/auto-stop \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update Auto-Stop Configuration **`PATCH /api/v1/computers/{id}/auto-stop`** ### Request Body | Field | Type | Description | |-------|------|-------------| | `seconds` | integer | Seconds of inactivity before auto-stop. `0` to disable. | ### Response - `200 OK` Updated auto-stop configuration. ```bash curl -X PATCH https://api.miosa.ai/api/v1/computers/{id}/auto-stop \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"seconds": 3600}' ``` --- ## Get Desktop Stream Credentials **`GET /api/v1/computers/{id}/vnc-credentials`** Returns credentials for direct desktop streaming access. ### Response - `200 OK` ```json { "desktop_url": "https://my-computer.computer.miosa.ai/desktop/index.html", "ws_url": "wss://my-computer.computer.miosa.ai/ws/vnc/550e8400-e29b-41d4-a716-446655440000?auth=stream_token", "token": "stream_auth_token", "expires_at": 1712700060, "computer_id": "550e8400-e29b-41d4-a716-446655440000", "slug": "my-computer" } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/vnc-credentials \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Stream Token **`POST /api/v1/computers/{id}/stream-token`** Returns a time-limited auth token for desktop and terminal WebSocket connections. ### Response - `200 OK` ```json { "token": "stream_auth_token", "expires_at": 1712700060 } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/stream-token \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Computer URLs **`GET /api/v1/computers/{id}/urls`** Returns all connection URLs for the computer. ### Response - `200 OK` ```json { "desktop_url": "https://my-computer.computer.miosa.ai/desktop/index.html", "stream_url": "https://my-computer.computer.miosa.ai/desktop/index.html", "terminal_url": "wss://my-computer.computer.miosa.ai/ws/terminal/550e8400-e29b-41d4-a716-446655440000", "computer_id": "550e8400-e29b-41d4-a716-446655440000" } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/urls \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## List Installed Apps **`GET /api/v1/computers/{id}/apps`** Returns the list of applications installed on the computer. ### Response - `200 OK` Response containing the list of installed applications. ```bash curl https://api.miosa.ai/api/v1/computers/{id}/apps \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Resize a Computer **`POST /api/v1/computers/{id}/resize`** Changes the vCPU, memory, and disk allocation. A running computer is resized in place. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `cpu` | integer | No | Target vCPU count | | `memory_mb` | integer | No | Target memory in MiB | | `disk_gb` | integer | No | Target disk in GiB | ### Response - `200 OK` Updated computer object with the new allocation. ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/resize \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"cpu": 2, "memory_mb": 8192, "disk_gb": 32}' ``` --- ## Move a Computer **`POST /api/v1/computers/{id}/move`** Moves a computer to a different workspace in the same organization. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `workspace_id` | string | Yes | Target workspace ID | ### Response - `200 OK` Updated computer object with the new `workspace_id`. ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/move \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id": "550e8400-e29b-41d4-a716-446655440001"}' ``` --- ## Get Metrics **`GET /api/v1/computers/{id}/metrics`** Returns time-series CPU, RAM, and credit-burn metrics. Useful for dashboards and auto-stop logic. ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `window` | string | Time window: `"1h"` (default), `"24h"`, `"7d"` | ### Response - `200 OK` ```json { "cpu_percent": [ {"ts": "2026-05-17T10:00:00Z", "value": 12.4}, {"ts": "2026-05-17T10:01:00Z", "value": 8.1} ], "memory_percent": [ {"ts": "2026-05-17T10:00:00Z", "value": 45.2} ], "credits_per_hour": 3 } ``` ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/metrics?window=1h" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Env Vars Encrypted per-computer environment variables. Values are decrypted at boot and injected into the VM environment. ### List **`GET /api/v1/computers/{id}/env`** ```bash curl https://api.miosa.ai/api/v1/computers/{id}/env \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ### Create **`POST /api/v1/computers/{id}/env`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Variable name (e.g. `DATABASE_URL`) | | `value` | string | Yes | Plaintext value - encrypted at rest | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/env \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "DATABASE_URL", "value": "postgres://user:pass@host/db"}' ``` ### Update **`PATCH /api/v1/computers/{id}/env/{name}`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `value` | string | Yes | New plaintext value | ### Delete **`DELETE /api/v1/computers/{id}/env/{name}`** Returns `200 OK`. --- ## Port Exposure Control per-port visibility for services running inside the computer. ### List Ports **`GET /api/v1/computers/{id}/ports`** ### Expose a Port **`POST /api/v1/computers/{id}/ports`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `port` | integer | Yes | Port number (1-65535) | | `visibility` | string | Yes | `"public"`, `"private"`, or `"protected"` | ### Update Port Visibility **`PATCH /api/v1/computers/{id}/ports/{port}`** ### Remove Port **`DELETE /api/v1/computers/{id}/ports/{port}`** ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/ports \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"port": 3000, "visibility": "public"}' ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 400 | `invalid computer id` | Path parameter is not a valid UUID | | 402 | `INSUFFICIENT_CREDITS` | Not enough credits to start or create | | 403 | `not a member of this computer` | No access to this computer | | 404 | `computer not found` | Does not exist or belongs to a different tenant | | 409 | `COMPUTER_NOT_RUNNING` | Operation requires a running computer | | 502 | `AGENT_UNAVAILABLE` | Computer command service unavailable | --- ## See also - [Desktop API](/docs/api-reference/desktop/) - screenshot, click, type, scroll, drag, windows - [Files API](/docs/api-reference/files/) - read, write, stat, mkdir, rename, copy, chmod, download - [Exec API](/docs/api-reference/exec/) - bash and Python execution - [Snapshots API](/docs/api-reference/snapshots/) - create, restore, and delete checkpoints - [Events (SSE)](/docs/api-reference/events/) - computer lifecycle event stream - [Regions](/docs/api-reference/regions/) - available regions for computer placement --- # Credits API URL: https://miosa.ai/docs/api-reference/credits Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/credits Source: src/routes/docs/api-reference/credits/+page.md Description: API reference for checking credit balance, transaction history, and usage data. MIOSA uses a credit-based billing system. Credits are consumed for compute time and AI API calls. Base path: `/api/v1/credits` --- ## Get Balance **`GET /api/v1/credits/balance`** Returns the current credit balance for your tenant. ### Response - `200 OK` ```json { "tenant_id": "550e8400-e29b-41d4-a716-446655440000", "balance_credits": 850, "lifetime_earned": 1000, "lifetime_spent": 150, "credit_expiry_at": "2026-10-08T00:00:00Z", "updated_at": "2026-04-11T15:00:00Z" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `tenant_id` | UUID | Your tenant ID | | `balance_credits` | integer | Current available credits | | `lifetime_earned` | integer | Total credits ever earned (purchases + promos) | | `lifetime_spent` | integer | Total credits ever spent | | `credit_expiry_at` | ISO 8601 | Nearest credit-lot expiry, or `null` when no granted credits are due to expire. Purchased credits do not expire; see [Credit expiry](#credit-expiry) | | `updated_at` | ISO 8601 | Last balance change | If no credit balance record exists yet, returns zeros: ```json { "tenant_id": "...", "balance_credits": 0, "lifetime_earned": 0, "lifetime_spent": 0, "credit_expiry_at": null, "updated_at": null } ``` ```bash curl https://api.miosa.ai/api/v1/credits/balance \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Transactions **`GET /api/v1/credits/transactions`** Returns paginated credit transaction history. ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page` | integer | 1 | Page number (1-based) | | `page_size` | integer | 20 | Items per page (max: 100) | ### Response - `200 OK` ```json { "transactions": [ { "id": "uuid", "tenant_id": "uuid", "user_id": "uuid", "computer_id": "uuid", "type": "spend", "amount_credits": -5, "description": "LLM API call", "provider": "ollama", "model": "nemotron-3-super", "input_tokens": 1500, "output_tokens": 500, "cost_usd_micros": 250, "inserted_at": "2026-04-11T15:00:00Z" }, { "id": "uuid", "tenant_id": "uuid", "user_id": "uuid", "computer_id": null, "type": "earn", "amount_credits": 1000, "description": "Business plan monthly credits", "provider": null, "model": null, "input_tokens": null, "output_tokens": null, "cost_usd_micros": null, "inserted_at": "2026-04-01T00:00:00Z" } ], "total": 47, "page": 1, "page_size": 20 } ``` ### Transaction Fields | Field | Type | Description | |-------|------|-------------| | `id` | UUID | Transaction ID | | `tenant_id` | UUID | Tenant ID | | `user_id` | UUID | User who triggered the transaction | | `computer_id` | UUID | Associated computer (if applicable) | | `type` | string | `"earn"` or `"spend"` | | `amount_credits` | integer | Credits (positive = earned, negative = spent) | | `description` | string | Human-readable description | | `provider` | string | LLM provider (for AI transactions) | | `model` | string | Model name (for AI transactions) | | `input_tokens` | integer | Input tokens used (for AI transactions) | | `output_tokens` | integer | Output tokens generated (for AI transactions) | | `cost_usd_micros` | integer | Cost in microdollars (1/1,000,000 USD) | | `inserted_at` | ISO 8601 | Transaction timestamp | ```bash curl "https://api.miosa.ai/api/v1/credits/transactions?page=1&page_size=50" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Usage Summary **`GET /api/v1/credits/usage`** Returns daily usage rollups grouped by provider and model. ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `from` | ISO 8601 | 30 days ago | Start of date range | | `to` | ISO 8601 | Now | End of date range | ### Response - `200 OK` ```json { "usage": [ { "day": "2026-04-11T00:00:00Z", "provider": "ollama", "model": "nemotron-3-super", "credits_spent": 45, "input_tokens": 15000, "output_tokens": 5000, "cost_usd_micros": 2250, "call_count": 12 }, { "day": "2026-04-10T00:00:00Z", "provider": "ollama", "model": "nemotron-3-super", "credits_spent": 30, "input_tokens": 10000, "output_tokens": 3000, "cost_usd_micros": 1500, "call_count": 8 } ], "from": "2026-03-12T00:00:00Z", "to": "2026-04-11T15:00:00Z" } ``` ### Usage Entry Fields | Field | Type | Description | |-------|------|-------------| | `day` | ISO 8601 | Date (truncated to day) | | `provider` | string | LLM provider | | `model` | string | Model name | | `credits_spent` | integer | Total credits spent that day | | `input_tokens` | integer | Total input tokens | | `output_tokens` | integer | Total output tokens | | `cost_usd_micros` | integer | Total cost in microdollars | | `call_count` | integer | Number of API calls | ### Errors | Status | Error | Cause | |--------|-------|-------| | 400 | `invalid 'from' timestamp` | Not valid RFC 3339 | | 400 | `invalid 'to' timestamp` | Not valid RFC 3339 | | 400 | `'from' must be before 'to'` | Invalid date range | ```bash curl "https://api.miosa.ai/api/v1/credits/usage?from=2026-04-01T00:00:00Z&to=2026-04-11T23:59:59Z" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Credit Pricing MIOSA prices sandboxes and computers per size: every size has one hourly price, billed per second while the machine runs, and the price does not change with how busy the machine is. Apps and databases are billed on the CPU and memory they use, and never more than the price of their size. One credit always equals one US cent. There are no per-product prices and no plan discounts. ### Size prices | Size | vCPU | RAM | Price / hour | Always on (730 h) | |------|------|-----|--------------|-------------------| | `micro` | 1 | 0.5 GiB | $0.0316 | $23.05 | | `tiny` | 1 | 1 GiB | $0.0359 | $26.22 | | `xs` | 1 | 2 GiB | $0.0446 | $32.58 | | `small` | 2 | 4 GiB | $0.0893 | $65.16 | | `quad` | 4 | 4 GiB | $0.1437 | $104.90 | | `medium` | 4 | 8 GiB | $0.1785 | $130.32 | | `large` | 8 | 16 GiB | $0.3570 | $260.65 | | `xl` | 16 | 32 GiB | $0.7141 | $521.29 | A size's price is $0.027216 per vCPU-hour plus $0.0087075 per GiB-hour of RAM, so a shape that is not in the table (for example 2 vCPU / 8 GiB) costs the same formula. `GET /api/v1/pricing` returns every size with `usd_per_hour` and `usd_per_month`. ### Other rates | Resource | Rate | |----------|------| | Disk (running; also volumes) | $0.09 / GiB-month (about 0.0123 credits / GiB-hour) | | Data at rest (paused disk, snapshots, backups, run artifacts) | $0.018 / GiB-month (about 0.00247 credits / GiB-hour) | | Object storage (buckets, static sites) | $0.0162 / GiB-month (about 0.00222 credits / GiB-hour) | | Network egress | 5.4 credits / GB (no free allowance) | | Builds | Free | | Optimal tokens | $1.50 / 1M input, $6.00 / 1M output | **Billing basis.** A sandbox or computer is billed the price of its size, metered per second with a 1-second minimum, and settled against your credit balance on a 5-minute tick. A stopped or paused machine is not billed for compute. Memory and disk are GiB (1024-based). ### Plans | Plan | Price | Included credits | |------|-------|------------------| | Free | $0/month | $1 of usage each month (100 credits) | | Developer | $20/month | 2,000 credits/month | | Business | $500/month | 50,000 credits/month | | Enterprise | $2,000/month | 200,000 credits/month | Every plan pays the same price for a size. Plans differ only in caps and concurrency, never in price. Free includes $1 of usage each month; add credits any time. Pay only for what you use. See the [pricing page](/pricing) for the live numbers and the cost calculator. ### Credit expiry Expiry depends on how the credits were granted: - **Credits you add** do not expire. - **Recurring plan credits** expire at the start of the UTC day after the 30-day billing period in which they were granted. - **Promotional credits** (the one-time welcome grant and promo-code redemptions) expire 180 days after they are granted. The nearest expiry is reported as `credit_expiry_at` on `GET /api/v1/credits/balance`. See [Pricing and limits](/docs/pricing/) for how usage is metered and billed. --- ## See also - [API Reference overview](/docs/api-reference/) - base URL and authentication - [Error Codes](/docs/api-reference/errors/) - `INSUFFICIENT_CREDITS` error handling - [Workspaces](/docs/api-reference/workspaces/) - workspace and tenant management --- # Custom Domains API URL: https://miosa.ai/docs/api-reference/custom-domains Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/custom-domains Source: src/routes/docs/api-reference/custom-domains/+page.md Description: API reference for mapping tenant-owned FQDNs to MIOSA previews with automatic TLS. Custom domains let you expose MIOSA previews, computers, and deployments under tenant-owned FQDNs. There are four related surfaces: - **Computer custom domains** map one fully qualified domain, such as `app.yourdomain.com`, to one Computer service. - **Deployment custom domains** map one fully qualified domain, such as `program.example.net`, to one Deployment. - **Tenant preview domains** white-label generated preview links for the whole organization. After configuration, sandbox `/expose` responses use URLs such as `https://5173-sbx01j9x.sandbox.example.com`. MIOSA handles verification and automatic TLS certificate issuance via Caddy on-demand TLS. Nested base paths: `/api/v1/computers/{id}/domains` and `/api/v1/deployments/{id}/domains`. Flat frontend-compatible base path: `/api/v1/custom-domains`. Custom domains must not end in `miosa.ai` - that namespace is reserved for platform subdomains. Use a domain you control. --- ## Quick Start ```typescript const client = new Miosa(); // 1. Register the domain const domain = await client.domains.register(computerId, { domain: 'app.yourcompany.com', }); console.log(domain.verificationTarget); // e.g. "verify.miosa.ai" // 2. Add a CNAME record at your DNS provider: // app.yourcompany.com → verify.miosa.ai // 3. Trigger verification const verified = await client.domains.verify(computerId, domain.id); console.log(verified.status); // "verified" or "failed" ``` ```bash # Register curl -X POST https://api.miosa.ai/api/v1/computers/{id}/domains \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "app.yourcompany.com"}' ``` --- ## Endpoints ### Computer custom domains | Method | Path | Description | |--------|------|-------------| | `POST` | `/computers/{id}/domains` | Register a custom domain | | `GET` | `/computers/{id}/domains` | List domains for a computer | | `POST` | `/computers/{id}/domains/{domain_id}/verify` | Trigger DNS verification | | `DELETE` | `/computers/{id}/domains/{domain_id}` | Remove a domain | ### Deployment custom domains | Method | Path | Description | |--------|------|-------------| | `POST` | `/deployments/{id}/domains` | Register a custom domain | | `GET` | `/deployments/{id}/domains` | List domains for a deployment | | `POST` | `/deployments/{id}/domains/{domain_id}/verify` | Trigger DNS verification | | `DELETE` | `/deployments/{id}/domains/{domain_id}` | Remove a domain | ### Flat custom domains | Method | Path | Description | |--------|------|-------------| | `GET` | `/custom-domains?computer_id={id}` | List domains for a computer | | `GET` | `/custom-domains?deployment_id={id}` | List domains for a deployment | | `POST` | `/custom-domains` | Register a domain with `computer_id` or `deployment_id` | | `DELETE` | `/custom-domains/{id}` | Remove a domain | ### Tenant preview domains | Method | Path | Description | |--------|------|-------------| | `GET` | `/tenant/preview-domain` | Read the tenant preview domain | | `PUT` | `/tenant/preview-domain` | Set the tenant preview domain | | `DELETE` | `/tenant/preview-domain` | Clear the tenant preview domain | | `POST` | `/tenant/preview-domain/verify` | Check DNS/TLS readiness | The preview domain is an organization setting. There is no workspace-level or project-level preview-domain endpoint. --- ## White-label Managed Links Use a tenant preview domain when generated app/artifact preview URLs should use your organization's domain instead of the MIOSA-managed fallback. ```bash curl -X PUT https://api.miosa.ai/api/v1/tenant/preview-domain \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"preview_domain":"preview.yourcompany.com"}' ``` Then expose a sandbox port: ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/expose \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"port":5173}' ``` Response: ```json { "url": "https://5173-sbx01j9x.sandbox.preview.yourcompany.com" } ``` For a single deployment under its own FQDN (not the shared preview domain), register a deployment custom domain instead: ```bash curl -X POST https://api.miosa.ai/api/v1/deployments/$DEPLOYMENT_ID/domains \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain":"program.example.net"}' ``` DNS must delegate wildcard traffic to the MIOSA preview router: | Record type | Name | Value | Used for | |---|---|---|---| | `CNAME` | `*` | `proxy.miosa.ai` | `https://{slug}.{domain}` managed deployment/computer/default preview URLs, and `https://{port}-{slug}.sandbox.{domain}` sandbox port previews | If no tenant preview domain is configured, MIOSA returns the managed fallback URL for that resource. This fallback remains available even after custom domains are attached. Exact deployment and computer custom domains always win over the organization-wide preview domain. --- ## Register a Domain **`POST /api/v1/computers/{id}/domains`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `domain` | string | Yes | Fully qualified domain name. RFC 1123 hostname, max 253 chars | ### Response - `201 Created` ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "computer_id": "...", "deployment_id": null, "tenant_id": "...", "workspace_id": "660e8400-e29b-41d4-a716-446655440001", "project_id": "770e8400-e29b-41d4-a716-446655440002", "fqdn": "app.yourcompany.com", "status": "pending", "verification_target": "verify.miosa.ai", "verified_at": null, "tls_issued_at": null, "external_workspace_id": "acct_123", "external_user_id": "user-456", "external_project_id": "project_789", "created_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } } ``` ### Status Values | Status | Description | |--------|-------------| | `pending` | Registered; awaiting DNS verification | | `verified` | CNAME confirmed; Caddy may issue a cert | | `active` | TLS certificate issued and in use | | `failed` | Verification or TLS issuance failed | | `removed` | Domain detached (soft marker before deletion) | ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `has already been taken` | FQDN already registered (globally unique) | | 422 | Validation error | Invalid FQDN format or ends with `miosa.ai` | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/domains \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "preview.yourapp.io"}' ``` ### Register a deployment domain ```bash curl -X POST https://api.miosa.ai/api/v1/deployments/{id}/domains \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "program.example.net"}' ``` Deployment domains inherit `workspace_id`, `project_id`, and external attribution from the deployment. --- ## List Domains **`GET /api/v1/computers/{id}/domains`** ### Response - `200 OK` ```json { "data": [ { "id": "...", "fqdn": "app.yourcompany.com", "status": "active", "verified_at": "2026-04-11T01:00:00Z", "tls_issued_at": "2026-04-11T01:05:00Z", "created_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/domains \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Verify a Domain **`POST /api/v1/computers/{id}/domains/{domain_id}/verify`** Checks that the CNAME record at `fqdn` resolves to `verification_target`. On success the domain transitions to `"verified"` and Caddy will issue a TLS certificate on the next HTTPS request to the FQDN. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Computer ID | | `domain_id` | UUID | Domain ID | ### Response - `200 OK` ```json { "data": { "id": "...", "fqdn": "app.yourcompany.com", "status": "verified", "verified_at": "2026-04-11T01:00:00Z" } } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `domain is already verified` | Already completed | | 422 | `CNAME not found` | DNS record missing or not yet propagated | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/domains/{domain_id}/verify \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Remove a Domain **`DELETE /api/v1/computers/{id}/domains/{domain_id}`** Detaches the domain from the computer. The certificate is not revoked immediately (Caddy lets it expire naturally), but the domain will no longer route to the computer. ### Response - `200 OK` ```json { "data": { "id": "...", "fqdn": "app.yourcompany.com", "status": "removed" } } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/domains/{domain_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## DNS Setup Reference After registering a domain, add the following DNS record at your registrar or DNS provider: | Record | Host | Value | |--------|------|-------| | CNAME | `app` (or full subdomain) | `verify.miosa.ai` | DNS propagation typically takes 1-60 minutes. Call `verify` once propagation is complete. If verification fails, wait a few minutes and retry - the endpoint is idempotent. --- ## Common Recipes ### Automate full domain onboarding ```python from miosa import Miosa client = Miosa() domain = client.domains.register(computer_id, fqdn="api.yourcompany.com") print(f"Add CNAME: {domain.fqdn} → {domain.verification_target}") print("Waiting for DNS propagation...") while True: result = client.domains.verify(computer_id, domain.id) if result.status == "verified": print("Domain verified! TLS will be issued on first HTTPS request.") break elif result.status == "failed": raise RuntimeError("Verification failed - check your DNS records.") time.sleep(30) ``` ### List all pending domains (potential stuck ACME issuances) ```typescript const computers = await client.computers.list(); for (const computer of computers.data) { const { data: domains } = await client.domains.list(computer.id); const pending = domains.filter(d => d.status === 'pending'); if (pending.length > 0) { console.log(`${computer.name}: ${pending.length} domain(s) pending verification`); } } ``` --- # Databases API URL: https://miosa.ai/docs/api-reference/databases Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/databases Source: src/routes/docs/api-reference/databases/+page.md Description: Provision and operate managed databases, backups, restores, branches, replicas, credentials, and placement. Managed databases have a lifecycle independent from sandboxes and deployment runtime instances. Deleting or replacing compute does not implicitly delete an attached database. Base path: `/api/v1/databases` The `managed-databases` alias is not available: use `/api/v1/databases`. ## Core lifecycle | Method | Path | Purpose | |---|---|---| | `GET` | `/databases` | List databases visible to the current tenant and workspace scope | | `POST` | `/databases` | Create a managed database | | `GET` | `/databases/{id}` | Inspect one database | | `DELETE` | `/databases/{id}` | Permanently delete one database | | `POST` | `/databases/{id}/start` | Start a stopped or recoverable database | | `POST` | `/databases/{id}/stop` | Stop a running database while preserving its volume | | `POST` | `/databases/{id}/restart` | Restart the database runtime | | `POST` | `/databases/{id}/wake` | Wake a database that can be resumed | | `POST` | `/databases/{id}/resize-compute` | Change its compute allocation | | `POST` | `/databases/{id}/move` | Move the database through the managed placement workflow | ## Data topology and recovery | Method | Path | Purpose | |---|---|---| | `POST` | `/databases/{id}/branch` | Create an independent database branch | | `POST` | `/databases/{id}/replicas` | Create a replica | | `POST` | `/databases/{id}/backups` | Create an on-demand backup | | `GET` | `/databases/{id}/backups` | List backups | | `GET` | `/databases/{id}/backups/{backup_id}` | Inspect one backup | | `POST` | `/databases/{id}/restore` | Restore from a selected backup | | `GET` | `/databases/{id}/restores` | List restore operations | | `GET` | `/databases/{id}/credentials` | Return the current connection contract | Logs and metrics use the common observability endpoints: ```http GET /api/v1/databases/{id}/logs GET /api/v1/databases/{id}/logs/stream GET /api/v1/databases/{id}/metrics ``` ## Create ```bash curl -X POST https://api.miosa.ai/api/v1/databases \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"app-production","engine":"postgresql"}' ``` Creation is asynchronous. Wait until the returned database reaches `running` before connecting. ```bash miosa api POST /databases -d '{"name":"app-production","engine":"postgresql"}' ``` ## Credentials ```bash curl https://api.miosa.ai/api/v1/databases/$DATABASE_ID/credentials \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` Use the returned recommended connection URL. Do not construct hostnames, ports, usernames, or database names from resource metadata. ## Backup and restore ```bash miosa db backup --json miosa api POST /databases//restore \ -d '{"backup_id": "", "allow_overwrite": true}' ``` Restore replaces the target database contents with the selected backup. Confirm the database ID and backup ID before approving the operation. Database restore does not change the active application release. Application rollback does not reverse database migrations. Use backward-compatible expand, migrate, and contract changes when releases must remain rollback-safe. ## Permissions | Action | Required scope | |---|---| | List, inspect, read metrics, or fetch credentials | `databases:read` | | Create, resize, branch, back up, restore, move, or delete | `databases:write` | ## See also Choose the correct managed data product and understand its lifecycle. Connect applications and manage schema changes safely. Bind separate databases to production, staging, and development. --- # API Reference / Deployments URL: https://miosa.ai/docs/api-reference/deployments Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/deployments Source: src/routes/docs/api-reference/deployments/+page.md Description: REST API for the Deployment resource - create, list, get, update, delete, publish, rollback. A **Deployment** is the stable production object for a published app/site/API. See [Deploy / Overview](/docs/docker-host/overview/) for the conceptual model. Use `POST /api/v1/sandboxes/:id/publish` or `miosa deploy create` to turn a tested sandbox into an immutable release. Pass an existing deployment ID when updating so its identity and URL remain stable. ## Endpoints ```http POST /api/v1/deployments GET /api/v1/deployments GET /api/v1/deployments/:id PATCH /api/v1/deployments/:id DELETE /api/v1/deployments/:id POST /api/v1/deployments/:id/publish POST /api/v1/deployments/:id/rollback POST /api/v1/sandboxes/:id/publish POST /api/v1/sandboxes/:id/deploy # Compatibility bridge ``` All endpoints require `Authorization: Bearer <msk_*>`. Mutations should include an `Idempotency-Key` header. ## Create ```http POST /api/v1/deployments ``` Body: ```json { "name": "Team Portal", "source_type": "sandbox", "workspace_slug": "acme-corp", "workspace_name": "Acme Corp", "project_slug": "team-portal", "project_name": "Team Portal", "external_workspace_id": "acct_123", "external_user_id": "user-456", "external_project_id": "project_789", "metadata": { } } ``` Ownership fields accepted on create: `workspace_id`, `workspace_slug`, `workspace_name`, `project_id`, `project_slug`, `project_name`, `external_workspace_id`, `external_user_id`, and `external_project_id`. Scopes: `deployments:write`. ## Publish ```http POST /api/v1/deployments/:id/publish ``` Body: ```json { "source_sandbox_id": "sbx_...", "kind": "auto", "environment": "production", "output_path": "/workspace", "build_command": "npm run build", "run_command": "npm start", "port": 3000, "health_check_path": "/_health", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "external_workspace_id": "acct_123" } ``` Server-side behavior is documented in [Publishing](/docs/docker-host/publishing/). Response: ```json { "data": { "deployment": { "id": "dep_...", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "active_version_id": "ver_...", "state": "running", "public_url": "https://team-portal.example-org.miosa.app" }, "version": { "id": "ver_...", "kind": "static", "state": "ready", "artifact_uri": "s3://...", "artifact_sha256": "...", "source_sha256": "..." }, "release": { "id": "rel_...", "state": "ready" }, "promoted": true } } ``` ## List ```http GET /api/v1/deployments GET /api/v1/deployments?workspace_id=550e8400-e29b-41d4-a716-446655440000 GET /api/v1/deployments?project_id=660e8400-e29b-41d4-a716-446655440001 GET /api/v1/deployments?external_workspace_id=acct_123 GET /api/v1/deployments?external_user_id=user-456 GET /api/v1/deployments?state=running ``` All filters are organization-scoped server-side. See [Ownership and Attribution](/docs/platform/attribution/). ## Get ```http GET /api/v1/deployments/:id ``` Returns the full deployment row including `workspace_id`, `project_id`, `active_version_id`, attribution, and metadata. ## Update ```http PATCH /api/v1/deployments/:id ``` Body: any subset of mutable fields (`name`, `auto_deploy`, `metadata`, `external_*`). ## Rollback ```http POST /api/v1/deployments/:id/rollback ``` Body: ```json { "version_id": "ver_..." } ``` See [Rollback](/docs/docker-host/rollback/). ## Delete ```http DELETE /api/v1/deployments/:id ``` Tears down runtime instances, removes routes, marks the deployment deleted. Versions and releases are retained per the retention policy. ## See also - [Versions](/docs/api-reference/versions/) - version sub-resource - [Releases](/docs/api-reference/releases/) - build artifact reference - [Custom Domains](/docs/api-reference/custom-domains/) - domain sub-resource - [Deploy / Overview](/docs/docker-host/overview/) - conceptual model --- # Desktop API URL: https://miosa.ai/docs/api-reference/desktop Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/desktop Source: src/routes/docs/api-reference/desktop/+page.md Description: API reference for all desktop control endpoints - screenshot, mouse, keyboard, clipboard, windows, accessibility tree. Desktop control endpoints proxy commands to the computer command service inside the computer VM via the MIOSA control plane. All coordinates are in screen pixels (0,0 = top-left). Base path: `/api/v1/computers/{id}/desktop` All desktop endpoints require the computer to be in `"running"` status. Returns `409 COMPUTER_NOT_RUNNING` if the computer is stopped or provisioning. Rate limits vary by key, tenant policy, and endpoint family. Honor `X-RateLimit-*` and `Retry-After` response headers. Individual desktop actions are fast but high-frequency AI agents should batch operations where possible. --- ## Screenshot **`GET /api/v1/computers/{id}/desktop/screenshot`** Captures the full desktop as a PNG image. ### Response - `200 OK` Binary PNG data with `Content-Type: image/png`. ```bash curl https://api.miosa.ai/api/v1/computers/{id}/desktop/screenshot \ -H "Authorization: Bearer $MIOSA_API_KEY" \ --output screenshot.png ``` --- ## Screenshot Region **`POST /api/v1/computers/{id}/desktop/screenshot/region`** Captures a rectangular region of the screen. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | Left edge X coordinate | | `y` | integer | Yes | Top edge Y coordinate | | `width` | integer | Yes | Width in pixels | | `height` | integer | Yes | Height in pixels | ### Response - `200 OK` Binary PNG data. ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/screenshot/region \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 0, "y": 0, "width": 800, "height": 600}' \ --output region.png ``` --- ## Click **`POST /api/v1/computers/{id}/desktop/click`** Performs a mouse click at the specified coordinates. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | X coordinate | | `y` | integer | Yes | Y coordinate | | `button` | string | No | `"left"` (default), `"right"`, `"middle"` | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/click \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 500, "y": 300}' ``` --- ## Double-Click **`POST /api/v1/computers/{id}/desktop/double-click`** Performs a double-click at the specified coordinates. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | X coordinate | | `y` | integer | Yes | Y coordinate | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/double-click \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 500, "y": 300}' ``` --- ## Type Text **`POST /api/v1/computers/{id}/desktop/type`** Types a string of text as keyboard input. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `text` | string | Yes | Text to type | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/type \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Hello, World!"}' ``` --- ## Key Press **`POST /api/v1/computers/{id}/desktop/key`** Presses a key or key combination. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `key` | string | Yes | Key name or combination | Key combinations use `+` as separator: `"ctrl+c"`, `"alt+Tab"`, `"ctrl+shift+t"`. ### Common Keys | Key | Value | |-----|-------| | Enter | `Return` | | Tab | `Tab` | | Escape | `Escape` | | Backspace | `BackSpace` | | Delete | `Delete` | | Space | `space` | | Arrows | `Up`, `Down`, `Left`, `Right` | | Function | `F1`...`F12` | | Home/End | `Home`, `End` | | Page Up/Down | `Page_Up`, `Page_Down` | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/key \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"key": "ctrl+c"}' ``` --- ## Scroll **`POST /api/v1/computers/{id}/desktop/scroll`** Scrolls the mouse wheel at the specified position. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | X coordinate | | `y` | integer | Yes | Y coordinate | | `direction` | string | Yes | `"up"` or `"down"` | | `amount` | integer | No | Number of scroll steps (default: 3) | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/scroll \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 500, "y": 300, "direction": "down", "amount": 5}' ``` --- ## Drag **`POST /api/v1/computers/{id}/desktop/drag`** Performs a click-and-drag from one point to another. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `start_x` | integer | Yes | Starting X coordinate | | `start_y` | integer | Yes | Starting Y coordinate | | `end_x` | integer | Yes | Ending X coordinate | | `end_y` | integer | Yes | Ending Y coordinate | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/drag \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"start_x": 100, "start_y": 200, "end_x": 400, "end_y": 500}' ``` --- ## Wait **`POST /api/v1/computers/{id}/desktop/wait`** Server-side pause. Holds the connection for the specified duration before responding. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `seconds` | number | No | Seconds to wait (default: 1, max: 30) | ### Response - `200 OK` ```json { "success": true, "waited_seconds": 2 } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/wait \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"seconds": 2}' ``` --- ## List Windows **`GET /api/v1/computers/{id}/desktop/windows`** Returns a list of open windows on the desktop. ### Response - `200 OK` ```json { "windows": [ { "id": "0x2200003", "title": "Terminal - user@computer", "class": "Xfce4-terminal", "x": 100, "y": 50, "width": 800, "height": 600 } ] } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/desktop/windows \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Focus Window **`POST /api/v1/computers/{id}/desktop/window/focus`** Brings a window to the foreground. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `window_id` | string | Yes | Window ID from the list windows response | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/window/focus \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"window_id": "0x2200003"}' ``` --- ## Launch Application **`POST /api/v1/computers/{id}/desktop/launch`** Launches an application by name or command. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `app` | string | Yes | Application name or command (e.g., `"firefox"`, `"xfce4-terminal"`) | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/launch \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"app": "firefox"}' ``` --- ## Cursor Position **`GET /api/v1/computers/{id}/desktop/cursor`** Returns the current mouse cursor coordinates. ### Response - `200 OK` ```json { "x": 500, "y": 300 } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/desktop/cursor \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Hotkey **`POST /api/v1/computers/{id}/desktop/hotkey`** Presses a hotkey combination. Functionally equivalent to `key` but uses a dedicated endpoint for named combinations. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `keys` | string[] | Yes | Ordered list of keys to press simultaneously (e.g. `["ctrl", "shift", "t"]`) | ### Response - `200 OK` ```json {"success": true} ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/hotkey \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"keys": ["ctrl", "shift", "t"]}' ``` --- ## Key Down / Key Up **`POST /api/v1/computers/{id}/desktop/key-down`** **`POST /api/v1/computers/{id}/desktop/key-up`** Send individual key-down or key-up events. Use these for precise control when holding a modifier key while performing another action. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `key` | string | Yes | Key name (same format as `key` endpoint) | ### Response - `200 OK` ```json {"success": true} ``` --- ## Mouse Down / Mouse Up **`POST /api/v1/computers/{id}/desktop/mouse-down`** **`POST /api/v1/computers/{id}/desktop/mouse-up`** Send individual mouse-button-down or mouse-button-up events. Use for custom drag sequences or long-press interactions. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | X coordinate | | `y` | integer | Yes | Y coordinate | | `button` | string | No | `"left"` (default), `"right"`, `"middle"` | ### Response - `200 OK` ```json {"success": true} ``` --- ## Move Mouse **`POST /api/v1/computers/{id}/desktop/move`** Moves the mouse cursor to the specified position without clicking. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | Target X coordinate | | `y` | integer | Yes | Target Y coordinate | ### Response - `200 OK` ```json {"success": true} ``` --- ## Clipboard ### Get Clipboard **`GET /api/v1/computers/{id}/desktop/clipboard`** Returns the current clipboard contents. ### Response - `200 OK` ```json {"text": "Hello, World!"} ``` ### Set Clipboard **`POST /api/v1/computers/{id}/desktop/clipboard`** Sets the clipboard contents. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `text` | string | Yes | Text to place in the clipboard | ### Response - `200 OK` ```json {"success": true} ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/clipboard \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Hello, World!"}' ``` --- ## Screen Size **`GET /api/v1/computers/{id}/desktop/screen-size`** Returns the current desktop resolution. ### Response - `200 OK` ```json {"width": 1280, "height": 720} ``` --- ## Environment **`GET /api/v1/computers/{id}/desktop/environment`** Returns desktop session metadata - display server, session type, active user, and desktop environment name. --- ## Set Wallpaper **`POST /api/v1/computers/{id}/desktop/wallpaper`** Sets the desktop wallpaper. Supports per-tenant white-label branding. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `url` | string | Yes | Publicly accessible URL of the image to set as wallpaper | --- ## Accessibility Tree **`GET /api/v1/computers/{id}/desktop/accessibility-tree`** Returns the AT-SPI accessibility tree for the current desktop state. The tree describes all visible UI elements with their roles, labels, states, and bounding boxes. Use this for element discovery when exact coordinates are unknown. ### Response - `200 OK` ```json { "tree": { "role": "application", "name": "Firefox", "children": [ { "role": "button", "name": "Close", "x": 1260, "y": 10, "width": 20, "height": 20 } ] } } ``` --- ## Window Management ### List Windows See [List Windows](#list-windows) above. ### Window Size **`GET /api/v1/computers/{id}/desktop/window/{window_id}/size`** Returns the width and height of a specific window. ### Window Position **`GET /api/v1/computers/{id}/desktop/window/{window_id}/position`** Returns the x,y position of a specific window. ### Resize Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/resize`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `width` | integer | Yes | New width in pixels | | `height` | integer | Yes | New height in pixels | ### Move Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/move`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | New X position | | `y` | integer | Yes | New Y position | ### Maximize Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/maximize`** ### Minimize Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/minimize`** ### Close Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/close`** All window management endpoints return `{"success": true}` on `200 OK`. --- ## Common Errors All desktop endpoints share these error responses: | Status | Code | Description | |--------|------|-------------| | 400 | `invalid computer id` | UUID format invalid | | 403 | `FORBIDDEN` | Not a member of this computer | | 404 | `NOT_FOUND` | Computer does not exist | | 409 | `COMPUTER_NOT_RUNNING` | Computer is stopped or provisioning | | 502 | `AGENT_UNAVAILABLE` | Cannot reach the computer command service | --- ## See also - [Computers API](/docs/api-reference/computers/) - create, start, stop computers - [Exec API](/docs/api-reference/exec/) - run bash/Python without desktop interaction - [Files API](/docs/api-reference/files/) - read and write files in the VM - [Error Codes](/docs/api-reference/errors/) - complete error code reference --- # API Reference / Docker Host URL: https://miosa.ai/docs/api-reference/docker-deploy Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/docker-deploy Source: src/routes/docs/api-reference/docker-deploy/+page.md Description: REST API for Docker Host templates, workspace hosts, and sandbox-to-container publishing. Docker Host is the dedicated workspace host for customers who need many containerized apps on one always-on MIOSA-managed runtime. Use the regular Deployments API for the deployment object. Use the Docker Host API to choose templates and provision the workspace host. Docker Host apps still return normal MIOSA deployment URLs and custom-domain records. The workspace host runs the app containers while MIOSA owns DNS, TLS, routing, health checks, and deployment records. ## Endpoints ```http GET /api/v1/docker-deploy/templates GET /api/v1/docker-deploy/templates/:id GET /api/v1/docker-deploy/hosts?workspace_id=:workspace_id POST /api/v1/docker-deploy/hosts/ensure GET /api/v1/docker-deploy/hosts/:id POST /api/v1/sandboxes/:id/publish POST /api/v1/deployments/:id/publish ``` All endpoints require `Authorization: Bearer `. Mutations should include an `Idempotency-Key` header. ## List templates ```http GET /api/v1/docker-deploy/templates ``` Returns curated container deployment templates such as Next.js app, Next.js static site, React/Vite, static HTML, Python API, Node API, and compose-backed app patterns. Response: ```json { "data": [ { "id": "nextjs-app", "name": "Next.js App", "runtime": "node", "build_command": "npm install && npm run build", "run_command": "npm start", "port": 3000, "health_check_path": "/" } ] } ``` ## Get template ```http GET /api/v1/docker-deploy/templates/:id ``` Use this when your UI needs the template defaults before creating or publishing a deployment. ## List workspace hosts ```http GET /api/v1/docker-deploy/hosts?workspace_id=550e8400-e29b-41d4-a716-446655440000 ``` Returns Docker Host hosts visible to the caller. Response: ```json { "data": [ { "id": "78745c76-d5e6-4541-b651-1018f7cc0d89", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "computer_id": "cmp_...", "status": "active", "runtime_status": "healthy", "runtime_version": "2026.07" } ] } ``` ## Ensure workspace host ```http POST /api/v1/docker-deploy/hosts/ensure ``` Creates or returns the dedicated Docker Host host for a workspace. One host can run many deployments for that workspace. Body: ```json { "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "region": "us-central", "size": "standard", "metadata": { "customer": "Lumen" } } ``` Response: ```json { "data": { "id": "78745c76-d5e6-4541-b651-1018f7cc0d89", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "computer_id": "cmp_...", "status": "provisioning", "runtime_status": "booting" } } ``` ## Get host ```http GET /api/v1/docker-deploy/hosts/:id ``` Returns the latest status for one Docker Host host. A deployment is not ready until the host is active and the deployed app health check passes through that host. ## Publish from sandbox ```http POST /api/v1/sandboxes/:id/publish ``` Body: ```json { "name": "Acme Lead Magnet", "deployment_type": "docker_deploy", "docker_deploy_template_id": "nextjs-app", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "build_command": "npm install && npm run build", "run_command": "npm start", "port": 3000, "health_check_path": "/", "external_workspace_id": "acct_123", "external_project_id": "lead_magnet_456" } ``` MIOSA packages the sandbox, ensures the workspace Docker Host host exists, installs the release, waits for the app route health check, then returns the deployment and public URL. ## Publish existing deployment ```http POST /api/v1/deployments/:id/publish ``` Use the same Docker Host fields when publishing a new version of an existing deployment: ```json { "source_sandbox_id": "sbx_...", "deployment_type": "docker_deploy", "docker_deploy_template_id": "nextjs-app", "build_command": "npm install && npm run build", "run_command": "npm start", "port": 3000, "health_check_path": "/" } ``` ## See also - [Docker Host guide](/docs/docker-host/docker-deploy/) - end-to-end setup and operating model - [Deployments](/docs/api-reference/deployments/): deployment object, publish, rollback, delete - [Custom Domains](/docs/api-reference/custom-domains/): attach exact domains to deployments - [Workspaces](/docs/api-reference/workspaces/): workspace ownership and attribution --- # Egress (Security) API URL: https://miosa.ai/docs/api-reference/egress Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/egress Source: src/routes/docs/api-reference/egress/+page.md Description: API reference for the MIOSA Egress credential vault, network allowlist, OAuth Connect, and audit log. The Egress API powers the **Security** tab on every Computer and Sandbox: encrypted credential vault, OAuth Connect, network allowlist with audit-only / enforce modes, and a live-tail audit log. Base path: `/api/v1/egress` All endpoints require `Authorization: Bearer <msk_*>`. See [Authentication](/docs/authentication). For higher-level access, use the [SDKs](/docs/sdks/egress). For the architecture overview, walkthroughs, and white-label patterns, see the [Security guides](/docs/security). --- ## Secrets ### Create a secret **`POST /api/v1/egress/secrets`** Stores an encrypted credential and optionally binds it to a resource as an environment variable. The raw value is encrypted with ChaCha20-Poly1305 and never returned again. ```bash curl -X POST https://api.miosa.ai/api/v1/egress/secrets \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "openai_key", "value": "sk-proj-...", "type": "api_key", "scope": "user", "expose_as_env": "OPENAI_API_KEY" }' ``` Required body fields: `name`, `value`, `type`, `scope`. | Field | Values | Notes | |---|---|---| | `type` | `api_key`, `oauth_connect`, `oauth_machine` | OAuth flows are usually initiated via `/oauth/start` instead. | | `scope` | `tenant`, `workspace`, `user`, `external_user`, `external_workspace` | The 5-tier scoping model. | | `expose_as_env` | shell-safe upper-snake-case | If set, auto-creates a binding. | Optional scoping fields based on `scope`: `workspace_id`, `owner_user_id`, `external_user_id`, `external_workspace_id`. Returns `201 Created` with `{data: , binding: }`. The `data.preview` is the first-6 + last-4 of the value; the full value is never returned. ### List secrets **`GET /api/v1/egress/secrets`** Query params: `scope`, `workspace_id`, `owner_user_id`, `external_user_id`, `external_workspace_id`. Returns metadata only (no values). ### Get a secret **`GET /api/v1/egress/secrets/:id`** - metadata for one secret. ### Rotate **`PATCH /api/v1/egress/secrets/:id`** Re-encrypts with a new value. The bound `placeholder_token` does not change; every sandbox using it picks up the new value on its next outbound request. ```json { "value": "sk-proj-new-value", "expires_at": "2026-12-31T00:00:00Z" } ``` ### Delete **`DELETE /api/v1/egress/secrets/:id`** --- ## Bindings A binding links a secret to a Computer or Sandbox and surfaces it as an environment variable (or opaque header token). ### Create a binding **`POST /api/v1/egress/bindings`** ```json { "secret_id": "", "resource_id": "", "resource_type": "sandbox", "exposure": "env_var", "expose_as_env": "OPENAI_API_KEY" } ``` Returns the binding including its `placeholder_token` - the opaque `miosa-tok-…` value the workload sees in env. ### List + delete - `GET /api/v1/egress/bindings?resource_id=&resource_type=sandbox` - `DELETE /api/v1/egress/bindings/:id` --- ## OAuth Connect ### List provider catalog **`GET /api/v1/egress/oauth/providers`** - returns providers visible to the calling tenant (platform defaults merged with tenant overrides). No client secrets exposed. ### Start an OAuth flow **`POST /api/v1/egress/oauth/start`** ```json { "provider": "github", "expose_as_env": "GITHUB_TOKEN", "scope": "user", "owner_user_id": "" } ``` Returns `{authorize_url, state}`. Open `authorize_url` in a browser; the user approves; the upstream redirects to `/api/v1/egress/oauth/callback?code=&state=`. On success the tokens are encrypted into a new `oauth_connect` secret + binding. ### Status **`GET /api/v1/egress/oauth/status?state=`** - poll while the user is completing the flow. Returns `pending`, `complete`, or `failed`. ### Admin: tenant-override providers (white-label) - `POST /api/v1/egress/oauth/admin/providers` - register your own OAuth app credentials so end-users see your brand on the consent screen. - `DELETE /api/v1/egress/oauth/admin/providers/:id` Requires admin (`msk_a_*`) credentials. --- ## Network policies + allowlist ### Policies - `GET /api/v1/egress/policies` - list (optionally filter by `resource_id`) - `POST /api/v1/egress/policies` - create - `GET /api/v1/egress/policies/:id` - `PATCH /api/v1/egress/policies/:id` - set `mode` (`audit_only` / `enforce`), `default_action` (`allow` / `deny`) Each Computer or Sandbox resolves the policy in this order: resource override → tenant default. Tenant default starts in `audit_only` mode. ### Allowlist rules - `GET /api/v1/egress/allowlist?policy_id=` - `POST /api/v1/egress/allowlist` - `DELETE /api/v1/egress/allowlist/:id` ```json { "policy_id": "", "pattern": "*.openai.com", "kind": "wildcard", "method": "POST", "path_glob": "/v1/*", "action": "allow", "warn_only": false, "priority": 100 } ``` Pattern kinds: `exact` (literal host), `wildcard` (`*.example.com`), `cidr` (IP literal targets only). --- ## Audit log ### Query **`GET /api/v1/egress/audit`** Query params: `resource_id`, `host`, `action` (`allow`/`reject`/`stub`/`error`), `since` (relative like `1h` / `7d` or ISO timestamp), `until`, `limit` (default 100, max 1000), `external_user_id`, `external_workspace_id`. Each row includes the full identity chain (`tenant_id`, `workspace_id`, `owner_user_id`, `external_user_id`, `external_workspace_id`), request metadata (`host`, `method`, `path`, `sni`, `mode`, `remote_addr`), outcome (`action`, `rejected_by`, `status_code`, `duration_ms`, `bytes_in`, `bytes_out`, `error_message`), and the transform trace JSONB (`request_transforms`, `response_transforms`, `mcp`). ### Get a single event **`GET /api/v1/egress/audit/:id`** ### Allowlist suggestions **`GET /api/v1/egress/audit/suggestions?resource_id=`** - returns the hosts a resource has contacted recently, ranked by frequency. The basis for the one-click "Lock down with selected as allowlist" UX. ### Live tail (WebSocket) **`wss://api.miosa.ai/api/v1/egress/audit/resource/:resource_id?token=`** Subprotocol: `miosa-egress-audit-v1`. Intended to push `{"type":"audit_event","data":{...}}` frames in real time as outbound requests are recorded. Egress audit capture is not yet enabled in the current release, so no frames are emitted yet. Tenant-scoped variant (admin only): `wss://api.miosa.ai/api/v1/egress/audit/tenant/:tenant_id?token=`. --- ## Rate limits + retention | Scope | Limit | |---|---| | All `/egress/*` endpoints | Current limit is returned through `X-RateLimit-*` headers | | Audit retention | 90 days for every tenant; longer on enterprise | | Audit live-tail (WebSocket) | unlimited; does not count against REST quota | --- ## Errors Same envelope as the rest of the API - `{"error": "...", "details": ...}` with the appropriate status code. | Status | When | |---|---| | `400` | invalid body, unknown scope, malformed env var name | | `401` | missing / invalid credential | | `403` | role does not allow the operation (e.g., creating tenant-scoped secret as a non-admin) | | `404` | secret / binding / policy / event not found in this tenant | | `409` | secret name collision within scope (unique partial index) | | `422` | scope ↔ identity column mismatch (e.g., scope=user without owner_user_id) | --- ## SDK shortcut All endpoints above are wrapped by the [Egress SDK reference](/docs/sdks/egress) with idiomatic methods in Python, TypeScript, Go, Java, and Elixir. --- # Error Codes URL: https://miosa.ai/docs/api-reference/errors Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/errors Source: src/routes/docs/api-reference/errors/+page.md Description: Every error code the MIOSA API returns, what it means, and how to handle it. Most MIOSA API errors follow a single envelope shape, `{error: {code, message, request_id}}`. The HTTP status indicates the broad class; the `code` field is the machine-readable reason. A few families differ: authentication failures return a flat `{error: {type, code}}` shape, and snapshot errors can return a flat message string. ```json { "error": { "code": "NOT_FOUND", "message": "sandbox not found", "request_id": "req_01jv..." } } ``` Include `request_id` whenever you contact support. It correlates the request across all internal services. --- ## Authentication errors Returned by the `ApiKeyAuth` plug before the request reaches any controller. | Code | HTTP | Meaning | What to do | |---|---|---|---| | `invalid_token` | 401 | `Authorization` header missing, malformed, or the key/JWT is invalid | Verify the header is `Authorization: Bearer msk_...` | | `invalid_token` | 401 | Refresh token used on a non-auth endpoint | Use a workspace API key (`msk_*`), not a refresh token | | `insufficient_scope` | 403 | Key exists but lacks the required scope for this operation | Adjust key scopes in the dashboard | | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests from this key or IP | Back off and retry after `Retry-After` seconds | All API endpoints are rate-limited. Limits vary by key family, tenant policy, and endpoint family. Honor `X-RateLimit-*` and `Retry-After` instead of hardcoding a client-side limit. --- ## General resource errors These codes appear across all resource types (computers, sandboxes, deployments, workspaces, etc.). | Code | HTTP | Meaning | What to do | |---|---|---|---| | `NOT_FOUND` | 404 | Resource does not exist or belongs to a different tenant | Confirm the ID and that the key's tenant owns the resource | | `FORBIDDEN` | 403 | Authenticated but not authorized to access this resource | Check membership or ownership | | `INVALID_ID` | 400 | Path parameter is not a valid UUID | Use a UUID v4 ID, not a slug or name | | `MISSING_PARAM` | 400 | A required request body field is absent | Check the field name in the request body schema | | `VALIDATION_ERROR` | 422 | One or more fields failed schema validation | Inspect `details` for per-field messages | | `VALIDATION_FAILED` | 422 | Changeset validation failed (alias of `VALIDATION_ERROR` in some controllers) | Inspect `details` | | `TENANT_RESOLUTION_FAILED` | 422 | Cannot determine the tenant for the authenticated identity | The API key may belong to a workspace that has been deleted or suspended | | `INTERNAL_ERROR` | 500 | Unexpected server-side error | Retry with exponential backoff; report if persistent | --- ## Sandbox errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `INSUFFICIENT_CREDITS` | 402 | Not enough credits to provision this sandbox | Top up credits in the dashboard or reduce the resource request | | `SANDBOX_LIMIT_REACHED` | 429 | Tenant has hit the concurrent sandbox cap for its plan (Developer 10, Business 250, Enterprise 500) | Destroy idle sandboxes, upgrade the plan, or contact support to raise the limit | | `SANDBOX_NOT_RUNNING` | 409 | Operation requires the sandbox to be in `running` state | Wait until state is `running`; poll `GET /sandboxes/{id}` or subscribe to events | | `AGENT_UNAVAILABLE` | 502 | The computer command service is reachable but not responding | Wait and retry; if persistent, destroy and recreate the sandbox | | `INVALID_TEMPLATE` | 400 | `template_id` is not a recognized template | Use `GET /api/v1/sandbox-templates` to list valid IDs | | `MISSING_PATH` | 400 | `path` field is required but absent | Include `path` in the request body | | `INVALID_PATH` | 400 | `path` is not a valid absolute filesystem path | Use an absolute path (starts with `/`) | | `MISSING_FILE` | 400 | Multipart upload missing the `file` part | Include the file part in the multipart form | | `INVALID_BODY` | 400 | Request body could not be parsed or decoded | Verify JSON or base64 encoding | | `MISSING_MODE` | 400 | `chmod` call missing `mode` field | Include an octal mode string, e.g. `"0644"` | | `INVALID_PORT` | 422 | Expose port is outside `1`-`65535` | Use a port in the valid range | | `MISSING_PARAM` | 400 | `port` omitted on `/expose` and template has no default preview port | Provide `port` explicitly | | `SANDBOX_NOT_RUNNING` | 409 | `/deploy` called but sandbox is not running | Confirm sandbox state before promoting | | `SANDBOX_PLACEMENT_UNCERTAIN` | 409 | A create without an `Idempotency-Key` could not confirm its placement outcome, so the sandbox may or may not exist | List your sandboxes to reconcile before creating another; retrying without a key risks a duplicate | | `SANDBOX_PLACEMENT_RECONCILING` | 503 | A create that carried an `Idempotency-Key` could not confirm its placement outcome yet | Retry with the same `Idempotency-Key` after `Retry-After` seconds; the retry resolves to the same sandbox | | `INVALID_SANDBOX_RUNTIME_PROFILE` | 422 | `runtime_profile` is not `standard`, `soma`, or `auto` | Send one of the allowed values; `details.allowed` names them | | `SOMA_RUNTIME_UNSUPPORTED_REQUEST` | 422 | The requested shape is above the runtime's maximum, or the runtime cannot serve it | Request a smaller shape, or use `runtime_profile "standard"`; `details.max_shape` names the ceiling | When a sandbox create cannot confirm its placement outcome, the answer depends on whether you sent an `Idempotency-Key` header. - With a key, the API returns a retryable `503 SANDBOX_PLACEMENT_RECONCILING` with `Retry-After`. Retry with the same key and it resolves to the same sandbox. - Without a key, the API returns a terminal `409 SANDBOX_PLACEMENT_UNCERTAIN`. List your sandboxes to reconcile before creating another, since a retry could create a duplicate. Send an `Idempotency-Key` on every create so an uncertain outcome is retryable instead of terminal. --- ## Sandbox template build errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `TEMPLATE_NOT_FOUND` | 404 | Template ID does not exist | Verify the template ID | | `BUILD_NOT_FOUND` | 404 | Build record does not exist | Verify the build ID | | `BUILD_ALREADY_TERMINAL` | 409 | Cannot cancel a build that is already `ready`, `failed`, or `cancelled` | No action needed; build is in a terminal state | | `BUILD_NOT_RETRYABLE` | 409 | Build is not in a state that allows retry (`failed` or `cancelled` only) | Only `failed` or `cancelled` builds may be retried | | `UNAUTHORIZED` | 401 | SSE ticket missing, invalid, or expired | Re-issue a ticket via `POST /auth/sse-ticket` and reconnect | --- ## Computer errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `INSUFFICIENT_CREDITS` | 402 | Not enough credits to start this computer | Top up credits | | `COMPUTER_NOT_RUNNING` | 409 | Desktop or exec operation requires a running computer | Start the computer first | | `COMPUTER_NOT_FOUND` | 404 | Computer does not exist in this tenant | Verify the computer ID | | `AGENT_UNAVAILABLE` | 502 | computer command service unreachable | Computer may be starting; retry after a few seconds | --- ## Deployment errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `DEPLOYMENT_NOT_FOUND` | 404 | Deployment ID does not exist | Verify the deployment ID | | `VALIDATION_ERROR` | 422 | Deployment fields failed validation | Check `details` for field-level messages | | `SANDBOX_NOT_RUNNING` | 409 | Publishing requires a running sandbox | Confirm sandbox state | | `RUNTIME_TARGET_UNAVAILABLE` | 409 | Sandbox has no IP yet - cannot route to it | Retry after a few seconds | | `RUNTIME_LOGS_UNAVAILABLE` | 502 | Cannot fetch runtime logs from the sandbox | Retry or check sandbox health | | `EMPTY_ARTIFACT` | 422 | Publish output contained no files | Verify your build step produces output files | | `SERVICE_UNAVAILABLE` | 503 | A required internal service (e.g. encryption) is not configured | Contact support | --- ## Snapshot errors Direct snapshot create, list, restore, and delete operations apply to desktop Computers under `/api/v1/computers/{id}/snapshots`. Canonical sandbox V1 exposes snapshot-backed fork at `/api/v1/sandboxes/{id}/fork`, not customer-managed snapshot IDs. | Code | HTTP | Meaning | What to do | |---|---|---|---| | `NOT_FOUND` (snapshot) | 404 | Snapshot does not exist or belongs to a different computer | Verify snapshot ID and computer ID | | `FORBIDDEN` | 403 | Authenticated tenant does not own this snapshot | Check ownership | | `already_deleted` | 409 | Snapshot has already been deleted | No action; already in terminal state | | `snapshot is not in ready state` | 409 | Restore attempted before snapshot reached `ready` | Poll snapshot status until `ready` | | `missing sse ticket` | 401 | SSE stream opened without a ticket | Issue a ticket via `POST /auth/sse-ticket` first | --- ## Custom domain errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `DOMAIN_NOT_FOUND` | 404 | Custom domain record not found | Verify the domain ID | | `COMPUTER_NOT_FOUND` | 404 | The computer this domain is attached to was not found | Verify the computer ID | | `FORBIDDEN` | 403 | Domain belongs to a different tenant | Verify ownership | | `VALIDATION_FAILED` | 422 | `fqdn` missing or invalid | Include a valid fully-qualified domain name | --- ## Workspace errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `VALIDATION_FAILED` | 422 | Workspace creation or update fields are invalid | Inspect `details` | | `TEMPLATE_NAME_TAKEN` | 409 | A template with that name already exists in this workspace | Choose a different name | --- ## Rate-limit and quota errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `RATE_LIMIT_EXCEEDED` | 429 | General API rate limit hit | Respect the `Retry-After` response header | | `SANDBOX_LIMIT_REACHED` | 429 | Concurrent sandbox cap reached | Destroy idle sandboxes or upgrade plan | | `INSUFFICIENT_CREDITS` | 402 | Credit balance depleted | Purchase credits from the billing dashboard | All 429 responses include a `Retry-After` header (seconds). Honor it - repeated violations may result in temporary IP blocks. --- ## See also - [API Reference overview](/docs/api-reference/) - base URL, auth, and request format - [Sandboxes](/docs/api-reference/sandboxes/) - sandbox lifecycle and file operations - [Computers](/docs/api-reference/computers/) - computer CRUD and desktop control - [Deployments](/docs/api-reference/deployments/) - deployment and version errors --- # Events (SSE) URL: https://miosa.ai/docs/api-reference/events Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/events Source: src/routes/docs/api-reference/events/+page.md Description: Server-Sent Event streams for real-time updates from sandboxes, computers, deployments, and builds. MIOSA exposes SSE streams for resources that produce timed events: sandboxes, computer sessions, deployment builds, sandbox-template builds, and computer logs. All streams follow the same authentication and envelope pattern. --- ## Authentication pattern The browser `EventSource` API cannot send an `Authorization` header. Use the two-step ticket flow for all SSE connections: Diagram: sequenceDiagram participant Client participant MIOSA API Client->>MIOSA API: POST /api/v1/auth/sse-ticket
Authorization: Bearer msk_u_... MIOSA API-->>Client: { "ticket": "sset_..." } Client->>MIOSA API: GET ?ticket=sset_...
Accept: text/event-stream MIOSA API-->>Client: text/event-stream (open connection) **Step 1 - Mint a ticket:** ```bash curl -X POST https://api.miosa.ai/api/v1/auth/sse-ticket \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" # Response: { "ticket": "sset_01hx9z...", "expires_in": 60 } ``` Tickets are single-use and expire in 60 seconds. Mint one immediately before opening the `EventSource`. **Step 2 - Open the stream:** ```bash curl -N "https://api.miosa.ai/api/v1/sandboxes/{id}/events?ticket=sset_01hx9z..." \ -H "Accept: text/event-stream" ``` --- ## Live SSE streams | Stream | Endpoint | Description | |---|---|---| | Sandbox events | `GET /sandboxes/{id}/events` | Exec progress, file change notifications, preview readiness | | Computer agent events | `GET /computers/{id}/agent/events` | Agent task progress and computer lifecycle | | Computer logs | `GET /computers/{id}/logs/stream` | stdout/stderr from computer processes | | Sandbox logs | `GET /sandboxes/{id}/logs/stream` | stdout/stderr from sandbox template lifecycle | | Build events | `GET /sandbox-template-builds/{id}/logs/stream` | Custom template build progress | --- ## Event envelope All events arrive in standard SSE format: ``` event: data: id: ``` All payloads are JSON. The `id:` line allows clients to resume with `Last-Event-ID` on reconnect. --- ## Sandbox event types | Event | Payload | Description | |---|---|---| | `sandbox.started` | `{sandbox_id, region}` | Boot complete; ready to accept exec | | `sandbox.exec.started` | `{exec_id, command}` | Command began | | `sandbox.exec.stdout` | `{exec_id, line}` | stdout chunk | | `sandbox.exec.stderr` | `{exec_id, line}` | stderr chunk | | `sandbox.exec.completed` | `{exec_id, exit_code, duration_ms}` | Command exited | | `sandbox.preview.ready` | `{preview_id, url, port}` | Preview URL available | | `sandbox.idle` | `{sandbox_id, last_activity_at}` | Going to auto-suspend | | `sandbox.suspended` | `{sandbox_id}` | Auto-suspended after idle window | --- ## Deployment event types | Event | Payload | Description | |---|---|---| | `deployment.publish.queued` | `{version_id}` | Publish accepted, waiting for builder | | `deployment.publish.building` | `{version_id, stage}` | Builder running | | `deployment.publish.uploading` | `{version_id, size_bytes}` | Release artifact upload | | `deployment.publish.promoting` | `{version_id}` | Switching active version | | `deployment.publish.completed` | `{version_id, deployment_id, url}` | Live | | `deployment.publish.failed` | `{version_id, error_code, message}` | Build or health-check failure | --- ## Computer event types | Event | Payload | Description | |---|---|---| | `computer.started` | `{computer_id, region}` | VM booted, desktop ready | | `computer.stream.token_ready` | `{token, expires_at}` | New short-lived stream credential | | `computer.idle` | `{computer_id, idle_seconds}` | Approaching auto-stop | | `computer.stopped` | `{computer_id, reason}` | Stopped (manual or auto) | --- ## Reconnect and replay Send `Last-Event-ID: <ULID>` on the resubscribe request. The server replays all events since that ID. Events are retained for **15 minutes** per resource. ```bash curl -N "https://api.miosa.ai/api/v1/sandboxes/{id}/events?ticket=sset_..." \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: 01hwqz4b3c2v1x..." ``` --- ## Error handling If the connection drops or the ticket expires, the next request returns `401`. Mint a fresh ticket and resubscribe: ```typescript async function openStream(sandboxId: string): Promise { const { ticket } = await fetch('/api/v1/auth/sse-ticket', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MIOSA_API_KEY}` }, }).then(r => r.json()); const es = new EventSource( `https://api.miosa.ai/api/v1/sandboxes/${sandboxId}/events/stream?ticket=${ticket}` ); es.addEventListener('error', () => { es.close(); setTimeout(() => openStream(sandboxId), 1000); }); es.addEventListener('sandbox.exec.completed', (e) => { console.log('Exec done:', JSON.parse(e.data)); }); } ``` --- ## See also Create sandboxes, run exec, write files, open previews. [Sandboxes →](/docs/develop/sandboxes/) Builder, Release, Version - the full deploy pipeline. [Publishing →](/docs/docker-host/publishing/) Desktop lifecycle, screenshot, click, keyboard APIs. [Computers →](/docs/computers/overview/) API keys, browser tokens, SSE tickets. [Authentication →](/docs/authentication/) --- ## Sandbox events stream **`GET /api/v1/sandboxes/{id}/events`** Opens a Server-Sent Events (SSE) stream that emits sandbox lifecycle events. The connection closes automatically when the sandbox reaches `destroyed` or `error`. ### Auth Bearer token required (sent as `Authorization: Bearer <key>` header). ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. | ### Response - `200 OK` (event stream) `Content-Type: text/event-stream` **Event: `state_changed`** ``` event: state_changed data: {"state":"running","previous_state":"provisioning","timestamp":"2026-04-25T10:00:07Z"} ``` **Event: `exec_output`** ``` event: exec_output data: {"exec_id":"exec_abc","stream":"stdout","data":"2\n","timestamp":"2026-04-25T10:00:10Z"} ``` **Event: `error`** ``` event: error data: {"message":"VM terminated unexpectedly","timestamp":"2026-04-25T10:05:00Z"} ``` ```bash curl -N "https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/events" \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Accept: text/event-stream" ``` --- # Exec API URL: https://miosa.ai/docs/api-reference/exec Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/exec Source: src/routes/docs/api-reference/exec/+page.md Description: API reference for executing bash commands and Python code on MIOSA computers. Run commands and scripts directly on a computer without desktop interaction. Both endpoints block until the command exits and return combined stdout+stderr. Base path: `/api/v1/computers/{id}/exec` The computer must be in `"running"` status. Commands run as the default user inside the VM. The bash exec timeout is capped at 86,400 s; the Python exec timeout is capped at 300 s. Rate limits vary by key, tenant policy, and endpoint family. Honor `X-RateLimit-*` and `Retry-After` response headers. For long-running output, prefer the streaming exec endpoint which avoids holding an HTTP connection open for the full duration. --- ## Execute Bash Command **`POST /api/v1/computers/{id}/exec`** Runs a shell command on the computer. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `command` | string | Yes | Shell command to execute | | `timeout` | integer | No | Timeout in seconds (default: 30, max: 86400) | ### Response - `200 OK` ```json { "output": "total 24\ndrwxr-xr-x 6 user user 4096 Apr 11 10:00 .\n", "exit_code": 0 } ``` The `output` field contains combined stdout and stderr. ### Errors | Status | Code | Description | |--------|------|-------------| | 409 | `COMPUTER_NOT_RUNNING` | Computer is not running | | 502 | - | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "ls -la /workspace", "timeout": 30}' ``` ### Examples ```bash # Install a package curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "sudo apt-get install -y jq", "timeout": 120}' # Check disk space curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "df -h"}' # Multi-line script curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "cd /workspace && mkdir -p project && echo done"}' ``` --- ## Execute Python Code **`POST /api/v1/computers/{id}/exec/python`** Runs Python code on the computer. The code is written to a temporary file and executed with `python3`. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `code` | string | Yes | Python source code | | `timeout` | integer | No | Timeout in seconds (default: 30, max: 300) | ### Response - `200 OK` ```json { "output": "42\n", "exit_code": 0 } ``` ### How It Works 1. Code is written to `/tmp/miosa_exec_<random>.py` 2. Executed via `timeout <N> python3 /tmp/miosa_exec_<random>.py 2>&1` 3. Temporary file is deleted after execution 4. stdout and stderr are captured in `output` ### Errors | Status | Code | Description | |--------|------|-------------| | 409 | `COMPUTER_NOT_RUNNING` | Computer is not running | | 502 | - | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/python \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import sys\nprint(f\"Python {sys.version}\")\nprint(2 + 2)", "timeout": 30 }' ``` ### Examples ```bash # JSON processing curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/python \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import json\ndata = {\"hello\": \"world\"}\nprint(json.dumps(data, indent=2))", "timeout": 10 }' # File processing curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/python \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import os\nfor f in os.listdir(\"/workspace\"):\n print(f)", "timeout": 10 }' # Long-running computation curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/python \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "total = sum(range(10_000_000))\nprint(f\"Sum: {total}\")", "timeout": 60 }' ``` --- ## Terminal ### Create Terminal Session **`POST /api/v1/computers/{id}/terminal`** Creates a WebSocket-based terminal session on the computer. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `cols` | integer | No | Terminal columns (default: 80) | | `rows` | integer | No | Terminal rows (default: 24) | | `shell` | string | No | Shell to use (default: `/bin/bash`) | #### Response - `201 Created` ```json { "data": { "session_id": "default", "ws_url": "wss://my-computer.sandbox.miosa.ai/ws/terminal/550e8400-.../default?auth=stream_token", "stream_auth": "stream_token", "expires_at": 1712700060, "computer_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/terminal \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"cols": 80, "rows": 24, "shell": "/bin/bash"}' ``` --- ### Resize Terminal **`POST /api/v1/computers/{id}/pty/{session_id}/resize`** Resizes an active terminal session. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `cols` | integer | Yes | New column count | | `rows` | integer | Yes | New row count | #### Response - `200 OK` ```json { "status": "ok" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/pty/{session_id}/resize \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"cols": 120, "rows": 40}' ``` --- ## Timeout Behavior - Commands exceeding the timeout are killed with SIGTERM - The bash exec timeout is capped at **86,400 seconds** - The Python exec timeout is clamped: `min(requested, 300)` - If no timeout is specified, the default is **30 seconds** ## Pre-installed Software The default template includes: - Python 3 with pip - Node.js (if installed via selected_apps) - Common CLI tools: curl, wget, git, vim, jq - System utilities: htop, tree, zip/unzip Install additional packages via the bash exec endpoint: ```bash # Python packages {"command": "pip install requests pandas numpy"} # System packages {"command": "sudo apt-get install -y postgresql-client"} ``` --- --- ## Sandbox exec The same exec surface exists for sandboxes under `/api/v1/sandboxes/{id}`. On a SOMA sandbox the executable and each argument are limited to 4096 bytes, and a longer shell command answers `502 AGENT_UNAVAILABLE`, which looks retryable but is not. Send the command as a file, or on standard input, instead. See [Choosing a runtime](/docs/soma/choosing-a-runtime/). ### Execute a Command (sandbox) **`POST /api/v1/sandboxes/{id}/exec`** Runs a shell command inside the sandbox. Blocks until the process exits. #### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. Must be in `running` state. | #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `command` | string | Yes | Shell command to execute. | | `timeout` | integer | No | Per-command timeout in seconds. Default `3600`, max `86400`. | | `cwd` | string | No | Working directory inside the VM. | | `env` | object | No | Env vars for this invocation only. | #### Response - `200 OK` ```json { "data": { "sandbox_id": "sbx_01j9xr2t4fk8me3n5q", "stdout": "2\n", "stderr": "", "exit_code": 0 } } ``` #### Errors | Status | Code | Cause | |--------|------|-------| | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | | 504 | - | Command timed out. | ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "python3 -c \"print(1+1)\"", "timeout": 30}' ``` ### Open a Terminal Session (sandbox) **`POST /api/v1/sandboxes/{id}/terminal`** Creates a PTY session inside a running sandbox and returns a short-lived stream token for browser WebSocket clients. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `cols` | integer | No | Initial terminal width. Defaults to `80`. | | `rows` | integer | No | Initial terminal height. Defaults to `24`. | | `shell` | string | No | Shell command. Defaults to login bash when available. | #### Response - `201 Created` ```json { "session_id": "52d1df5a0a3a0018", "ws_url": "wss://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/terminal/stream", "stream_auth": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...", "stream_auth_expires_at": "2026-05-13T12:30:00Z" } ``` Open the WebSocket with: ```text wss://api.miosa.ai/api/v1/sandboxes/{id}/terminal/stream?session_id={session_id}&cols=120&rows=32&token={stream_auth} ``` Clients should send raw terminal input as binary frames. Resize events are sent as JSON text frames: ```json {"type":"resize","cols":120,"rows":32} ``` Delete the session when finished: ```bash curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/{id}/terminal/{session_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## See also - [Streaming Exec](/docs/api-reference/streaming-exec/) - SSE stream for long-running commands - [Computers API](/docs/api-reference/computers/) - computer CRUD and lifecycle - [Files API](/docs/api-reference/files/) - read and write files without exec - [Error Codes](/docs/api-reference/errors/) - `COMPUTER_NOT_RUNNING`, `AGENT_UNAVAILABLE` --- # Files API URL: https://miosa.ai/docs/api-reference/files Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/files Source: src/routes/docs/api-reference/files/+page.md Description: API reference for file upload, download, list, export, and delete operations. Manage files on a running computer. All paths are restricted to `/home/user`, `/home/ubuntu`, and `/tmp`. Base path: `/api/v1/computers/{id}/files` The computer must be in `"running"` status. All operations go through the computer command service. --- ## Upload a File **`POST /api/v1/computers/{id}/files/upload`** Uploads a file to the computer via multipart form data. ### Request Content-Type: `multipart/form-data` | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | file | Yes | File to upload | | `path` | string | No | Remote path. If ends with `/`, filename is appended. Default: `/home/ubuntu/` | ### Response - `201 Created` ```json { "success": true, "file": { "path": "/workspace/script.py", "filename": "script.py", "size_bytes": 1234 } } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_FILE` | No file field in request | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 413 | `FILE_TOO_LARGE` | File exceeds the 500 MB upload limit | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/upload \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -F "file=@./script.py" \ -F "path=/workspace/scripts/" ``` --- ## List Directory **`GET /api/v1/computers/{id}/files`** Lists files and directories at a given path. ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | No | Directory to list. Default: `/home/ubuntu` | ### Response - `200 OK` ```json { "files": [ { "name": "Documents", "type": "directory", "size_bytes": 4096, "modified_at": "2026-04-11T10:30:00Z" }, { "name": "script.py", "type": "file", "size_bytes": 1234, "modified_at": "2026-04-11T10:25:00Z" } ], "path": "/workspace" } ``` ### File Entry Fields | Field | Type | Description | |-------|------|-------------| | `name` | string | File or directory name | | `type` | string | `"file"` or `"directory"` | | `size_bytes` | integer | Size in bytes | | `modified_at` | ISO 8601 | Last modification time | ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files?path=/workspace" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Download a File **`GET /api/v1/computers/{id}/files/download`** Downloads a file from the computer as binary data. ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Full path to the file | ### Response - `200 OK` Binary file content. Content-Type is inferred from the file extension. The `Content-Disposition` header is set for download. ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_PATH` | No path parameter | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files/download?path=/workspace/output.txt" \ -H "Authorization: Bearer $MIOSA_API_KEY" \ --output output.txt ``` --- ## Export a File **`POST /api/v1/computers/{id}/files/export`** Returns file metadata and base64-encoded content in a single response. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full path to the file | ### Response - `200 OK` ```json { "success": true, "file": { "path": "/workspace/data.json", "filename": "data.json", "size_bytes": 5678, "content_type": "application/json", "modified_at": "2026-04-11T10:30:00Z" }, "content_base64": "eyJrZXkiOiAidmFsdWUifQ==" } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_PATH` | No path in request body | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 404 | `FILE_NOT_FOUND` | File does not exist or is empty | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/export \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/data.json"}' ``` --- ## Delete a File **`DELETE /api/v1/computers/{id}/files`** Removes a file from the computer. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full path to the file to delete | ### Response - `200 OK` ```json { "success": true, "path": "/workspace/old-file.txt" } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_PATH` | No path in request body | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/files \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/old-file.txt"}' ``` --- ## Path Security All paths are validated before execution: 1. **Expansion** - `Path.expand` resolves `..`, `~`, and symlinks 2. **Prefix check** - Expanded path must start with `/home/user`, `/home/ubuntu`, or `/tmp` 3. **Rejection** - Paths outside these prefixes return `403 FORBIDDEN_PATH` This prevents path traversal attacks. For example, `/workspace/../../etc/passwd` expands to `/etc/passwd` and is rejected. --- ## Sandbox files Sandbox files live under `/api/v1/sandboxes/{id}`. The sandbox must be in `running` state. ### Upload a File (sandbox) **`POST /api/v1/sandboxes/{id}/files`** Writes a file to the sandbox filesystem. Two request formats are accepted. #### Request Body - JSON | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Absolute destination path inside the sandbox. | | `content` | string | Yes | Base64-encoded file content. | ```json { "path": "/workspace/script.py", "content": "cHJpbnQoJ2hlbGxvJyk=" } ``` #### Request Body - Multipart Alternatively, send `multipart/form-data` with fields: | Field | Description | |-------|-------------| | `path` | Absolute destination path. | | `file` | File part containing the raw content. | #### Response - `200 OK` ```json { "data": { "sandbox_id": "sbx_01j9xr2t4fk8me3n5q", "path": "/workspace/script.py", "size": 16 } } ``` #### Errors | Status | Code | Cause | |--------|------|-------| | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | | 413 | - | File exceeds the 500 MB upload limit. | ```bash # JSON (base64-encoded content) curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/files \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"path\": \"/workspace/script.py\", \"content\": \"$(base64 -w0 script.py)\"}" # Multipart curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/files \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -F "path=/workspace/script.py" \ -F "file=@script.py" ``` ### Download a File (sandbox) **`GET /api/v1/sandboxes/{id}/files/{path}`** Downloads a file from the sandbox filesystem. | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. Must be in `running` state. | | `path` | string | URL-encoded path to the file inside the sandbox (leading `/` stripped). | #### Response - `200 OK` Raw file bytes with `Content-Type: application/octet-stream`. #### Errors | Status | Code | Cause | |--------|------|-------| | 404 | `FILE_NOT_FOUND` | Path does not exist inside the sandbox. | | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | ```bash curl -o output.json \ "https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/files/workspace%2Foutput.json" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ### Export Files (sandbox) Exports are for files an agent created inside the sandbox that your product needs to show or download: static HTML, PDFs, CSVs, screenshots, generated reports, ZIPs, and build folders. **`POST /api/v1/sandboxes/{id}/exports`** Creates a ready export descriptor. This does not move the file out of the sandbox; it returns download URLs backed by the sandbox filesystem. | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes, unless `paths` is set | Single file path inside the sandbox. | | `paths` | array | Yes, unless `path` is set | Multiple file paths to package as a tar.gz archive. | | `label` | string | No | Human-readable export label. | | `filename` | string | No | Suggested filename for download responses. | #### Response - `200 OK` ```json { "data": { "id": "exp_01j9xr2t4fk8me3n5q", "sandbox_id": "sbx_01j9xr2t4fk8me3n5q", "label": "homepage build", "status": "ready", "files": [ { "path": "/workspace/dist/index.html", "filename": "index.html", "download_url": "https://api.miosa.ai/api/v1/sandboxes/sbx_.../exports/download?path=..." } ], "archive_download_url": "https://api.miosa.ai/api/v1/sandboxes/sbx_.../exports/download?paths%5B%5D=...", "created_at": "2026-06-14T21:00:00Z" } } ``` **`GET /api/v1/sandboxes/{id}/exports/download?path=/workspace/report.pdf`** Downloads one file as binary. **`GET /api/v1/sandboxes/{id}/exports/download?paths[]=/workspace/dist/index.html&paths[]=/workspace/dist/app.js`** Downloads multiple paths as a tar.gz archive. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/$SBX/exports \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"paths":["/workspace/dist/index.html","/workspace/dist/app.js"],"label":"build"}' curl "https://api.miosa.ai/api/v1/sandboxes/$SBX/exports/download?path=/workspace/report.pdf" \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -o report.pdf ``` ### Read Logs (sandbox) **`GET /api/v1/sandboxes/{id}/logs`** Returns recent sandbox/template lifecycle logs. Pass `lines` to control the tail length. ```bash curl "https://api.miosa.ai/api/v1/sandboxes/{id}/logs?lines=200" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` Stream logs with SSE: ```bash curl -N "https://api.miosa.ai/api/v1/sandboxes/{id}/logs/stream" \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Accept: text/event-stream" ``` --- # Filesystem API URL: https://miosa.ai/docs/api-reference/filesystem Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/filesystem Source: src/routes/docs/api-reference/filesystem/+page.md Description: Complete API reference for file and directory operations on MIOSA computers - upload, download, stat, mkdir, rename, copy, chmod, and readdir. The Filesystem API gives you full programmatic access to files and directories inside a running computer. All operations are proxied through the computer command service and restricted to allowed paths. Base path: `/api/v1/computers/{id}/files` **Allowed paths:** `/workspace`, `/home`, `/root`, `/tmp`, `/opt`, `/srv` The computer must be in `"running"` status. Paths outside the allowed prefixes return `403 FORBIDDEN_PATH`. --- ## Quick Start ```typescript const client = new Miosa(); // Write a file await client.files.write(computerId, { path: '/workspace/hello.py', content: 'print("Hello from MIOSA")', }); // List the directory const { files } = await client.files.list(computerId, '/workspace'); // Download it back const content = await client.files.download(computerId, '/workspace/hello.py'); ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/files/upload` | Upload a file (multipart) | | `POST` | `/files/write` | Write text/binary content directly | | `GET` | `/files` | List directory (`readdir`) | | `GET` | `/files/download` | Download a file | | `POST` | `/files/export` | Download with metadata (base64) | | `DELETE` | `/files` | Delete a file | | `GET` | `/files/stat` | Stat a file or directory | | `POST` | `/files/mkdir` | Create a directory | | `POST` | `/files/rename` | Rename or move a file/directory | | `POST` | `/files/copy` | Copy a file | | `POST` | `/files/chmod` | Change file permissions | --- ## Upload a File **`POST /api/v1/computers/{id}/files/upload`** Multipart upload - use this for binary files or large payloads. ### Request - `multipart/form-data` | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | file | Yes | File to upload | | `path` | string | No | Remote path. If ends with `/`, filename is appended. Default: `/workspace/` | ### Response - `201 Created` ```json { "success": true, "file": { "path": "/workspace/script.py", "filename": "script.py", "size_bytes": 1234 } } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_FILE` | No file field in request | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 413 | `FILE_TOO_LARGE` | File exceeds 10 MB limit | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/upload \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -F "file=@./script.py" \ -F "path=/workspace/scripts/" ``` --- ## Write a File **`POST /api/v1/computers/{id}/files/write`** Write text or base64-encoded binary content. Faster than multipart for programmatic use. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full destination path | | `content` | string | Yes | File content (UTF-8 text or base64 when `encoding="base64"`) | | `encoding` | string | No | `"utf8"` (default) or `"base64"` | ### Response - `201 Created` ```json { "success": true, "file": { "path": "/workspace/config.json", "size_bytes": 256 } } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/write \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/config.json", "content": "{\"key\": \"value\"}"}' ``` --- ## List Directory (readdir) **`GET /api/v1/computers/{id}/files`** ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | No | Directory to list. Default: `/workspace` | ### Response - `200 OK` ```json { "files": [ { "name": "Documents", "type": "directory", "size_bytes": 4096, "permissions": "drwxr-xr-x", "modified_at": "2026-04-11T10:30:00Z" }, { "name": "script.py", "type": "file", "size_bytes": 1234, "permissions": "-rw-r--r--", "modified_at": "2026-04-11T10:25:00Z" } ], "path": "/workspace" } ``` ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files?path=/workspace" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Download a File **`GET /api/v1/computers/{id}/files/download`** ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Full path to the file | ### Response - `200 OK` Binary file content. `Content-Disposition` is set for browser download. ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_PATH` | No path parameter | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 404 | `FILE_NOT_FOUND` | File does not exist | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files/download?path=/workspace/output.txt" \ -H "Authorization: Bearer $MIOSA_API_KEY" \ --output output.txt ``` --- ## Export a File **`POST /api/v1/computers/{id}/files/export`** Returns metadata and base64-encoded content in a single JSON response. Useful when you need the file and its metadata together without handling binary responses. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full path to the file | ### Response - `200 OK` ```json { "success": true, "file": { "path": "/workspace/data.json", "filename": "data.json", "size_bytes": 5678, "content_type": "application/json", "modified_at": "2026-04-11T10:30:00Z" }, "content_base64": "eyJrZXkiOiAidmFsdWUifQ==" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/export \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/data.json"}' ``` --- ## Delete a File **`DELETE /api/v1/computers/{id}/files`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full path to delete | ### Response - `200 OK` ```json { "success": true, "path": "/workspace/old-file.txt" } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/files \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/old-file.txt"}' ``` --- ## Stat a File or Directory **`GET /api/v1/computers/{id}/files/stat`** Returns metadata without downloading file content. ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Full path | ### Response - `200 OK` ```json { "path": "/workspace/script.py", "type": "file", "size_bytes": 1234, "permissions": "-rw-r--r--", "owner": "user", "group": "user", "modified_at": "2026-04-11T10:25:00Z", "created_at": "2026-04-11T09:00:00Z" } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 404 | `FILE_NOT_FOUND` | Path does not exist | ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files/stat?path=/workspace/script.py" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Create a Directory **`POST /api/v1/computers/{id}/files/mkdir`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Directory path to create | | `recursive` | boolean | No | Create parent directories if missing (default: `false`) | ### Response - `201 Created` ```json { "success": true, "path": "/workspace/project/src" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/mkdir \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/project/src", "recursive": true}' ``` --- ## Rename or Move **`POST /api/v1/computers/{id}/files/rename`** Renames a file or directory, or moves it to a different path. Both source and destination must be within allowed paths. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `source` | string | Yes | Current path | | `destination` | string | Yes | Target path | ### Response - `200 OK` ```json { "success": true, "source": "/workspace/old.txt", "destination": "/workspace/new.txt" } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 404 | `FILE_NOT_FOUND` | Source path does not exist | | 409 | `DESTINATION_EXISTS` | Destination already exists | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/rename \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"source": "/workspace/app.py", "destination": "/workspace/app_v2.py"}' ``` --- ## Copy a File **`POST /api/v1/computers/{id}/files/copy`** Copies a file to a new path. Directory copy is not supported - copy individual files. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `source` | string | Yes | Source file path | | `destination` | string | Yes | Destination file path | ### Response - `201 Created` ```json { "success": true, "source": "/workspace/template.py", "destination": "/workspace/project/main.py" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/copy \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"source": "/workspace/template.py", "destination": "/workspace/project/main.py"}' ``` --- ## Change Permissions **`POST /api/v1/computers/{id}/files/chmod`** Sets UNIX file permissions. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | File path | | `mode` | string | Yes | Octal permission string, e.g. `"755"` or `"644"` | ### Response - `200 OK` ```json { "success": true, "path": "/workspace/script.sh", "mode": "755" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/chmod \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/script.sh", "mode": "755"}' ``` --- ## Path Security All paths are validated before execution: 1. **Expansion** - `Path.expand` resolves `..`, `~`, and symlinks 2. **Prefix check** - Must start with `/workspace`, `/home`, `/root`, `/tmp`, `/opt`, or `/srv` 3. **Rejection** - Paths outside these prefixes return `403 FORBIDDEN_PATH` `/workspace/../../etc/passwd` expands to `/etc/passwd` and is rejected. --- ## Common Recipes ### Scaffold a project directory ```typescript // Create structure await client.files.mkdir(computerId, { path: '/workspace/app/src', recursive: true }); await client.files.mkdir(computerId, { path: '/workspace/app/tests', recursive: true }); // Write files await client.files.write(computerId, { path: '/workspace/app/src/main.py', content: mainPy }); await client.files.write(computerId, { path: '/workspace/app/requirements.txt', content: reqs }); // Make entrypoint executable await client.files.chmod(computerId, { path: '/workspace/app/src/main.py', mode: '755' }); ``` ### Retrieve build artifacts ```python # List the build output directory files = client.files.list(computer_id, path="/workspace/app/dist") for f in files["files"]: if f["type"] == "file": content = client.files.export(computer_id, path=f"/workspace/app/dist/{f['name']}") save_artifact(f["name"], content["content_base64"]) ``` --- ## Sandbox filesystem The sandbox primitive exposes the same list and stat operations under `/api/v1/sandboxes/{id}`. ### List Files (sandbox) **`GET /api/v1/sandboxes/{id}/files?path=/workspace`** Lists files and directories in a sandbox directory. ```bash curl "https://api.miosa.ai/api/v1/sandboxes/{id}/files?path=/workspace" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` Response: ```json { "data": { "path": "/workspace", "entries": [ {"name": "index.html", "path": "/workspace/index.html", "is_dir": false, "size": 128} ] } } ``` ### Stat a File (sandbox) **`POST /api/v1/sandboxes/{id}/files/stat`** Returns metadata for one file or directory. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/files/stat \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path":"/workspace/index.html"}' ``` --- # Invites API URL: https://miosa.ai/docs/api-reference/invites Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/invites Source: src/routes/docs/api-reference/invites/+page.md Description: API reference for the workspace invite and org invite flows - send, list, revoke, preview, and accept email invitations. MIOSA has two parallel invite flows: - **Workspace invites** - invite a user to a specific workspace. If the email already belongs to an org member, the user is added directly (no email sent). If not, an invite email is dispatched and accepting it creates both the `tenant_members` and `workspace_members` rows atomically. - **Org invites** - invite a user to join the org (tenant) without pre-selecting a workspace. The accept step only creates the `tenant_members` row. Both flows produce opaque tokens. The `GET /invites/:token` and `GET /workspace-invites/:token` preview endpoints are **public** - they can be called without an API key to render a landing page before the user signs in. Email invite links should point at the MIOSA frontend, not the API host. Use `https://miosa.ai/invites/{token}` for org invites and `https://miosa.ai/workspace-invites/{token}` for workspace invites. Legacy `https://miosa.ai/invite/{token}` links redirect to the org invite landing page for compatibility. --- ## Workspace Invite Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/workspaces/{id}/invites` | Required | Send a workspace invite | | `GET` | `/workspaces/{id}/invites` | Required | List pending invites | | `DELETE` | `/workspaces/{id}/invites/{invite_id}` | Required | Revoke an invite | | `GET` | `/workspace-invites/{token}` | None (public) | Preview invite details | | `POST` | `/workspace-invites/{token}/accept` | Required | Accept the invite | ## Org Invite Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/tenants/{id}/invites` | Required (admin/owner) | Send an org invite | | `GET` | `/tenants/{id}/invites` | Required (admin/owner) | List pending org invites | | `DELETE` | `/tenants/{id}/invites/{invite_id}` | Required (admin/owner) | Revoke an org invite | | `GET` | `/invites/{token}` | None (public) | Preview org invite details | | `POST` | `/invites/{token}/accept` | Required | Accept the org invite | --- ## Workspace Invite Flow ### Send Workspace Invite **`POST /api/v1/workspaces/{id}/invites`** Sends an invite email and returns the invite record. If the `email` already belongs to a tenant member the user is added directly (response `type = "added"`, no email sent). #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `email` | string | Yes | Email address to invite | | `role` | string | Yes | `owner`, `admin`, `member`, or `viewer` | #### Response - `201 Created` (new invite) ```json { "type": "invited", "data": { "id": "inv_abc123", "workspace_id": "ws-uuid", "tenant_id": "ten-uuid", "email": "alice@example.com", "role": "member", "invited_by": "usr_owner", "expires_at": "2026-05-29T09:00:00Z", "accepted_at": null, "inserted_at": "2026-05-22T09:00:00Z" } } ``` #### Response - `200 OK` (existing member added directly) ```json { "type": "added", "data": { "user_id": "usr_def456", "workspace_id": "ws-uuid", "role": "member", "joined_at": "2026-05-22T09:00:00Z", "added_by": "usr_owner" } } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/workspaces/{id}/invites \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "alice@example.com", "role": "member"}' ``` ```typescript const client = new Miosa(); const result = await client.workspaceInvites.create('ws-uuid', { email: 'alice@example.com', role: 'member', }); // result.type === 'invited' | 'added' ``` ```python client = miosa.Miosa() result = client.workspace_invites.create("ws-uuid", "alice@example.com", "member") print(result["type"]) # "invited" or "added" ``` ```go result, err := client.WorkspaceInvites.Create(ctx, "ws-uuid", miosa.CreateWorkspaceInviteInput{ Email: "alice@example.com", Role: "member", }) ``` ```elixir {:ok, result} = Miosa.WorkspaceInvites.create(client, "ws-uuid", "alice@example.com", "member") IO.inspect(result["type"]) ``` ```java Map result = miosa.workspaceInvites().create("ws-uuid", "alice@example.com", "member"); String type = (String) result.get("type"); // "invited" or "added" ``` --- ### List Workspace Invites **`GET /api/v1/workspaces/{id}/invites`** Returns all pending workspace invites (excludes accepted and revoked). #### Response - `200 OK` ```json { "data": [ { "id": "inv_abc123", "workspace_id": "ws-uuid", "tenant_id": "ten-uuid", "email": "alice@example.com", "role": "member", "invited_by": "usr_owner", "expires_at": "2026-05-29T09:00:00Z", "accepted_at": null, "inserted_at": "2026-05-22T09:00:00Z" } ] } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/invites \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const invites = await client.workspaceInvites.list('ws-uuid'); ``` ```python invites = client.workspace_invites.list("ws-uuid") ``` ```go invites, err := client.WorkspaceInvites.List(ctx, "ws-uuid") ``` ```elixir {:ok, invites} = Miosa.WorkspaceInvites.list(client, "ws-uuid") ``` ```java List> invites = miosa.workspaceInvites().list("ws-uuid"); ``` --- ### Revoke Workspace Invite **`DELETE /api/v1/workspaces/{id}/invites/{invite_id}`** Revokes a pending invite. Returns `409` if the invite was already accepted. #### Response - `200 OK` ```json { "invite_id": "inv_abc123", "revoked": true } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/workspaces/{id}/invites/{invite_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript await client.workspaceInvites.revoke('ws-uuid', 'inv_abc123'); ``` ```python client.workspace_invites.revoke("ws-uuid", "inv_abc123") ``` ```go _, err := client.WorkspaceInvites.Revoke(ctx, "ws-uuid", "inv_abc123") ``` ```elixir {:ok, _} = Miosa.WorkspaceInvites.revoke(client, "ws-uuid", "inv_abc123") ``` ```java miosa.workspaceInvites().revoke("ws-uuid", "inv_abc123"); ``` --- ### Preview Workspace Invite (public) **`GET /api/v1/workspace-invites/{token}`** Returns invite metadata without requiring authentication. Use this to render the pre-auth landing page. #### Response - `200 OK` ```json { "data": { "workspace_name": "Acme Corp", "role": "member", "expires_at": "2026-05-29T09:00:00Z", "expired": false, "accepted": false } } ``` ```bash curl https://api.miosa.ai/api/v1/workspace-invites/{token} ``` ```typescript const preview = await client.workspaceInvites.preview(token); if (!preview.expired) { // show accept button } ``` ```python preview = client.workspace_invites.preview(token) if not preview["expired"]: pass # show accept button ``` ```go preview, err := client.WorkspaceInvites.Preview(ctx, token) ``` ```elixir {:ok, preview} = Miosa.WorkspaceInvites.preview(client, token) unless preview["expired"], do: ... ``` ```java Map preview = miosa.workspaceInvites().preview(token); ``` --- ### Accept Workspace Invite **`POST /api/v1/workspace-invites/{token}/accept`** Accepts the invite on behalf of the authenticated user. If the user is new to the org, a `tenant_members` row is created atomically alongside the `workspace_members` row. The caller's API key email must match the invite email (case-insensitive). #### Response - `200 OK` ```json { "accepted": true, "workspace_id": "ws-uuid", "tenant_id": "ten-uuid", "role": "member" } ``` #### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_TOKEN` | Token not found or expired | | 400 | `REVOKED` | Invite was revoked | | 422 | `EMAIL_MISMATCH` | Authenticated user's email differs from invite email | ```bash curl -X POST https://api.miosa.ai/api/v1/workspace-invites/{token}/accept \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const result = await client.workspaceInvites.accept(token); console.log('joined workspace:', result.workspaceId); ``` ```python result = client.workspace_invites.accept(token) print("joined workspace:", result["workspace_id"]) ``` ```go result, err := client.WorkspaceInvites.Accept(ctx, token) ``` ```elixir {:ok, result} = Miosa.WorkspaceInvites.accept(client, token) IO.puts("joined workspace: #{result["workspace_id"]}") ``` ```java Map result = miosa.workspaceInvites().accept(token); ``` --- ## Org Invite Flow Org invites grant membership in the tenant (org) without assigning a specific workspace. Write operations require `admin` or `owner` role. ### Send Org Invite **`POST /api/v1/tenants/{id}/invites`** Dispatches an invite email. The response includes an `invite_url` that is host-aware - on white-label tenants it uses the tenant's custom domain. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `email` | string | Yes | Email address to invite | | `role` | string | Yes | `owner`, `admin`, or `member` | #### Response - `201 Created` ```json { "data": { "invite_id": "inv_xyz789", "email": "bob@example.com", "role": "member", "expires_at": "2026-05-29T09:00:00Z", "invite_url": "https://miosa.ai/invites/eyJ..." } } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/tenants/{id}/invites \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "bob@example.com", "role": "member"}' ``` ```typescript const invite = await client.orgInvites.create(tenantId, { email: 'bob@example.com', role: 'member', }); console.log('invite URL:', invite.inviteUrl); ``` ```python invite = client.org_invites.create(tenant_id, "bob@example.com", "member") print("invite URL:", invite["invite_url"]) ``` ```go invite, err := client.OrgInvites.Create(ctx, tenantID, miosa.CreateOrgInviteInput{ Email: "bob@example.com", Role: "member", }) ``` ```elixir {:ok, invite} = Miosa.OrgInvites.create(client, tenant_id, "bob@example.com", "member") IO.puts("invite URL: #{invite["invite_url"]}") ``` ```java Map invite = miosa.orgInvites().create(tenantId, "bob@example.com", "member"); System.out.println("invite URL: " + invite.get("invite_url")); ``` --- ### List Org Invites **`GET /api/v1/tenants/{id}/invites`** Returns all pending org invites. ```bash curl https://api.miosa.ai/api/v1/tenants/{id}/invites \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const invites = await client.orgInvites.list(tenantId); ``` ```python invites = client.org_invites.list(tenant_id) ``` ```go invites, err := client.OrgInvites.List(ctx, tenantID) ``` ```elixir {:ok, invites} = Miosa.OrgInvites.list(client, tenant_id) ``` ```java List> invites = miosa.orgInvites().list(tenantId); ``` --- ### Revoke Org Invite **`DELETE /api/v1/tenants/{id}/invites/{invite_id}`** Revokes a pending org invite. Returns `409` if the invite was already accepted. ```bash curl -X DELETE https://api.miosa.ai/api/v1/tenants/{id}/invites/{invite_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript await client.orgInvites.revoke(tenantId, inviteId); ``` ```python client.org_invites.revoke(tenant_id, invite_id) ``` ```go _, err := client.OrgInvites.Revoke(ctx, tenantID, inviteID) ``` ```elixir {:ok, _} = Miosa.OrgInvites.revoke(client, tenant_id, invite_id) ``` ```java miosa.orgInvites().revoke(tenantId, inviteId); ``` --- ### Preview Org Invite (public) **`GET /api/v1/invites/{token}`** Returns invite metadata without authentication. #### Response - `200 OK` ```json { "data": { "email": "bob@example.com", "tenant_name": "Acme Corp", "role": "member", "expires_at": "2026-05-29T09:00:00Z", "expired": false, "accepted": false } } ``` ```bash curl https://api.miosa.ai/api/v1/invites/{token} ``` ```typescript const preview = await client.orgInvites.preview(token); console.log('invited to org:', preview.tenantName); ``` ```python preview = client.org_invites.preview(token) print("invited to org:", preview["tenant_name"]) ``` ```go preview, err := client.OrgInvites.Preview(ctx, token) ``` ```elixir {:ok, preview} = Miosa.OrgInvites.preview(client, token) IO.inspect(preview["tenant_name"]) ``` ```java Map preview = miosa.orgInvites().preview(token); ``` --- ### Accept Org Invite **`POST /api/v1/invites/{token}/accept`** Accepts an org invite on behalf of the authenticated user. Creates a `tenant_members` row. The caller's API key email must match the invite email (case-insensitive). #### Response - `200 OK` ```json { "accepted": true, "tenant_id": "ten-uuid", "tenant": { "id": "ten-uuid", "name": "Acme Corp" } } ``` #### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `invalid_token` | Token not found or expired | | 422 | `EMAIL_MISMATCH` | Session email differs from invite email | ```bash curl -X POST https://api.miosa.ai/api/v1/invites/{token}/accept \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const result = await client.orgInvites.accept(token); console.log('joined org:', result.tenantId); ``` ```python result = client.org_invites.accept(token) print("joined org:", result["tenant_id"]) ``` ```go result, err := client.OrgInvites.Accept(ctx, token) ``` ```elixir {:ok, result} = Miosa.OrgInvites.accept(client, token) IO.puts("joined org: #{result["tenant_id"]}") ``` ```java Map result = miosa.orgInvites().accept(token); ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_TOKEN` / `invalid_token` | Token not found, expired, or revoked | | 400 | `REVOKED` | Invite was explicitly revoked | | 409 | `ALREADY_ACCEPTED` | Cannot revoke an invite that was already accepted | | 422 | `EMAIL_MISMATCH` | Authenticated user's email does not match the invite email | | 422 | `MISSING_EMAIL` | `email` field missing from request body | --- ## See also - [Workspace Members API](/docs/api-reference/members) - manage the roster directly (requires prior org membership) - [Workspaces API](/docs/api-reference/workspaces) - workspace CRUD - [Error Codes](/docs/api-reference/errors) - full error reference --- # Workspace Members API URL: https://miosa.ai/docs/api-reference/members Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/members Source: src/routes/docs/api-reference/members/+page.md Description: API reference for managing workspace member rosters - add, remove, and update roles for users inside a workspace. Workspace members are the user roster for a specific workspace. A user must already be an org member (`tenant_members` row) before they can be added to a workspace. The last `owner` in a workspace cannot be removed until another member is promoted to owner. Base path: `/api/v1/workspaces/:id/members` To invite a new user who is not yet an org member, use the [Workspace Invites API](/docs/api-reference/invites) instead. Accepting a workspace invite automatically creates the `tenant_members` row when the user is new to the org. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/workspaces/{id}/members` | List workspace members | | `POST` | `/workspaces/{id}/members` | Add an existing org member to the workspace | | `PATCH` | `/workspaces/{id}/members/{user_id}` | Update a member's role | | `DELETE` | `/workspaces/{id}/members/{user_id}` | Remove a member | --- ## Roles | Role | Description | |------|-------------| | `owner` | Full admin including transfer and deletion | | `admin` | Can manage members and resources | | `member` | Can create and manage resources | | `viewer` | Read-only access | --- ## List Members **`GET /api/v1/workspaces/{id}/members`** Returns all members of the workspace. ### Response - `200 OK` ```json { "data": [ { "user_id": "usr_abc123", "email": "alice@example.com", "name": "Alice", "avatar_url": null, "role": "owner", "joined_at": "2026-05-01T10:00:00Z", "added_by": null }, { "user_id": "usr_def456", "email": "bob@example.com", "name": "Bob", "avatar_url": null, "role": "member", "joined_at": "2026-05-10T14:00:00Z", "added_by": "usr_abc123" } ] } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/members \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const client = new Miosa(); const members = await client.workspaceMembers.list('ws-uuid'); console.log(members); ``` ```python client = miosa.Miosa() members = client.workspace_members.list("ws-uuid") print(members) ``` ```go client := miosa.NewClient(os.Getenv("MIOSA_API_KEY")) members, err := client.WorkspaceMembers.List(ctx, "ws-uuid") ``` ```elixir client = Miosa.client("msk_u_...") {:ok, members} = Miosa.WorkspaceMembers.list(client, "ws-uuid") ``` ```java MiosaClient miosa = new MiosaClient("msk_u_..."); List> members = miosa.workspaceMembers().list("ws-uuid"); ``` --- ## Add Member **`POST /api/v1/workspaces/{id}/members`** Adds an existing org member to the workspace with the given role. The user must already hold a `tenant_members` row for this org. If they are not yet an org member, send a workspace invite via `POST /workspaces/{id}/invites` instead - accepting the invite creates the org membership atomically. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `user_id` | UUID | Yes | The user's ID - must already be a tenant member | | `role` | string | Yes | `owner`, `admin`, `member`, or `viewer` | ### Response - `201 Created` ```json { "data": { "user_id": "usr_def456", "workspace_id": "ws-uuid", "role": "member", "joined_at": "2026-05-22T09:00:00Z", "added_by": "usr_abc123" } } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 422 | `NOT_TENANT_MEMBER` | User is not a member of the parent org | | 422 | `MISSING_USER_ID` | `user_id` field missing | | 422 | `MISSING_ROLE` | `role` field missing | | 409 | - | User is already a workspace member | ```bash curl -X POST https://api.miosa.ai/api/v1/workspaces/{id}/members \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"user_id": "usr_def456", "role": "member"}' ``` ```typescript const record = await client.workspaceMembers.add('ws-uuid', { userId: 'usr_def456', role: 'member', }); ``` ```python record = client.workspace_members.add("ws-uuid", user_id="usr_def456", role="member") ``` ```go record, err := client.WorkspaceMembers.Add(ctx, "ws-uuid", miosa.AddWorkspaceMemberInput{ UserID: "usr_def456", Role: "member", }) ``` ```elixir {:ok, record} = Miosa.WorkspaceMembers.add(client, "ws-uuid", "usr_def456", "member") ``` ```java Map record = miosa.workspaceMembers().add("ws-uuid", "usr_def456", "member"); ``` --- ## Update Member Role **`PATCH /api/v1/workspaces/{id}/members/{user_id}`** Changes a workspace member's role. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `role` | string | Yes | New role: `owner`, `admin`, `member`, or `viewer` | ### Response - `200 OK` Updated workspace member record (same shape as the add response). ### Errors | Status | Code | Cause | |--------|------|-------| | 404 | - | User is not a member of this workspace | | 422 | `MISSING_ROLE` | `role` field missing | ```bash curl -X PATCH https://api.miosa.ai/api/v1/workspaces/{id}/members/{user_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"role": "admin"}' ``` ```typescript const updated = await client.workspaceMembers.updateRole('ws-uuid', 'usr_def456', { role: 'admin', }); ``` ```python updated = client.workspace_members.update_role("ws-uuid", "usr_def456", role="admin") ``` ```go updated, err := client.WorkspaceMembers.UpdateRole(ctx, "ws-uuid", "usr_def456", miosa.UpdateWorkspaceMemberRoleInput{Role: "admin"}) ``` ```elixir {:ok, updated} = Miosa.WorkspaceMembers.update_role(client, "ws-uuid", "usr_def456", "admin") ``` ```java Map updated = miosa.workspaceMembers().updateRole("ws-uuid", "usr_def456", "admin"); ``` --- ## Remove Member **`DELETE /api/v1/workspaces/{id}/members/{user_id}`** Removes a user from the workspace. This does not remove them from the parent org. ### Response - `200 OK` ```json { "deleted": true } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 404 | - | User is not a member of this workspace | | 409 | `LAST_OWNER` | Cannot remove the last workspace owner - promote another member first | ```bash curl -X DELETE https://api.miosa.ai/api/v1/workspaces/{id}/members/{user_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript await client.workspaceMembers.remove('ws-uuid', 'usr_def456'); ``` ```python client.workspace_members.remove("ws-uuid", "usr_def456") ``` ```go err := client.WorkspaceMembers.Remove(ctx, "ws-uuid", "usr_def456") ``` ```elixir {:ok, _} = Miosa.WorkspaceMembers.remove(client, "ws-uuid", "usr_def456") ``` ```java miosa.workspaceMembers().remove("ws-uuid", "usr_def456"); ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 403 | - | Workspace belongs to a different tenant | | 404 | - | Workspace or user not found | | 409 | `LAST_OWNER` | Refusing to remove the sole workspace owner | | 422 | `NOT_TENANT_MEMBER` | User must join the org before joining the workspace | | 422 | `MISSING_USER_ID` | `user_id` body field is required | | 422 | `MISSING_ROLE` | `role` body field is required | --- ## See also - [Workspace Invites API](/docs/api-reference/invites) - invite users by email (creates org membership on accept) - [Workspaces API](/docs/api-reference/workspaces) - workspace CRUD - [Error Codes](/docs/api-reference/errors) - full error reference --- # Network Policy API URL: https://miosa.ai/docs/api-reference/network-policy Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/network-policy Source: src/routes/docs/api-reference/network-policy/+page.md Description: API reference for configuring per-computer network access controls on MIOSA computers. Network policies control what external hosts and ports a computer can reach. Use them to sandbox untrusted workloads, enforce egress restrictions, or prevent AI agents from making unexpected outbound connections. Base path: `/api/v1/computers/{id}/network-policy` Network policies apply at the microVM level. Changing a policy takes effect within seconds without restarting the computer. --- ## Quick Start ```typescript const client = new Miosa(); // Restrict to only allow HTTPS to GitHub and PyPI await client.networkPolicy.update(computerId, { mode: 'allowlist', rules: [ { host: 'github.com', port: 443, protocol: 'tcp' }, { host: 'pypi.org', port: 443, protocol: 'tcp' }, { host: 'files.pythonhosted.org', port: 443, protocol: 'tcp' }, ], }); ``` ```bash curl -X PUT https://api.miosa.ai/api/v1/computers/{id}/network-policy \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mode": "allowlist", "rules": [ { "host": "github.com", "port": 443, "protocol": "tcp" } ] }' ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/computers/{id}/network-policy` | Get current network policy | | `PUT` | `/computers/{id}/network-policy` | Set network policy (replaces existing) | | `DELETE` | `/computers/{id}/network-policy` | Reset to default (unrestricted) | --- ## Get Network Policy **`GET /api/v1/computers/{id}/network-policy`** ### Response - `200 OK` ```json { "data": { "computer_id": "...", "mode": "unrestricted", "rules": [], "updated_at": "2026-04-11T00:00:00Z" } } ``` ### Policy Modes | Mode | Description | |------|-------------| | `unrestricted` | Default. All outbound traffic allowed | | `allowlist` | Only listed hosts/ports permitted | | `denylist` | All traffic permitted except listed rules | | `isolated` | All outbound blocked (DNS and MIOSA internal excluded) | ```bash curl https://api.miosa.ai/api/v1/computers/{id}/network-policy \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Set Network Policy **`PUT /api/v1/computers/{id}/network-policy`** Replaces the entire policy. Partial updates are not supported - send all rules on every PUT. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `mode` | string | Yes | `"unrestricted"`, `"allowlist"`, `"denylist"`, or `"isolated"` | | `rules` | array | No | Required when mode is `"allowlist"` or `"denylist"` | #### Rule Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `host` | string | Yes | Hostname or CIDR block (e.g. `10.0.0.0/8`) | | `port` | integer | No | Port number. Omit to match all ports | | `protocol` | string | No | `"tcp"`, `"udp"`, or `"any"` (default) | ### Response - `200 OK` Updated policy object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 400 | `rules required for allowlist/denylist mode` | Empty rules for a filtering mode | | 404 | `computer not found` | Computer does not exist | ```bash # Full isolation except for GitHub curl -X PUT https://api.miosa.ai/api/v1/computers/{id}/network-policy \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mode": "allowlist", "rules": [ { "host": "github.com", "port": 443, "protocol": "tcp" }, { "host": "objects.githubusercontent.com", "port": 443, "protocol": "tcp" } ] }' ``` --- ## Reset to Default **`DELETE /api/v1/computers/{id}/network-policy`** Removes any custom policy. The computer reverts to unrestricted outbound access. ### Response - `200 OK` ```json { "data": { "computer_id": "...", "mode": "unrestricted" } } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/network-policy \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Common Recipes ### Sandbox an untrusted AI task ```python # Lock down before running untrusted code client.network_policy.update(computer_id, mode="isolated") # Run the task result = client.exec(computer_id, command="python3 /workspace/untrusted.py") # Restore after client.network_policy.delete(computer_id) ``` ### Allow only package registries for a build environment ```typescript const packageHosts = [ { host: 'registry.npmjs.org', port: 443, protocol: 'tcp' as const }, { host: 'pypi.org', port: 443, protocol: 'tcp' as const }, { host: 'files.pythonhosted.org', port: 443, protocol: 'tcp' as const }, { host: 'pkg.go.dev', port: 443, protocol: 'tcp' as const }, { host: 'proxy.golang.org', port: 443, protocol: 'tcp' as const }, ]; await client.networkPolicy.update(computerId, { mode: 'allowlist', rules: packageHosts, }); ``` ### Block a known malicious IP range ```typescript await client.networkPolicy.update(computerId, { mode: 'denylist', rules: [ { host: '185.220.0.0/16' }, ], }); ``` --- # OpenComputers API URL: https://miosa.ai/docs/api-reference/open-computers Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/open-computers Source: src/routes/docs/api-reference/open-computers/+page.md Description: Register and control customer-owned machines, including on-premises servers, via the MIOSA API - exec, files, tunnels, AI agents, inference clusters, and secrets. # OpenComputers API OpenComputers lets you register physical or virtual machines you already own (Mac, Linux, Windows) and control them through MIOSA's API - run commands, manage files, expose HTTP tunnels, dispatch AI agents, build inference clusters, and manage secrets. Hosts may be on-premises servers, workstations, or existing machines at a hosting provider. Available operations depend on the installed agent and the host's capabilities. See [OpenComputers](/docs/opencomputers/) for hardware, connectivity, and data responsibilities. Base path: `/api/v1/opencomputers` Verbs supported: **GET** (list/show), **POST** (register/create/exec/dispatch), **PATCH** (update tags/tunnels), **DELETE** (revoke/cancel). Rate limits vary by key, tenant policy, and endpoint family. Honor `X-RateLimit-*` and `Retry-After` response headers. Exec jobs and agent sessions run asynchronously - poll or stream for results rather than holding the connection. Register the host and follow the generated instructions to connect the supported OpenComputers agent on the target machine. The **host key** returned on registration is shown exactly once - store it securely. --- ## Hosts ### List hosts **`GET /api/v1/opencomputers/hosts`** Returns an object with a `hosts` array. Inspect each host's `id`, `state`, `capabilities`, and `last_heartbeat` before selecting it. Use the returned ID exactly; examples below use placeholders. ```json { "hosts": [ { "id": "", "name": "lab-server", "state": "active", "capabilities": ["exec"], "last_heartbeat": "2026-09-08T12:00:00Z" } ] } ``` This is an abbreviated response. A recent heartbeat and a completed operation are stronger evidence of readiness than a host record alone. --- ### Register a host **`POST /api/v1/opencomputers/hosts`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Display name for the host | | `region` | string | No | Region label | | `platform` | string | No | Host operating-system selection | Registration returns `{ host, host_key, control_url, warning }`. The `host` object contains the new host ID; `host_key` authenticates the installed agent. Use your platform API key for REST requests, not the host key. `host_key` is returned **only once** at registration time. The host agent uses it to authenticate. Store it immediately - it cannot be retrieved later. --- ### Get a host **`GET /api/v1/opencomputers/hosts/{id}`** --- ### Revoke a host **`DELETE /api/v1/opencomputers/hosts/{id}`** Returns `204 No Content`. The host agent will no longer be able to authenticate. --- ### Host event stream (SSE) **`GET /api/v1/opencomputers/hosts/{id}/events`** Streams real-time lifecycle events from the host as Server-Sent Events. ``` event: status_change data: {"type":"status_change","host_id":"host_abc","data":{"status":"online"},"timestamp":"..."} ``` --- ## Jobs Run shell commands on a registered host and retrieve output. ### Run a job **`POST /api/v1/opencomputers/hosts/{id}/exec`** | Field | Type | Required | Description | |---|---|---|---| | `cmd` | string | Yes | Command to execute | | `args` | string[] | No | Arguments passed separately | | `env` | string[] | No | Environment entries in `KEY=VALUE` format | | `cwd` | string | No | Working directory | | `timeout_ms` | integer | No | Timeout in milliseconds; default 30000 | | `stream` | boolean | No | Default `true`: stream SSE; `false`: collect output as JSON | With `stream: true`, the POST response carries `exec_chunk`, `exec_result`, or `exec_error` events. Keep the connection open until the terminal result. With `stream: false`, a completed command returns: ```json { "job_id": "", "exit_code": 0, "stdout": "lab-server\n", "stderr": "" } ``` A timeout or disconnected client is not proof that the process stopped. Inspect the job and host before retrying an operation with side effects. --- ### List jobs **`GET /api/v1/opencomputers/hosts/{id}/jobs`** --- ### Get a job **`GET /api/v1/opencomputers/hosts/{id}/exec/{job_id}`** --- ### Stream job output (SSE) **`GET /api/v1/opencomputers/hosts/{id}/exec/{job_id}/stream`** Streams stdout/stderr in real time while the job is running. --- ### Cancel a job **`DELETE /api/v1/opencomputers/hosts/{id}/exec/{job_id}`** Returns `204 No Content`. --- ## File System Manage files and directories on a registered host. | Endpoint | Method | Description | |----------|--------|-------------| | `/opencomputers/hosts/{id}/fs/list` | GET | List directory (`?path=`) | | `/opencomputers/hosts/{id}/fs/stat` | GET | Stat a path (`?path=`) | | `/opencomputers/hosts/{id}/fs/download` | GET | Download a file (`?path=`) | | `/opencomputers/hosts/{id}/fs/upload` | POST | Upload a file (multipart, `?path=`) | | `/opencomputers/hosts/{id}/fs/delete` | DELETE | Delete file/dir (`?path=`) | | `/opencomputers/hosts/{id}/fs/mkdir` | POST | Create directory (`{"path":"..."}`) | **List response:** ```json { "path": "/workspace/projects", "entries": [ { "name": "my-app", "path": "/workspace/projects/my-app", "size": 0, "is_dir": true, "modified_at": "2026-01-01T00:00:00Z" } ] } ``` --- ## Terminal ### Issue a terminal ticket **`POST /api/v1/opencomputers/hosts/{id}/terminal/ticket`** Returns a short-lived WebSocket authentication ticket. Connect to `ws_url` immediately using the ticket as a query parameter. ```json { "ticket": "tk_abc123", "ws_url": "wss://api.miosa.ai/opencomputers/ws/terminal?ticket=tk_abc123", "expires_at": "2026-01-01T00:00:30Z" } ``` --- ## Desktop (VNC) ### Issue a desktop ticket **`POST /api/v1/opencomputers/hosts/{id}/desktop/ticket`** Same shape as the terminal ticket. Connect to `ws_url` with the ticket to start a VNC session. --- ## Tunnels Expose local ports on the host over MIOSA-managed public URLs. ### List tunnels **`GET /api/v1/opencomputers/hosts/{id}/tunnels`** ### Create a tunnel **`POST /api/v1/opencomputers/hosts/{id}/tunnels`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `target_port` | integer | Yes | Local port to expose | | `auth_mode` | string | No | `public` \| `tenant_only` \| `password` (default: `tenant_only`) | | `slug` | string | No | Custom slug for the public URL | ```json { "id": "tun_abc", "host_id": "host_abc123", "slug": "my-app-dev", "target_port": 3000, "auth_mode": "tenant_only", "public_url": "https://api.miosa.ai/t/my-app-dev", "enabled": true, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ``` ### Get / Update / Delete a tunnel **`GET /api/v1/opencomputers/hosts/{id}/tunnels/{tunnel_id}`** **`PATCH /api/v1/opencomputers/hosts/{id}/tunnels/{tunnel_id}`** | Field | Type | Description | |-------|------|-------------| | `target_port` | integer | Change target port | | `auth_mode` | string | Change auth mode | | `enabled` | boolean | Enable or disable the tunnel | **`DELETE /api/v1/opencomputers/hosts/{id}/tunnels/{tunnel_id}`** - `204 No Content` --- ## Agents Dispatch an AI agent to complete a task autonomously on the host. Dispatch over the API with `miosa api`: ```bash miosa api POST /opencomputers/hosts/host_abc123/agent/dispatch \ -d '{"task": "Audit the local browser workflow"}' ``` ### Dispatch an agent **`POST /api/v1/opencomputers/hosts/{id}/agent/dispatch`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `task` | string | Yes | Natural-language task description | | `model` / `model_id` | string | No | Override the default runtime/model | | `tools` | string[] | No | Tool names to expose to the agent session | | `budget.max_steps` | integer | No | Maximum agent steps (default: 30) | | `budget.max_tokens` | integer | No | Maximum token budget (default: 100000) | | `budget.timeout_ms` | integer | No | Session timeout in milliseconds | | `agent_runtime_profile_id` | UUID/string | No | Force a specific runtime profile | | `skip_agent_runtime_profile` | boolean | No | Do not apply the tenant/workspace default profile | | `context` | object | No | Additional context key-value pairs | ```json { "id": "sess_abc", "session_id": "sess_abc", "host_id": "host_abc123", "task": "Run the test suite and fix any failing tests", "status": "running", "sse_url": "/api/v1/opencomputers/hosts/host_abc123/agent/sessions/sess_abc/events", "optimal_session_id": "opt_sess_123", "agent_runtime_profile_id": "arp_123", "runtime_context": { "agent_runtime_profile": { "id": "arp_123", "runtime": "claude-code", "tools": ["browser", "filesystem"], "env_keys": ["ANTHROPIC_API_KEY"] } } } ``` **Session statuses:** `running` | `succeeded` | `failed` | `canceled` ### List / Get sessions **`GET /api/v1/opencomputers/hosts/{id}/agent/sessions`** **`GET /api/v1/opencomputers/hosts/{id}/agent/sessions/{session_id}`** ### Stream agent events (SSE) **`GET /api/v1/opencomputers/hosts/{id}/agent/sessions/{session_id}/events`** ### Cancel a session **`DELETE /api/v1/opencomputers/hosts/{id}/agent/sessions/{session_id}`** - `204 No Content` --- ## Inference Clusters Group multiple hosts to serve an LLM over an OpenAI-compatible endpoint. ### List clusters **`GET /api/v1/opencomputers/clusters`** ### Create a cluster **`POST /api/v1/opencomputers/clusters`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Cluster name | | `model` | string | Yes | Model to serve (e.g. `llama3:70b`) | | `host_ids` | string[] | Yes | IDs of hosts in the cluster | ```json { "id": "cl_abc", "name": "my-cluster", "model": "llama3:70b", "slug": "my-cluster", "status": "active", "host_ids": ["host_abc123"], "inference_url": "https://api.miosa.ai/inference/my-cluster/v1", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ``` The `inference_url` is OpenAI-compatible: ``` POST {inference_url}/chat/completions ``` ### Get / Start / Stop / Delete **`GET /api/v1/opencomputers/clusters/{id}`** **`POST /api/v1/opencomputers/clusters/{id}/start`** **`POST /api/v1/opencomputers/clusters/{id}/stop`** **`DELETE /api/v1/opencomputers/clusters/{id}`** - `204 No Content` --- ## Secrets Store encrypted key-value secrets accessible to the host agent at runtime. ### Tenant-scoped secrets | Endpoint | Method | Description | |----------|--------|-------------| | `/opencomputers/secrets` | GET | List tenant secrets | | `/opencomputers/secrets` | POST | Create a secret | | `/opencomputers/secrets/{id}` | PATCH | Update value or description | | `/opencomputers/secrets/{id}` | DELETE | Delete a secret | ### Host-scoped secrets | Endpoint | Method | Description | |----------|--------|-------------| | `/opencomputers/hosts/{id}/secrets` | GET | List host secrets | | `/opencomputers/hosts/{id}/secrets` | POST | Create a host secret | | `/opencomputers/hosts/{id}/secrets/{secret_id}` | DELETE | Delete a host secret | **Create request:** ```json { "name": "GITHUB_TOKEN", "value": "ghp_xxx", "description": "CI token" } ``` **Response (value is never returned):** ```json { "id": "sec_abc", "name": "GITHUB_TOKEN", "description": "CI token", "host_id": null, "tenant_id": "t_abc", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ``` --- ## SDK Examples ```typescript const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY }); // Register a host - save host_key immediately const host = await miosa.openComputers.hosts.create({ name: 'my-mac' }); console.log('Host key (save this!):', host.host_key); // Run a command const job = await miosa.openComputers.jobs.run(host.id, { command: 'npm test', }); console.log('Exit code:', job.exit_code); console.log('Output:', job.stdout); // Expose a local dev server const tunnel = await miosa.openComputers.tunnels.create(host.id, { target_port: 3000, }); console.log('Public URL:', tunnel.public_url); // Dispatch an AI agent const session = await miosa.openComputers.agents.dispatch(host.id, { task: 'Run the test suite and fix any failing tests', max_turns: 30, }); console.log('Session:', session.id, session.status); ``` ```python client = miosa.Miosa(api_key="msk_u_...") # Register a host host = client.open_computers.hosts.create( miosa.resources.open_computers.types.HostCreateParams(name="my-mac") ) print("Host key:", host.host_key) # Run a command from miosa.resources.open_computers.types import JobRunParams job = client.open_computers.jobs.run(host.id, JobRunParams(command="npm test")) print(f"Exit code: {job.exit_code}, stdout: {job.stdout}") # Create a tunnel from miosa.resources.open_computers.types import TunnelCreateParams tunnel = client.open_computers.tunnels.create(host.id, TunnelCreateParams(target_port=3000)) print("Public URL:", tunnel.public_url) ``` ```go client := miosa.NewClient(os.Getenv("MIOSA_API_KEY")) // Register a host host, err := client.OpenComputers.Hosts.Create(ctx, miosa.CreateHostInput{ Name: "my-mac", }) if err != nil { log.Fatal(err) } fmt.Println("Host key:", *host.HostKey) // Run a command job, err := client.OpenComputers.Jobs.Run(ctx, host.ID, miosa.RunJobInput{ Command: "npm test", }) if err != nil { log.Fatal(err) } fmt.Printf("Exit code: %d\nStdout: %s\n", *job.ExitCode, *job.Stdout) // Create a tunnel tunnel, err := client.OpenComputers.Tunnels.Create(ctx, host.ID, miosa.CreateTunnelInput{ TargetPort: 3000, }) fmt.Println("Public URL:", tunnel.PublicURL) ``` ```elixir client = Miosa.client("msk_u_...") # Register a host {:ok, host} = Miosa.OpenComputers.Hosts.create(client, %{name: "my-mac"}) IO.puts("Host key: #{host["host_key"]}") # Run a command {:ok, job} = Miosa.OpenComputers.Jobs.run(client, host["id"], %{command: "npm test"}) IO.puts("Exit code: #{job["exit_code"]}") # Create a tunnel {:ok, tunnel} = Miosa.OpenComputers.Tunnels.create(client, host["id"], %{target_port: 3000}) IO.puts("Public URL: #{tunnel["public_url"]}") ``` ```java MiosaClient miosa = new MiosaClient(System.getenv("MIOSA_API_KEY")); // Register a host HostData host = miosa.openComputers().hosts() .create(new CreateHostParams("my-mac")); System.out.println("Host key: " + host.hostKey); // Run a command JobData job = miosa.openComputers().jobs() .run(host.id, new RunJobParams("npm test")); System.out.printf("Exit code: %d%nStdout: %s%n", job.exitCode, job.stdout); // Create a tunnel TunnelData tunnel = miosa.openComputers().tunnels() .create(host.id, new CreateTunnelParams(3000)); System.out.println("Public URL: " + tunnel.publicUrl); ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 404 | `NOT_FOUND` | Host or resource does not exist in this tenant | | 403 | `FORBIDDEN` | Authenticated but not authorized | | 400 | `INVALID_ID` | Path parameter is not a valid ID | | 409 | - | Host is offline; command cannot be dispatched | | 502 | - | Host agent is unreachable | --- ## See also - [Computers API](/docs/api-reference/computers/) - managed MIOSA VMs vs. BYOC hosts - [Exec API](/docs/api-reference/exec/) - exec on managed computers - [Regions](/docs/api-reference/regions/) - region slugs accepted on host creation - [Error Codes](/docs/api-reference/errors/) - complete error code reference --- # Preview Tokens API URL: https://miosa.ai/docs/api-reference/preview-tokens Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/preview-tokens Source: src/routes/docs/api-reference/preview-tokens/+page.md Description: Generate short-lived signed tokens for iframe embedding without exposing an API key. Preview tokens (`mp_*`) are short-lived signed credentials that allow you to embed a running sandbox in an ` ``` The token is verified by the sandbox proxy on every request during the token's lifetime. Once it expires, the iframe will show an access-denied page. --- # Previews and publish API URL: https://miosa.ai/docs/api-reference/previews Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/previews Source: src/routes/docs/api-reference/previews/+page.md Description: Expose a sandbox port as a preview URL, publish a sandbox to a stable deployment, and promote a running sandbox to a deployment route. # Previews and publish Base path: `/api/v1/sandboxes/{id}` For preview token minting (embedding a sandbox in an iframe without an API key), see the [Preview Tokens API](/docs/api-reference/preview-tokens/). ## Expose a Preview Port **`POST /api/v1/sandboxes/{id}/expose`** Returns a public, tenant-aware preview URL for a server running inside the sandbox. Use this when an agent generated a Vite/Next/FastAPI/Flask app and started it on a local port. Inside the sandbox, bind dev servers to `0.0.0.0`, not `localhost`: ```bash npm run dev -- --host 0.0.0.0 --port 5173 python -m http.server 8000 --bind 0.0.0.0 ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `port` | integer | No | Port inside the sandbox to expose. If omitted, MIOSA uses the template lifecycle preview port when available. Must be `1` through `65535` when provided. | ### Response - `200 OK` ```json { "url": "https://5173-sbx01j9x.sandbox.miosa.ai" } ``` If the tenant has a white-label preview domain configured, the same endpoint returns that domain instead of the platform default. Default app ports such as `3000`, `5173`, `8080`, `8000`, and `80` may be returned as `https://{slug}.sandbox.{domain}`; non-default ports use `https://{port}-{slug}.sandbox.{domain}`. ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `MISSING_PARAM` | `port` was omitted and the sandbox template has no default preview port. | | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | | 422 | `INVALID_PORT` | Port is outside `1` through `65535`. | ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/expose \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"port": 5173}' ``` For static artifacts such as PDFs, images, Markdown, CSV, or ZIP files, write them under `/workspace` and download them through the files API. For web apps, start a server and expose the port. --- ## Publish a Sandbox **`POST /api/v1/sandboxes/{id}/publish`** Freezes sandbox source, creates an immutable release and deployment version, verifies the candidate, and promotes it behind a stable deployment URL. Use this endpoint for production publishing. The CLI is the recommended happy path: ```bash miosa deploy create --from-sandbox \ --name \ --type docker_deploy \ --wait \ --json ``` Use the same `--name` on every publish so MIOSA preserves the deployment ID and canonical URL. See [Publishing](/docs/docker-host/publishing/) for request options, proof, and failure behavior. ## Promote a Sandbox Runtime to a Deployment **`POST /api/v1/sandboxes/{id}/deploy`** This compatibility bridge routes a persistent deployment URL directly to a running sandbox. The sandbox remains the runtime behind the route and its auto-destroy timer is cancelled. Use it only when the sandbox itself must remain the runtime. For immutable production versions and Docker Host, use `/publish`. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Human-readable deployment name. Used to create the deployment slug. | | `port` | integer | No | Runtime port to route. Defaults to the template lifecycle preview port, then `80`. | | `domain` | string | No | Custom domain to attach. | | `custom_domain` | string | No | Alias for `domain`, used by frontend clients. | ### Response - `201 Created` ```json { "deployment_id": "dep_01j9xr2t4fk8me3n5q", "url": "https://my-app-a1b2c3.acme.miosa.app", "state": "running" } ``` Managed deployment URLs are tenant-scoped: `https://{deployment-slug}.{tenant-slug}.miosa.app`. MIOSA persists the sandbox runtime target on the deployment and reconciles running routes, so a temporary proxy restart or admin API outage can be repaired from the database. ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `MISSING_PARAM` | `name` was omitted. | | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | | 409 | `SANDBOX_RUNTIME_UNAVAILABLE` | Sandbox is marked running but has no VM IP yet. | | 422 | `INVALID_PORT` | Port is outside `1` through `65535`. | | 422 | `VALIDATION_ERROR` | Deployment record validation failed. | ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/deploy \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"my-app","port":8000}' ``` --- # Projects API URL: https://miosa.ai/docs/api-reference/projects Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/projects Source: src/routes/docs/api-reference/projects/+page.md Description: API reference for creating and managing projects inside MIOSA workspaces. A **Project** is the app, website, lead magnet, document, workflow, or customer build inside a workspace. Projects own sandboxes, computers, deployments, databases, storage buckets, volumes, functions, jobs, auth configuration, integrations, and custom domains. Base path: `/api/v1/projects` If a resource create call includes `project_slug`, `project_name`, or `external_project_id`, MIOSA resolves or creates the project before creating the resource. You can also create projects explicitly with this API. ## Endpoints | Method | Path | Description | |---|---|---| | `GET` | `/projects` | List projects for the organization | | `POST` | `/projects` | Create a project inside a workspace | | `GET` | `/projects/{id}` | Get one project | | `PATCH` | `/projects/{id}` | Update a project | | `GET` | `/workspaces/{id}/projects` | List projects in one workspace | --- ## List Projects **`GET /api/v1/projects`** ### Query Parameters | Parameter | Type | Description | |---|---|---| | `workspace_id` | UUID | Limit results to one workspace | ### Response - `200 OK` ```json { "data": [ { "id": "660e8400-e29b-41d4-a716-446655440001", "tenant_id": "tnt_abc123", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "external_workspace_id": "acct_123", "external_project_id": "project_789", "name": "Lead Magnet", "slug": "lead-magnet", "description": null, "metadata": {}, "settings": {}, "created_at": "2026-05-18T10:00:00Z", "updated_at": "2026-05-18T10:00:00Z" } ], "total": 1 } ``` ```bash curl "https://api.miosa.ai/api/v1/projects?workspace_id=550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Create a Project **`POST /api/v1/projects`** A project must belong to a workspace. Supply one of `workspace_id`, `workspace_slug`, or `external_workspace_id`. ### Request Body | Field | Type | Required | Description | |---|---|---|---| | `workspace_id` | UUID | Conditional | Existing MIOSA workspace ID | | `workspace_slug` | string | Conditional | Existing workspace slug | | `external_workspace_id` | string | Conditional | Your external workspace/customer ID | | `name` | string | Yes | Human-readable project name | | `slug` | string | No | URL-safe identifier. Auto-derived from `name` if omitted. | | `external_project_id` | string | No | Your project/app/document ID | | `description` | string | No | Optional description | | `metadata` | object | No | Caller metadata stored on the project | ### Example ```bash curl -X POST https://api.miosa.ai/api/v1/projects \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workspace_slug": "acme-corp", "name": "Lead Magnet", "slug": "lead-magnet", "external_workspace_id": "acct_123", "external_project_id": "project_789" }' ``` ### Response - `201 Created` Full project object. ### Errors | Status | Code | Cause | |---|---|---| | 400 | `WORKSPACE_REQUIRED` | No `workspace_id`, `workspace_slug`, or `external_workspace_id` was supplied | | 404 | `WORKSPACE_NOT_FOUND` | Workspace selector did not resolve inside the organization | | 422 | `VALIDATION_FAILED` | Name, slug, or external project ID failed validation | --- ## Get a Project **`GET /api/v1/projects/{id}`** Returns one project if it belongs to the authenticated organization. ```bash curl https://api.miosa.ai/api/v1/projects/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update a Project **`PATCH /api/v1/projects/{id}`** ### Request Body | Field | Type | Description | |---|---|---| | `name` | string | New display name | | `slug` | string | New URL-safe slug, unique inside the workspace | | `description` | string | New description (`null` to clear) | | `external_project_id` | string | Your project/app/document ID | | `external_workspace_id` | string | Your customer/workspace ID | | `metadata` | object | Replacement metadata map | | `settings` | object | Replacement settings map. | ```bash curl -X PATCH https://api.miosa.ai/api/v1/projects/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Summer Lead Magnet"}' ``` --- ## Use Projects on Resource Create Most create endpoints accept the same ownership fields: ```json { "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "external_workspace_id": "acct_123", "external_project_id": "project_789" } ``` You can use slugs instead of UUIDs: ```json { "workspace_slug": "acme-corp", "workspace_name": "Acme Corp", "project_slug": "lead-magnet", "project_name": "Lead Magnet" } ``` This applies to sandboxes, computers, deployments, databases, storage buckets, volumes, edge functions, cron jobs, project auth, and integrations. Derived records inherit ownership automatically. --- ## Domains Projects do not own a preview domain. Preview domains are an organization setting, and per-deployment custom domains are attached to the deployment: - [Custom Domains API](/docs/api-reference/custom-domains/) - the organization preview domain and deployment/computer custom domains. ## See also - [Workspaces API](/docs/api-reference/workspaces/) - parent resource - [Ownership and Attribution](/docs/platform/attribution/) - how IDs flow to usage and events - [Sandboxes API](/docs/api-reference/sandboxes/) - create resources inside a project - [Deployments API](/docs/api-reference/deployments/) - publish projects to URLs --- # Quotas API URL: https://miosa.ai/docs/api-reference/quotas Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/quotas Source: src/routes/docs/api-reference/quotas/+page.md Description: Set per-end-user resource limits for white-label multi-tenant deployments. Quotas let you cap how many sandboxes an individual end-user of your product can create and run. They are keyed by `external_user_id` - the same opaque identifier you pass when creating sandboxes on behalf of your users. Base path: `/api/v1/quotas/external/{external_user_id}` Quotas are enforced at sandbox creation time. Setting a quota does not affect sandboxes already running. Deleting a quota removes all limits for that user. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/quotas/external/{external_user_id}` | Get quota and current usage | | `PUT` | `/api/v1/quotas/external/{external_user_id}` | Create or update a quota | | `DELETE` | `/api/v1/quotas/external/{external_user_id}` | Remove quota (unlimited) | --- ## Get Quota **`GET /api/v1/quotas/external/{external_user_id}`** Returns the configured limits and live usage counters for the user. ### Auth ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` ```json { "data": { "external_user_id": "user_42", "limits": { "max_sandboxes": 10, "max_concurrent": 3, "max_storage_gb": 50, "max_credit_cents": 5000 }, "usage": { "total_sandboxes": 4, "concurrent": 1 } } } ``` | Field | Type | Description | |-------|------|-------------| | `limits.max_sandboxes` | integer\|null | Lifetime sandbox creation limit | | `limits.max_concurrent` | integer\|null | Max sandboxes running simultaneously | | `limits.max_storage_gb` | integer\|null | Total attached storage cap in GiB | | `limits.max_credit_cents` | integer\|null | Spend cap in cents (platform credits) | | `usage.total_sandboxes` | integer | Total sandboxes created by this user | | `usage.concurrent` | integer | Currently running sandboxes | A `null` limit means no cap is applied for that dimension. --- ## Set Quota **`PUT /api/v1/quotas/external/{external_user_id}`** Creates or replaces the quota. All limit fields are optional - omit a field to leave it uncapped. ### Request Body ```json { "max_sandboxes": 10, "max_concurrent": 3, "max_storage_gb": 50, "max_credit_cents": 5000 } ``` ### Response - `200 OK` Same shape as GET response. --- ## Delete Quota **`DELETE /api/v1/quotas/external/{external_user_id}`** Removes all limits for the user. Future sandbox creation is governed only by your tenant plan. ### Response - `204 No Content` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 404 | `not_found` | No quota exists for this `external_user_id` (GET/DELETE) | | 422 | validation errors | A limit value is non-integer or negative | --- ## Examples ```bash # Set curl -X PUT https://api.miosa.ai/api/v1/quotas/external/user_42 \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{"max_sandboxes": 10, "max_concurrent": 3}' # Get curl https://api.miosa.ai/api/v1/quotas/external/user_42 \ -H "Authorization: Bearer msk_u_..." # Remove curl -X DELETE https://api.miosa.ai/api/v1/quotas/external/user_42 \ -H "Authorization: Bearer msk_u_..." ``` ```python client = miosa.Miosa() # Set quota when user signs up client.quotas.set("user_42", max_sandboxes=10, max_concurrent=3) # Check quota + usage quota = client.quotas.get("user_42") print(quota.usage.concurrent) # Remove limits client.quotas.delete("user_42") ``` ```typescript const client = new Miosa(); // Set await client.quotas.set('user_42', { maxSandboxes: 10, maxConcurrent: 3, maxStorageGb: 50, }); // Get const quota = await client.quotas.get('user_42'); console.log(quota.usage.concurrent); // Remove await client.quotas.delete('user_42'); ``` --- # Regions API URL: https://miosa.ai/docs/api-reference/regions Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/regions Source: src/routes/docs/api-reference/regions/+page.md Description: List available regions and pin Computers and Sandboxes to a specific region. MIOSA Computers and Sandboxes run in geographic regions. Every Computer (and every Sandbox, since a Sandbox is a lightweight Computer flavor) is created in exactly one region and stays there for its lifetime. Base path: `/api/v1/regions` --- ## Overview - **3 regions are live today.** All have equal status - none are preview, none are restricted. - **Default region** when you don't specify one: `us-west`. - **All VM sizes** (XS, S, M, L, XL) are available in every region for both Computers and Sandboxes. - **Latency** is roughly equidistant for US users - pick by data residency or proximity preference rather than performance. - **No multi-region resources.** A Computer lives in exactly one region. To run workloads in multiple regions, create multiple Computers. --- ## Available Regions | Slug | Location | City | Status | |------------------|-------------------|------------------|--------| | `us-west` | US West | Los Angeles, CA | Live | | `us-east` | US East | New York, NY | Live | | `us-mia` | US East 2 | Miami | Live | The region slug is what you pass on create and what comes back on read - every Computer and Sandbox response includes a `region` field, e.g. `"region": "us-west"`. --- ## Specify a region on create Pass `region` in the request body when creating a Computer or Sandbox. If omitted, the default (`us-west`) is used. ```python computer = client.computers.create( name="ny-build-box", size="M", region="us-east", ) print(computer.region) # "us-east" ``` ```typescript const computer = await client.computers.create({ name: 'ny-build-box', size: 'M', region: 'us-east', }); console.log(computer.region); // "us-east" ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ny-build-box", "size": "M", "region": "us-east" }' ``` The same `region` field works on `POST /api/v1/sandboxes`. ### Errors | Status | Error | Cause | |--------|----------------------|------------------------------------------------| | 400 | `invalid_region` | Slug not in the list above | | 409 | `region_unavailable` | Transient capacity issue - retry or pick another | --- ## Default region behavior If `region` is omitted from the create request: - The resource resolves to your tenant's default region (`us-west` by default; change it in dashboard settings - that override applies to API calls that omit `region`). - The response still includes the resolved `region` so you always know where the resource lives. --- ## List regions **`GET /api/v1/regions`** Returns the regions available to your tenant. Useful for region pickers in UIs. ### Response - `200 OK` ```json { "regions": [ { "id": "us-west", "label": "US-West (Los Angeles)", "active_nodes": 2, "available": true, "small_slots_free": 70, "headroom": { "xs_slots_free": 250, "small_slots_free": 70, "medium_slots_free": 14, "large_slots_free": 7, "total_memory_mb": 524288, "used_memory_mb": 12288 } }, { "id": "us-east", "label": "US-East (New York)", "active_nodes": 1, "available": true, "small_slots_free": 40, "headroom": { "xs_slots_free": 120, "small_slots_free": 40, "medium_slots_free": 8, "large_slots_free": 4, "total_memory_mb": 262144, "used_memory_mb": 6144 } }, { "id": "us-mia", "label": "US-East (Miami)", "active_nodes": 1, "available": true, "small_slots_free": 30, "headroom": { "xs_slots_free": 90, "small_slots_free": 30, "medium_slots_free": 6, "large_slots_free": 3, "total_memory_mb": 262144, "used_memory_mb": 4096 } } ] } ``` ### Response Fields | Field | Type | Description | |---------------------|---------|--------------------------------------------------------------------| | `id` | string | Stable region slug - pass this as `region` on create | | `label` | string | Human-readable label (safe to render in UIs) | | `active_nodes` | integer | Hosts registered in the region right now | | `available` | boolean | `true` when the region has capacity for at least one `small` sandbox | | `small_slots_free` | integer | Free slots at the `small` shape | | `headroom` | object | Free slots per size, plus total and used memory for the region | ```bash curl https://api.miosa.ai/api/v1/regions \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` The list endpoint is unauthenticated-friendly - you can use a short-lived ticket or a public token to populate region pickers without exposing your `msk_*` key. --- ## FAQ ### Why these 3 regions? We picked Los Angeles, New York, and Miami to cover the US west coast, east coast, and central corridor. The three sites are roughly equidistant for the median US user, and they each sit on independent network and power footprints - so a regional outage at one doesn't take the others down. ### How do I pick a region? Pick by **data residency** first. If you have a customer or compliance requirement that pins data to a coast, that decides for you. If you have no residency requirement, pick by **proximity to your users or your CI runners** - shorter physical distance means lower RTT for VNC streaming and PTY round-trips. Compute performance itself is identical across regions. ### Can a Computer span multiple regions? No. A Computer (or Sandbox) lives in exactly one region for its lifetime. To run workloads in multiple regions: - Create one Computer per region. - Use OpenComputers federation if you want a single logical workspace across them (see [OpenComputers](/docs/api-reference/open-computers/)). There's no live migration between regions today. ### Will more regions ship? Yes - EU and APAC regions are on the roadmap. When they ship they'll appear in `GET /api/v1/regions` alongside the current three. Watch the [changelog](/docs/changelog/) for announcements. --- ## See also - [Computers API](/docs/api-reference/computers/) - pass `region` on create - [Sandboxes API](/docs/api-reference/sandboxes/) - sandbox region support - [OpenComputers](/docs/api-reference/open-computers/) - federate BYOC hosts across regions --- # Releases API URL: https://miosa.ai/docs/api-reference/releases Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/releases Source: src/routes/docs/api-reference/releases/+page.md Description: Inspect and activate exact immutable deployment artifacts. A **Release** is the immutable artifact produced from a build or sandbox publish. It records the artifact digest, runtime command, port, health check, source attribution, and readiness state used for an exact promotion. Base path: `/api/v1/deployments/{deployment_id}/releases` ## Endpoints | Method | Path | Purpose | |---|---|---| | `GET` | `/deployments/{deployment_id}/releases` | List releases for one deployment | | `GET` | `/deployments/{deployment_id}/releases/{release_id}` | Inspect one exact release | | `POST` | `/deployments/{deployment_id}/releases/{release_id}/promote` | Activate one exact release | Publish creates the candidate release. Promotion changes the active production artifact only after the requested release is ready and the configured runtime accepts it. ## List releases ```bash curl https://api.miosa.ai/api/v1/deployments/$DEPLOYMENT_ID/releases \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` The response contains `data`, a compatibility `releases` array, and `total`. ```json { "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "deployment_id": "660e8400-e29b-41d4-a716-446655440000", "deployment_version_id": "770e8400-e29b-41d4-a716-446655440000", "kind": "dynamic", "state": "ready", "artifact_sha256": "a5e6f0c1...", "start_command": "node server.js", "port": 3000, "health_check_path": "/api/health", "ready_at": "2026-07-24T18:19:48Z", "created_at": "2026-07-24T18:18:06Z" } ], "total": 1 } ``` ## Inspect one release ```bash curl \ https://api.miosa.ai/api/v1/deployments/$DEPLOYMENT_ID/releases/$RELEASE_ID \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` Use this endpoint to confirm the exact artifact digest, runtime contract, source sandbox, build, and readiness state before promotion. ## Promote one exact release Promotion is a mutation and requires an idempotency key. ```bash curl -X POST \ https://api.miosa.ai/api/v1/deployments/$DEPLOYMENT_ID/releases/$RELEASE_ID/promote \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" ``` The response contains the deployment plus the durable operation record. Repeating the same request with the same idempotency key returns the existing result instead of activating the release twice. Promotion uses the deployment's configured runtime product. For Docker Host, the release is activated through the workspace Docker Host host. For standard MIOSA Deploy, it is activated through the standard dynamic or static runtime. ## CLI ```bash miosa deploy releases --json miosa deploy versions --json miosa deploy promote --json miosa deploy rollback --to --yes --json ``` ## Immutability A release is content-addressed by `artifact_sha256`. Changing application content creates a new release. Promotion never mutates the bytes of an existing release. Rollback selects a previously ready release and activates that exact artifact through the same runtime-aware path. ## Permissions | Action | Required scope | |---|---| | List or inspect releases | `deployments:read` | | Promote or roll back | `deployments:write` | ## See also Create candidate releases from sandbox source. Inspect logical deployment versions and their build state. Restore a previously healthy release. --- # Runtime Env URL: https://miosa.ai/docs/api-reference/runtime-env Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/runtime-env Source: src/routes/docs/api-reference/runtime-env/+page.md Description: Store inherited runtime environment variables for sandboxes, computers, agents, and deployments. # Runtime Env Runtime Env stores inherited environment variables for tenant, workspace, and project scopes. Values are encrypted at rest and are never returned in plaintext. MIOSA materializes them into sandboxes, computers, agents, or deployments according to the target you choose. Base path: `/api/v1/runtime-env` Runtime Env can hold provider keys such as Anthropic, OpenAI, Higgsfield, Resend, Stripe, or customer API tokens. Scope them as narrowly as possible and avoid putting tenant admin credentials into broad defaults. ## Endpoints | Method | Path | Description | |---|---|---| | `GET` | `/api/v1/runtime-env` | List inherited env variables | | `POST` | `/api/v1/runtime-env` | Create or update one variable | | `GET` | `/api/v1/runtime-env/{id}` | Show one variable without plaintext | | `DELETE` | `/api/v1/runtime-env/{id}` | Delete one variable | List supports these filters: | Query | Values | |---|---| | `scope` | `tenant`, `workspace`, `project` | | `workspace_id` | Workspace id for workspace-scoped variables | | `project_id` | Project id for project-scoped variables | | `target` | `all`, `sandbox`, `computer`, `agent`, `deployment` | ## Create ```http POST /api/v1/runtime-env Authorization: Bearer msk_u_... Content-Type: application/json ``` ```json { "scope": "workspace", "workspace_id": "ws_123", "target": "sandbox", "name": "ANTHROPIC_API_KEY", "value": "sk-ant-...", "metadata": { "provider": "anthropic", "owner": "lumen" } } ``` Environment variable names must be uppercase shell-safe names matching `/^[A-Z][A-Z0-9_]*$/`. ## Scope model | Scope | Required id | Use it for | |---|---|---| | `tenant` | none | Organization-wide defaults. | | `workspace` | `workspace_id` | A customer/workspace provider key or tool config. | | `project` | `project_id` | A single app or deployment pipeline. | | Target | Receives the variable | |---|---| | `all` | All supported runtime targets. | | `sandbox` | Sandbox sessions and sandbox agent runs. | | `computer` | Desktop computer sessions and computer agent runs. | | `agent` | Agent runtime sessions. | | `deployment` | Production deployment/runtime boot. | Use narrower scopes for spend-capable keys. For example, set `ANTHROPIC_API_KEY` at workspace scope for Lumen instead of tenant scope if that key should only power Lumen sandboxes and computers. ## CLI Runtime Env has no dedicated `miosa` subcommand; drive it with `miosa api` against the `/runtime-env` endpoint. ```bash miosa api POST /runtime-env -d '{ "name": "ANTHROPIC_API_KEY", "value": "sk-ant-...", "scope": "workspace", "workspace_id": "ws_123", "target": "sandbox" }' miosa api POST /runtime-env -d '{ "name": "HIGGSFIELD_API_KEY", "value": "hgf_...", "scope": "project", "project_id": "proj_123", "target": "agent" }' miosa api GET /runtime-env?scope=workspace&workspace_id=ws_123 miosa api GET /runtime-env/env_123 miosa api DELETE /runtime-env/env_123 ``` ## Relationship to profiles and connectors Runtime Env, Agent Runtime Profiles, and managed connectors are separate: | Primitive | Owns | |---|---| | Runtime Env | Inherited environment variables and encrypted provider keys. | | Agent Runtime Profile | Runtime choice, tools, connectors, default env, and policy. | | Managed connector | Brokered provider access and scoped token exchange. | Use Runtime Env for direct provider environment variables. Use connectors when MIOSA should broker provider access or restrict egress. Use profiles to attach the right defaults to new sandboxes, computers, and agent runs. ## Next Standardize runtimes, tools, connectors, and policy for each workspace or project. Dispatch prompts with inherited env and runtime profile resolution. Put inherited env, profiles, groups, events, and artifacts together. --- # Sandbox Env API URL: https://miosa.ai/docs/api-reference/sandbox-env Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-env Source: src/routes/docs/api-reference/sandbox-env/+page.md Description: CRUD for per-sandbox environment variables, encrypted at rest. The sandbox env API manages a key-value store of environment variables attached to a sandbox. Variables are encrypted at rest and injected into the sandbox process environment on start. The API never returns plaintext values - only key names and metadata. Base path: `/api/v1/sandboxes/{id}/env` Variable names must match `[A-Z][A-Z0-9_]*` - uppercase letters, digits, and underscores, starting with a letter. This mirrors POSIX shell environment variable conventions. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/sandboxes/{id}/env` | List all env vars (names only, no values) | | `PUT` | `/api/v1/sandboxes/{id}/env` | Bulk set or update env vars | | `DELETE` | `/api/v1/sandboxes/{id}/env/{key}` | Delete a single env var | --- ## List Env Vars **`GET /api/v1/sandboxes/{id}/env`** Returns all variable names. Values are never returned. ### Auth Requires `sandboxes:read` scope. ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` ```json { "data": [ { "key": "DATABASE_URL", "encrypted": true }, { "key": "API_SECRET", "encrypted": true }, { "key": "LOG_LEVEL", "encrypted": false } ] } ``` --- ## Set Env Vars **`PUT /api/v1/sandboxes/{id}/env`** Bulk upsert. Existing keys are updated; new keys are created. Keys not present in the request are left unchanged. ### Auth Requires `sandboxes:write` scope. ``` Authorization: Bearer msk_... ``` ### Request Body ```json { "vars": [ { "key": "DATABASE_URL", "value": "postgres://...", "encrypted": true }, { "key": "LOG_LEVEL", "value": "debug" } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `vars[].key` | string | Yes | Variable name (`[A-Z][A-Z0-9_]*`) | | `vars[].value` | string | Yes | Variable value | | `vars[].encrypted` | boolean | No | Store encrypted at rest (default: `true`) | ### Response - `200 OK` ```json { "data": [ { "key": "DATABASE_URL", "encrypted": true }, { "key": "LOG_LEVEL", "encrypted": false } ] } ``` --- ## Delete Env Var **`DELETE /api/v1/sandboxes/{id}/env/{key}`** Removes a single variable. ### Response - `200 OK` ```json { "deleted": true, "name": "LOG_LEVEL" } ``` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_ID` | Sandbox ID is not a valid UUID | | 400 | `invalid vars` | `vars` missing or a key fails name validation | | 404 | `NOT_FOUND` | Sandbox does not exist or wrong tenant | | 404 | `NOT_FOUND` | Key does not exist (on DELETE) | --- ## Examples ```bash # Set curl -X PUT https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/env \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{ "vars": [ {"key": "DATABASE_URL", "value": "postgres://user:pass@host/db"}, {"key": "LOG_LEVEL", "value": "info"} ] }' # List curl https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/env \ -H "Authorization: Bearer msk_u_..." # Delete curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/env/LOG_LEVEL \ -H "Authorization: Bearer msk_u_..." ``` ```python client = miosa.Miosa() # Set client.sandboxes.env.set("sbx_abc123", { "DATABASE_URL": "postgres://user:pass@host/db", "LOG_LEVEL": "info", }) # List (names only) keys = client.sandboxes.env.list("sbx_abc123") # Delete one client.sandboxes.env.delete("sbx_abc123", "LOG_LEVEL") ``` ```typescript const client = new Miosa(); // Set await client.sandboxes.env.set('sbx_abc123', { DATABASE_URL: 'postgres://user:pass@host/db', LOG_LEVEL: 'info', }); // List const { data } = await client.sandboxes.env.list('sbx_abc123'); // Delete await client.sandboxes.env.delete('sbx_abc123', 'LOG_LEVEL'); ``` --- # Sandbox Files (Advanced) API URL: https://miosa.ai/docs/api-reference/sandbox-files-advanced Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-files-advanced Source: src/routes/docs/api-reference/sandbox-files-advanced/+page.md Description: Upload a file, write bytes as well as text, traverse the file tree, batch write, and watch file-change events in a sandbox. These advanced file operations extend the core [Files API](/docs/api-reference/files) with IDE-oriented capabilities: recursive tree listing, atomic batch writes, and a live file-change event stream. Base path: `/api/v1/sandboxes/{id}/files`, and `/api/v1/sandboxes/{id}/fs` for the upload route. These routes use `:sandbox_id` as the path parameter internally but are accessed as `/sandboxes/{id}/files/...` - the `{id}` segment is the sandbox UUID. File read, write, list, mkdir, and remove work on a SOMA sandbox through these routes. They no longer answer `501 FILES_UNSUPPORTED_ON_RUNTIME`. Files on SOMA are kept only while the sandbox runs, because SOMA does not persist state across a stop yet. See [Choosing a runtime](/docs/soma/choosing-a-runtime/). --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/sandboxes/{id}/files/tree` | Recursive file tree | | `POST` | `/api/v1/sandboxes/{id}/files/write-many` | Batch write multiple files | | `GET` | `/api/v1/sandboxes/{id}/files/watch` | SSE stream of file-change events | | `POST` | `/api/v1/sandboxes/{id}/fs/upload` | Upload a file (multipart) | --- ## Upload a file **`POST /api/v1/sandboxes/{id}/fs/upload`** Send the bytes as multipart form data. This is the route the SDKs use, and the only upload route today. | Field | Type | Required | Description | |---|---|---|---| | `file` | file | Yes | The bytes to write. A missing part answers `400 MISSING_FILE`. | | `path` | string | No | Destination. Defaults to `/home/user/`. A trailing slash appends the uploaded filename; without one, `path` is the exact destination. | ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/$SBX/fs/upload \ -H "Authorization: Bearer msk_u_..." \ -F "path=/workspace/assets/" \ -F "file=@./logo.png" ``` ### Errors | Status | Code | Cause | |---|---|---| | 400 | `MISSING_FILE` | No `file` part. | | 400 | `INVALID_PATH` | `path` contains a null byte. | | 413 | `FILE_TOO_LARGE` | The upload exceeds the size limit. | `/api/v1/sandboxes/{id}/files/upload` is an alias of this route. --- ## Write bytes, not text `/files/write` stores `content` verbatim, so it is for text. For anything binary, send `content_base64` instead; `/files/write-many` takes it per file. ```bash # macOS uses `base64 -i`, GNU coreutils uses `base64 -w0` B64=$(base64 -i ./logo.png | tr -d '\n') curl -X POST https://api.miosa.ai/api/v1/sandboxes/$SBX/files/write \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ --data "$(jq -n --arg p /workspace/logo.png --arg b "$B64" '{path: $p, content_base64: $b}')" ``` --- ## File Tree **`GET /api/v1/sandboxes/{id}/files/tree`** Returns a recursive directory listing up to a configurable depth. Useful for populating an IDE file-explorer panel. ### Auth ``` Authorization: Bearer msk_... ``` ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `path` | string | `/workspace` | Root directory to list | | `depth` | integer | `3` | Max traversal depth (1-10) | | `include_hidden` | boolean | `false` | Include dot-files and dot-directories | ### Response - `200 OK` ```json { "data": { "path": "/workspace", "type": "directory", "children": [ { "name": "src", "path": "/workspace/src", "type": "directory", "children": [ { "name": "index.ts", "path": "/workspace/src/index.ts", "type": "file", "size": 1842, "modified_at": "2026-05-26T09:12:01Z" } ] }, { "name": "package.json", "path": "/workspace/package.json", "type": "file", "size": 512, "modified_at": "2026-05-25T18:00:00Z" } ] } } ``` --- ## Batch Write **`POST /api/v1/sandboxes/{id}/files/write-many`** Writes multiple files in a single request. All writes are applied atomically from the guest's perspective - either all succeed or an error is returned for the failing file. Maximum 50 files per request. Files larger than 90 KB should be uploaded individually via `POST /files/write`. ### Request Body ```json { "files": [ { "path": "/workspace/src/index.ts", "content": "export const hello = () => 'world';\n", "encoding": "utf8" }, { "path": "/workspace/src/utils.ts", "content": "ZXhwb3J0IGZ1bmN0aW9uIG5vb3AoKSB7fQo=", "encoding": "base64" } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `files[].path` | string | Yes | Absolute path inside the sandbox | | `files[].content` | string | Yes | File content (UTF-8 text or base64-encoded binary) | | `files[].encoding` | string | No | `"utf8"` (default) or `"base64"` | ### Response - `200 OK` ```json { "data": { "written": 2, "paths": [ "/workspace/src/index.ts", "/workspace/src/utils.ts" ] } } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_BODY` | `files` is missing or not an array | | 400 | `TOO_MANY_FILES` | More than 50 files in one request | | 400 | `INVALID_PATH` | A path contains a null byte or traverses outside the sandbox | --- ## File Watch (SSE) **`GET /api/v1/sandboxes/{id}/files/watch`** Streams file-system change events as SSE. Useful for syncing editor state in real time. ### Auth ``` Authorization: Bearer msk_... ``` (Browser `EventSource` cannot set this header - use a server-side client or a scoped session token.) ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `path` | string | `/workspace` | Directory to watch | ### Response - `200 OK` (`text/event-stream`) ``` id: wev_001 event: file.changed data: {"path":"/workspace/src/index.ts","event":"modified","ts":"2026-05-26T10:14:23Z"} id: wev_002 event: file.changed data: {"path":"/workspace/src/new-file.ts","event":"created","ts":"2026-05-26T10:14:24Z"} : heartbeat ``` **Event values for `data.event`:** | Value | Trigger | |-------|---------| | `created` | New file or directory | | `modified` | File content changed | | `deleted` | File or directory removed | | `renamed` | File moved or renamed | --- ## Examples ```bash # File tree (depth 2) curl "https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/files/tree?depth=2" \ -H "Authorization: Bearer msk_u_..." # Batch write curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/files/write-many \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{ "files": [ {"path": "/workspace/hello.ts", "content": "console.log(\"hello\");\n"} ] }' # File watch (SSE) curl -N -H "Authorization: Bearer msk_u_..." \ -H "Accept: text/event-stream" \ "https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/files/watch?path=/workspace/src" ``` ```python client = miosa.Miosa() # Tree tree = client.sandboxes.files.tree("sbx_abc123", path="/workspace", depth=3) # Batch write client.sandboxes.files.write_many("sbx_abc123", [ {"path": "/workspace/index.ts", "content": "export {};"}, {"path": "/workspace/utils.ts", "content": "export function noop() {}"}, ]) ``` ```typescript const client = new Miosa(); // File tree const tree = await client.sandboxes.files.tree('sbx_abc123', { path: '/workspace', depth: 3, includeHidden: false, }); // Batch write await client.sandboxes.files.writeMany('sbx_abc123', [ { path: '/workspace/index.ts', content: 'export {};' }, { path: '/workspace/utils.ts', content: 'export function noop() {}' }, ]); ``` --- # Sandbox Processes API URL: https://miosa.ai/docs/api-reference/sandbox-processes Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-processes Source: src/routes/docs/api-reference/sandbox-processes/+page.md Description: Start, monitor, and stream logs for long-running background processes inside sandboxes. The processes API lets you launch long-running background commands inside a sandbox and inspect their output - useful for starting dev servers, watchers, or any process that runs beyond a single exec call. Base path: `/api/v1/sandboxes/{id}/processes` Processes are managed by the sandbox runtime and survive for the lifetime of the sandbox. They are not preserved across snapshots or forks. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/api/v1/sandboxes/{id}/processes` | Start a process | | `GET` | `/api/v1/sandboxes/{id}/processes` | List processes | | `GET` | `/api/v1/sandboxes/{id}/processes/{pid}` | Get process status | | `DELETE` | `/api/v1/sandboxes/{id}/processes/{pid}` | Stop a process | | `GET` | `/api/v1/sandboxes/{id}/processes/{pid}/logs` | Fetch recent log lines | | `GET` | `/api/v1/sandboxes/{id}/processes/{pid}/stream` | SSE live log stream | --- ## Start a Process **`POST /api/v1/sandboxes/{id}/processes`** ### Auth ``` Authorization: Bearer msk_... ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `cmd` | string | Yes | Command to run (passed to `/bin/sh -c`) | | `cwd` | string | No | Working directory (default: `/workspace`) | | `env` | object | No | Additional environment variables | | `name` | string | No | Human-readable label | ```json { "cmd": "npm run dev -- --port 3000", "cwd": "/workspace", "name": "dev-server" } ``` ### Response - `201 Created` ```json { "data": { "pid": "proc_01hwxyz...", "name": "dev-server", "cmd": "npm run dev -- --port 3000", "cwd": "/workspace", "status": "running", "started_at": "2026-05-26T10:14:23Z", "exited_at": null, "exit_code": null } } ``` --- ## List Processes **`GET /api/v1/sandboxes/{id}/processes`** ### Response - `200 OK` ```json { "data": [ { "pid": "proc_01hwxyz...", "name": "dev-server", "status": "running", "started_at": "2026-05-26T10:14:23Z", "exited_at": null, "exit_code": null } ] } ``` --- ## Get Process Status **`GET /api/v1/sandboxes/{id}/processes/{pid}`** ### Response - `200 OK` ```json { "data": { "pid": "proc_01hwxyz...", "name": "dev-server", "cmd": "npm run dev -- --port 3000", "cwd": "/workspace", "status": "exited", "started_at": "2026-05-26T10:14:23Z", "exited_at": "2026-05-26T10:22:01Z", "exit_code": 0 } } ``` **Process status values:** | Status | Description | |--------|-------------| | `running` | Process is alive | | `exited` | Process exited normally | | `killed` | Process was stopped via DELETE | | `error` | Process failed to start | --- ## Stop a Process **`DELETE /api/v1/sandboxes/{id}/processes/{pid}`** Sends `SIGTERM` followed by `SIGKILL` after a grace period. ### Response - `204 No Content` --- ## Fetch Logs **`GET /api/v1/sandboxes/{id}/processes/{pid}/logs`** Returns the last N lines of stdout+stderr. ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `tail` | integer | `200` | Number of lines to return | ### Response - `200 OK` ```json { "data": { "lines": [ " > Local: http://localhost:3000", " > Network: http://0.0.0.0:3000", " ready in 743ms" ] } } ``` --- ## Live Log Stream (SSE) **`GET /api/v1/sandboxes/{id}/processes/{pid}/stream`** Streams stdout+stderr as SSE. Uses `text/event-stream` - requires Bearer token auth. ``` id: log_001 event: log data: {"line":" ready in 743ms","ts":"2026-05-26T10:14:24Z"} : heartbeat ``` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_ID` | Sandbox ID is not a valid UUID | | 404 | `NOT_FOUND` | Sandbox or process does not exist | | 409 | `SANDBOX_NOT_RUNNING` | Sandbox must be in `running` state to start processes | | 500 | `PROCESS_START_FAILED` | Runtime failed to spawn the process | --- ## Examples ```bash # Start curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/processes \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{"cmd": "npm run dev -- --port 3000", "name": "dev-server"}' # List curl https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/processes \ -H "Authorization: Bearer msk_u_..." # Logs curl "https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/processes/proc_01hwxyz/logs?tail=50" \ -H "Authorization: Bearer msk_u_..." # Stop curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/processes/proc_01hwxyz \ -H "Authorization: Bearer msk_u_..." ``` ```python client = miosa.Miosa() # Start a dev server proc = client.sandboxes.processes.start( "sbx_abc123", cmd="npm run dev -- --port 3000", name="dev-server", ) # Wait a moment then check logs time.sleep(3) logs = client.sandboxes.processes.logs("sbx_abc123", proc.pid, tail=50) for line in logs: print(line) # Stop client.sandboxes.processes.stop("sbx_abc123", proc.pid) ``` ```typescript const client = new Miosa(); // Start const proc = await client.sandboxes.processes.start('sbx_abc123', { cmd: 'npm run dev -- --port 3000', name: 'dev-server', }); // Poll status let status = proc.status; while (status === 'running') { await new Promise(r => setTimeout(r, 2000)); const updated = await client.sandboxes.processes.get('sbx_abc123', proc.pid); status = updated.status; } // Logs const { data } = await client.sandboxes.processes.logs('sbx_abc123', proc.pid); console.log(data.lines.join('\n')); ``` --- # Sandbox Shares API URL: https://miosa.ai/docs/api-reference/sandbox-shares Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-shares Source: src/routes/docs/api-reference/sandbox-shares/+page.md Description: Create and manage public share URLs for sandboxes. Sandbox shares are publicly accessible URLs that give read-only access to a running sandbox. Unlike preview tokens, share URLs are not scoped to an iframe and do not require the recipient to hold any credentials. Share tokens use the prefix `ms_` followed by a base64url-encoded payload. The public URL pattern is: ``` https://{sandbox-id}.sandboxes.miosa.ai?ms=ms_ ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/api/v1/sandboxes/{id}/shares` | Create a share URL | | `GET` | `/api/v1/sandboxes/{id}/shares` | List active shares | | `DELETE` | `/api/v1/sandboxes/{id}/shares/{share_id}` | Revoke a share | --- ## Create a Share **`POST /api/v1/sandboxes/{id}/shares`** Creates a public share link. The `share_url` field is safe to distribute - it contains the token as a query parameter. ### Auth ``` Authorization: Bearer msk_... ``` ### Request Body | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `expires_in` | integer | No | `3600` | TTL in seconds | | `scope` | string | No | `"read"` | Must be `"read"` (only supported value) | ```json { "expires_in": 86400 } ``` ### Response - `201 Created` ```json { "share_id": "shr_01hx9m7n3k...", "share_url": "https://sbx-abc123.sandboxes.miosa.ai?ms=ms_eyJ...", "expires_at": "2026-05-27T12:00:00Z", "scope": "read" } ``` | Field | Type | Description | |-------|------|-------------| | `share_id` | string | Opaque share identifier used to revoke | | `share_url` | string | Publicly accessible URL with token embedded | | `expires_at` | string | ISO 8601 expiry, or `null` for no expiry | | `scope` | string | Always `"read"` | --- ## List Shares **`GET /api/v1/sandboxes/{id}/shares`** Returns all active (non-revoked, non-expired) shares for the sandbox. ### Response - `200 OK` ```json { "data": [ { "share_id": "shr_01hx9m7n3k...", "share_url": "https://sbx-abc123.sandboxes.miosa.ai?ms=ms_eyJ...", "expires_at": "2026-05-27T12:00:00Z", "scope": "read" } ] } ``` --- ## Revoke a Share **`DELETE /api/v1/sandboxes/{id}/shares/{share_id}`** Immediately invalidates the share. Any visitor using the share URL will receive a 403 after revocation. ### Response - `204 No Content` No response body. --- ## Errors | Status | Code | Cause | |--------|------|-------| | 404 | `sandbox_not_found` | Sandbox does not exist or wrong tenant | | 403 | `forbidden` | Tenant mismatch | | 404 | `not_found` | Share ID does not exist (on DELETE) | | 422 | `invalid_scope` | `scope` value is not `"read"` | --- ## Examples ```bash # Create curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/shares \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{"expires_in": 86400}' # List curl https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/shares \ -H "Authorization: Bearer msk_u_..." # Revoke curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/shares/shr_01hx9m7n3k \ -H "Authorization: Bearer msk_u_..." ``` ```python client = miosa.Miosa() # Create share = client.sandboxes.shares.create("sbx_abc123", expires_in=86400) print(share.share_url) # List shares = client.sandboxes.shares.list("sbx_abc123") # Revoke client.sandboxes.shares.delete("sbx_abc123", share.share_id) ``` ```typescript const client = new Miosa(); // Create const share = await client.sandboxes.shares.create('sbx_abc123', { expiresIn: 86400, }); console.log(share.shareUrl); // List const shares = await client.sandboxes.shares.list('sbx_abc123'); // Revoke await client.sandboxes.shares.delete('sbx_abc123', share.shareId); ``` --- # Sandbox Templates API URL: https://miosa.ai/docs/api-reference/sandbox-templates Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-templates Source: src/routes/docs/api-reference/sandbox-templates/+page.md Description: The templates that stage content inside the one sandbox image, the legacy image ids that are aliases, and how to create a sandbox from a template. Sandbox templates are pre-built images with toolchains, dependencies, and configuration baked in. Launching from a template is faster than installing packages at runtime and ensures reproducible environments. Every sandbox boots the same main image. A template is content staged inside that image at create time, never a separate image. See the [Templates guide](/docs/develop/templates/). Templates are read-only marketplace items managed by MIOSA. To build your own custom templates, see the [Templates guide](/docs/develop/templates). --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/sandbox-templates` | List the templates available to you | | `GET` | `/api/v1/sandbox-templates/{id}` | Get a specific template | | `POST` | `/api/v1/sandboxes` | Create a sandbox from a template (via `template_id`) | --- ## List templates **`GET /api/v1/sandbox-templates`** Returns the built-in catalog plus any custom templates your workspace has built, as a JSON array. Each row carries the template `slug`, its `source` (`builtin` or `workspace`), and the registry `status`. Alias ids are left out by default. Pass `include_aliases=true` to include them; each alias is nested on the row it resolves to, under `aliases`. ```bash curl "https://api.miosa.ai/api/v1/sandbox-templates?include_aliases=true" \ -H "Authorization: Bearer msk_u_..." ``` --- ## Listed templates Five templates are listed, and all five run the one image: | `template_id` | Kind | What it is | |---|---|---| | `miosa-sandbox` | `image` | The default sandbox. Everything inside, and what you get when you omit `template_id`. | | `nextjs` | `starter` | Stages a Next.js app in the sandbox and starts it on port 3000. | | `nextjs-postgres` | `starter` | A Next.js app with Postgres client tooling on port 3000; bind `DATABASE_URL`. | | `static-html` | `starter` | Static HTML, CSS, and JavaScript served from `/workspace` on port 8000. | | `fastapi-auth` | `starter` | A FastAPI starter with signup, login, me, and dashboard routes, on port 8000. Its health check does not work yet. | The four starter templates add only content inside the image: the scaffold, the install command, and the start command. Templates your workspace builds are returned with kind `custom`. `miosa-desktop` is a separate Computer product, not a sandbox template; see [Computers](/docs/computers/overview/). --- ## Aliases Alias ids are never listed, and still work as `template_id`. These resolve to `miosa-sandbox`, the plain image: `miosa-sandbox-prod-1`, `miosa-sandbox-docker`, `agent-browser`, `instant-code`, `bookworm`, `debian-12`, `docker`, `sandbox-docker`, `browser-agent`, `code`, `instant`, `instant-node`, `node-20`, `postgres-15`, `postgres-16`, `ubuntu-22.04`, `ubuntu-24.04`. These resolve to a starter, which stays listed: `static` to `static-html`; `next-postgres` and `fullstack-next-postgres` to `nextjs-postgres`; `python-auth` and `fastapi-auth-starter` to `fastapi-auth`. --- ## Create a sandbox from a template **`POST /api/v1/sandboxes`** ```json { "template_id": "nextjs", "size": "small" } ``` Omit `template_id` for a sandbox with nothing staged, or pass a content template to stage a scaffold inside the same image. For a desktop, create a Computer with `template_type: "miosa-desktop"`; see [Computers](/docs/computers/overview/). See the [Sandboxes API](/docs/api-reference/sandboxes) for the full creation reference. --- ## Errors | Status | Code | Cause | |--------|------|-------| | 404 | `not_found` | Template ID does not exist | --- ## Examples ```bash # List templates, including the legacy alias ids curl "https://api.miosa.ai/api/v1/sandbox-templates?include_aliases=true" \ -H "Authorization: Bearer msk_u_..." # Get one template curl https://api.miosa.ai/api/v1/sandbox-templates/nextjs \ -H "Authorization: Bearer msk_u_..." # Create a sandbox with the Next.js template staged curl -X POST https://api.miosa.ai/api/v1/sandboxes \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{"template_id": "nextjs", "size": "small"}' ``` ```python client = miosa.Miosa() # Browse templates templates = client.sandbox_templates.list() for t in templates: print(t.slug, "-", t.description) # Create the default sandbox sandbox = client.sandboxes.create(template_id="nextjs", size="small") ``` ```typescript const client = new Miosa(); // Browse const templates = await client.sandboxTemplates.list(); for (const t of templates) { console.log(t.slug, '-', t.description); } // Create a sandbox with the Next.js template staged const sandbox = await client.sandboxes.create({ templateId: 'nextjs', size: 'small', }); ``` --- # Sandboxes API URL: https://miosa.ai/docs/api-reference/sandboxes Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandboxes Source: src/routes/docs/api-reference/sandboxes/+page.md Description: API reference for creating, managing, and executing code in MIOSA sandboxes - persistent-by-default isolated microVM workspaces. Sandboxes are persistent-by-default isolated microVM workspaces that restore from pre-seeded snapshots, accept exec and file operations, expose previews, and are billed by compute/runtime usage. They are designed for AI-agent code execution workloads, artifact generation, and app previews. Base path: `/api/v1/sandboxes` All endpoints require `Authorization: Bearer <api_key>`. Get a key from the [MIOSA dashboard](https://miosa.ai/settings/api-keys). For higher-level access, use the official SDKs: [Python](/docs/sdks/python/), [TypeScript](/docs/sdks/typescript/), [Go](/docs/sdks/go/), [Java](/docs/sdks/java/), [Elixir](/docs/sdks/elixir/). Create a sandbox, the fused run endpoint, template apps, artifacts, and custom templates. List, get, destroy, pause, resume, fork, extend the timeout, and read usage. --- ## List sandbox templates and sizes **`GET /api/v1/templates`** This is the canonical catalog for sandbox template IDs, template defaults, per-template size support, readiness, and exact versioned resource contracts. Clients need no secondary catalog endpoint to discover or admit a sandbox configuration. ```bash curl https://api.miosa.ai/api/v1/templates ``` ### Response - `200 OK` ```json { "data": [ { "id": "miosa-sandbox", "name": "MIOSA Sandbox", "product": "sandbox", "default_size": "small", "primitive": "sandbox", "sdk_name": "miosa-sandbox", "cli_name": "miosa-sandbox", "readiness": "fast_ready", "sizes": [ { "size": "small", "state": "fast_ready", "resource_contract": { "contract_id": "sandbox/small@v1", "contract_version": "v1", "product": "sandbox", "size": "small", "vcpus": 2, "memory_mb": 4096, "disk_size_mb": 10240 } } ] } ], "templates": [ { "id": "miosa-sandbox", "name": "MIOSA Sandbox", "product": "sandbox", "default_size": "small", "primitive": "sandbox", "sdk_name": "miosa-sandbox", "cli_name": "miosa-sandbox", "readiness": "fast_ready", "sizes": [ { "size": "small", "state": "fast_ready" } ] } ], "shape_contracts": { "sandbox": [], "computer": [] }, "readiness_states": ["fast_ready", "cold_boot_only", "missing"] } ``` `fast_ready` sizes are creatable through the fast start path. `cold_boot_only` sizes are also creatable but start more slowly. `missing` means unavailable and must not be submitted. Use that template's `default_size` when the caller does not have a stronger requirement. Do not combine a size advertised by one template with another template. --- ## Configuration and request parameters `GET /api/v1/templates` returns platform configuration: available templates, the default size, readiness, and versioned resource contracts. Applications select that configuration with sandbox parameters; they do not send internal host, provider, network, or placement fields. Prefer `size` for create requests. The default `small` contract is 2 vCPU, 4096 MiB RAM, and 10240 MiB disk. `xs` is selectable when the catalog reports it as available. Compatibility resource fields must include `cpu_count`, `memory_mb`, and `disk_size_mb` together and exactly match one published contract. Responses include the resolved contract under `resource_contract`. Contract IDs are versioned, for example `sandbox/small@v1`; persist that identity when usage or audit records must remain reproducible. --- ## Sandbox Object The full sandbox object returned by create, get, and list endpoints: | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique sandbox identifier. | | `tenant_id` | string | Owning tenant ID. | | `owner_id` | string | Creating user ID. | | `template_id` | string | Boot template name. | | `state` | string | Current lifecycle state. | | `size` | string | Canonical named size: `xs`, `small`, `medium`, `large`, or `xl`. | | `resource_contract` | object | Versioned resolved contract with ID, product, size, vCPU, memory, and disk. | | `cpu_count` | integer | Allocated vCPUs. | | `memory_mb` | integer | Allocated RAM in MB. | | `disk_size_mb` | integer | Allocated root disk in MB. | | `ready` | boolean | Whether command and readiness checks are accepting work. | | `ready_at` | string/null | ISO timestamp when `ready` became true. | | `preview_url` | string/null | Tenant-aware base preview URL for the sandbox slug. Use `/expose` for a specific port. | | `timeout_sec` | integer | Max seconds for the active session. Persistent timeout pauses; non-persistent timeout destroys. | | `timeout_remaining_ms` | integer \| null | Remaining active-session time; null before start or when timeout enforcement is disabled. | | `always_on` | boolean | Whether timeout and idle-timeout enforcement are disabled. | | `persistent` | boolean | Whether the sandbox preserves filesystem state across stop/timeout. | | `idle_timeout_sec` | integer | Seconds without activity before the sandbox pauses. `0` disables it; accounts created on or after October 11, 2026 default to `1800` (30 minutes) when unset. | | `total_runtime_sec` | integer \| null | Billed seconds. Set only after destruction. | | `metadata` | object | Caller-supplied metadata. | | `created_at` | string | ISO-8601 creation timestamp. | | `started_at` | string \| null | ISO-8601 timestamp when VM entered `running`. | | `destroyed_at` | string \| null | ISO-8601 timestamp when VM was destroyed. | --- ## Lifecycle States | State | Description | |-------|-------------| | `provisioning` | VM is booting. Exec and file operations are not yet available. | | `running` | VM is reachable. All operations allowed. | | `paused` | Compute is stopped and filesystem state is preserved. Can be resumed. | | `destroyed` | Terminal. VM is gone, billing is settled. | | `error` | Terminal failure. Destroy and create a new sandbox. | --- ## Error Response Format All error responses use this shape: ```json { "error": { "code": "NOT_FOUND", "message": "sandbox not found", "details": null } } ``` The `x-request-id` response header is set on every request. Include it in support requests. --- ## Code interpreter **`POST /api/v1/sandboxes/{id}/code/run`** (alias: `POST /api/v1/sandboxes/{id}/code`) Runs one cell in a persistent interpreter and returns its output. The kernel starts lazily on first use, and variables, imports and definitions persist across calls in the same context. `language` is `python` (the default) or `javascript`; an unsupported value returns `400 UNKNOWN_LANGUAGE`. `context` names an independent namespace, `default` unless you pass one; `timeout` is capped at 300 seconds. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/code/run \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "total = sum(range(10))\ntotal", "language": "python" }' ``` ```json { "data": { "ok": true, "stdout": "", "stderr": "", "result": "45", "language": "python" } } ``` | Endpoint | What it does | |---|---| | `GET /api/v1/sandboxes/{id}/code` | Report interpreter state. Never starts a kernel. Returns `sandbox_not_running` when the sandbox is not running. | | `GET /api/v1/sandboxes/{id}/code/contexts?language=` | List the contexts that exist for a language. | | `POST /api/v1/sandboxes/{id}/code/contexts` | Create or reuse a context. | | `DELETE /api/v1/sandboxes/{id}/code/contexts/{context}` | Reset one context. | | `DELETE /api/v1/sandboxes/{id}/code?language=` | Reset every context for a language. | Reading (`GET`) needs `sandboxes:read`; running and resetting need `sandboxes:exec`. The sandbox must be running: the interpreter does not wake a paused sandbox. --- ## Rate Limits | Limit | Value | |-------|-------| | Request rate | Enforced per workspace and per endpoint group; an over-limit response carries `Retry-After` | | Concurrent sandboxes per tenant | Plan cap: 10 (Developer), 250 (Business), 500 (Enterprise) | | `timeout_sec` | 1-86400 seconds; default 3600 | | Default resource shape | `small`: 2 vCPU, 4 GiB RAM, 10 GiB disk | | Named resource schema | `xs` through `xl`; actual availability comes from `GET /templates` and tenant policy | | Max exec timeout | 1-86400 seconds; default 3600 | | Max file upload size | 500 MB | Rate-limited responses return HTTP `429` with a `Retry-After` header indicating seconds to wait. --- ## See also - [Python SDK](/docs/sdks/python/) - `miosa` - [TypeScript SDK](/docs/sdks/typescript/) - `@miosa/sdk` - [Go SDK](/docs/sdks/go/) - `github.com/miosa-ai/miosa-go` - [Java SDK](/docs/sdks/java/) - `ai.miosa:miosa-sdk` - [Elixir SDK](/docs/sdks/elixir/) - `:miosa` - [Events (SSE)](/docs/api-reference/events/) - SSE reference for other MIOSA resources - [Error Codes](/docs/api-reference/errors/) - full error code catalog including sandbox-specific codes --- # Sandboxes API - create and run URL: https://miosa.ai/docs/api-reference/sandboxes/create Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandboxes/create Source: src/routes/docs/api-reference/sandboxes/create/+page.md Description: Create a MIOSA sandbox, use the fused run endpoint, start a template app, get artifacts, and the custom-template boundary. # Create and run Base path: `/api/v1/sandboxes` All endpoints require `Authorization: Bearer <api_key>`. ## Create a Sandbox **`POST /api/v1/sandboxes`** Spawns a new sandbox VM. The API returns the sandbox record immediately after the create request is accepted; poll `GET /api/v1/sandboxes/{id}` or subscribe to events until `state` is `running` and `ready` is `true`. The production alias `miosa-sandbox` resolves to a certified immutable artifact generation. ### Auth Bearer token required. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `template_id` | string | No | Template to stage inside the sandbox image at create. Omit it for a sandbox with nothing staged. See templates below. | | `size` | string | No | Canonical resource configuration. Defaults to `small` (`2 vCPU`, `4096 MiB`, `10240 MiB` disk). | | `cpu_count` | integer | No | Exact vCPU compatibility input. Supply the full matching resource triple. | | `memory_mb` | integer | No | Exact RAM override. It must match the selected canonical size. | | `disk_size_mb` | integer | No | Exact disk override. It must match the selected canonical size. `disk_mb` is deprecated. | | `timeout_sec` | integer | No | Active-session timeout. Defaults to `3600`; minimum `1`, maximum `86400`. | | `always_on` | boolean | No | Disable timeout and idle-timeout enforcement until an explicit lifecycle action. Defaults to `false` and remains subject to policy. | | `persistent` | boolean | No | Defaults to `true`. Preserve filesystem state across stop/timeout and allow resume. Set `false` only for disposable one-off jobs. | | `idle_timeout_sec` | integer | No | Seconds without activity before the sandbox pauses. Left unset, it defaults to `0` (disabled), or `1800` (30 minutes) for accounts created on or after October 11, 2026; set `0` to disable. | | `env` | object | No | Key-value env vars injected at boot. | | `metadata` | object | No | Arbitrary caller-supplied metadata stored on the record. | | `agent_runtime_profile_id` | UUID/string | No | Force a specific agent runtime profile for this sandbox. Alias: `agent_profile_id`. | | `skip_agent_runtime_profile` | boolean | No | If `true`, do not apply the tenant/workspace default runtime profile. | | `auto_start` | boolean | No | If `true`, MIOSA starts the selected template after the sandbox reaches `running`. Generated-app platforms usually keep this false, write files first, then call `/template/start`. | | `workspace_id` | UUID | No | Existing MIOSA workspace that owns the sandbox. Defaults to the organization default workspace. | | `workspace_slug` | string | No | Existing or auto-created workspace slug. | | `workspace_name` | string | No | Workspace display name if auto-created. | | `project_id` | UUID | No | Existing MIOSA project that owns the sandbox. Defaults to the workspace default project. | | `project_slug` | string | No | Existing or auto-created project slug inside the workspace. | | `project_name` | string | No | Project display name if auto-created. | | `external_workspace_id` | string | No | Your customer/account/workspace ID. | | `external_user_id` | string | No | Your end-user ID. | | `external_project_id` | string | No | Your project/app/document ID. | If a default agent runtime profile exists for the workspace or tenant, MIOSA applies it automatically during create. Profile env vars are merged with request env vars, request env wins on conflict, profile metadata is recorded under `metadata.agent_runtime_profile`, and profile connectors are attached through egress placeholder tokens rather than copied as plaintext secrets. **Available templates:** | `template_id` | Description | |---------------|-------------| | `miosa-sandbox` | The default sandbox, and the default when you omit `template_id`. Everything inside: Docker Engine 29.9 with `docker compose` and `docker buildx`, a live browser with CDP on a virtual display, the persistent code interpreter, Node, and Python. | Every other built-in id is an alias of `miosa-sandbox`, kept so existing code keeps working; the [Sandbox Templates API](/docs/api-reference/sandbox-templates/) lists them. For a desktop, create a Computer with `template_type: "miosa-desktop"`; see [Computers](/docs/computers/overview/). ### Request Headers | Header | Description | |--------|-------------| | `Idempotency-Key` | Client-generated key (UUID recommended). Same key within 24 h returns the existing sandbox instead of creating a new one. | ### Response - `201 Created` ```json { "id": "sbx_01j9xr2t4fk8me3n5q", "tenant_id": "tnt_abc123", "owner_id": "usr_def456", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "external_workspace_id": "acct_123", "external_user_id": "user-456", "external_project_id": "project_789", "template_id": "miosa-sandbox", "image_id": "miosa-sandbox", "state": "provisioning", "ready": false, "size": "small", "resource_contract": { "id": "sandbox/small@v1", "product": "sandbox", "size": "small", "version": "v1", "vcpus": 2, "memory_mb": 4096, "disk_size_mb": 10240 }, "cpu_count": 2, "memory_mb": 4096, "disk_size_mb": 10240, "preview_url": "https://sbx01j9x.sandbox.miosa.ai", "timeout_sec": 3600, "timeout_remaining_ms": null, "always_on": false, "persistent": true, "idle_timeout_sec": 0, "total_runtime_sec": null, "metadata": {}, "created_at": "2026-04-25T10:00:00Z", "started_at": null, "ready_at": null, "destroyed_at": null } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_TEMPLATE` | `template_id` is not a recognized template. | | 402 | `INSUFFICIENT_CREDITS` | Not enough credits to provision the VM. | | 429 | `SANDBOX_LIMIT_REACHED` | Tenant has reached the concurrent sandbox limit for its plan (Developer 10, Business 250, Enterprise 500). | | 422 | `VALIDATION_ERROR` | Invalid field values (for example, a resource request above the tenant's plan cap). | --- ## Create and Run **`POST /api/v1/sandboxes/run`** Creates a sandbox, waits for it to become `running`, executes the first command, and returns the sandbox record plus the command result in one request. Use this for agent loops where the first useful action is "create a sandbox and run code now." This is the production fast path for agents that need a sandbox and an immediate first command. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/run \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: run-2026-06-09-001" \ -d '{ "template_id": "miosa-sandbox", "command": "python3 -c \"print(1+1)\"", "size": "small", "timeout_sec": 300, "wait_timeout_ms": 30000, "metadata": { "agent_run": "abc123" } }' ``` ### Request Body `/run` accepts the same sandbox creation fields as `POST /api/v1/sandboxes`, plus: | Field | Type | Required | Description | |-------|------|----------|-------------| | `command` | string | Yes | First command to execute after the sandbox reaches `running`. | | `cwd` | string | No | Working directory for the command. Defaults to `/workspace`. | | `timeout` | integer | No | Command timeout in milliseconds. | | `wait_timeout_ms` | integer | No | Max time to wait for sandbox readiness before returning `504 SANDBOX_READY_TIMEOUT`. | ### Response - `201 Created` ```json { "data": { "id": "sbx_01j9xr2t4fk8me3n5q", "state": "running", "ready": true, "template_id": "miosa-sandbox", "cpu_count": 2, "memory_mb": 4096 }, "exec": { "stdout": "2\n", "stderr": "", "exit_code": 0 }, "timings": { "server_wait_and_exec_ms": 512 } } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `MISSING_PARAM` | `command` is missing. | | 400 | `INVALID_TEMPLATE` | `template_id` is not recognized. | | 402 | `INSUFFICIENT_CREDITS` | Not enough credits to provision the VM. | | 429 | `SANDBOX_LIMIT_REACHED` | Tenant has reached the concurrent sandbox limit. | | 409 | `SANDBOX_NOT_RUNNING` | The sandbox failed to reach a running state for exec. | | 502 | `SANDBOX_BOOT_FAILED` | VM boot failed before the command could run. | | 502 | `AGENT_UNAVAILABLE` | The sandbox agent was not reachable for exec. | | 504 | `SANDBOX_READY_TIMEOUT` | The sandbox did not become ready before `wait_timeout_ms`. | --- ## Start a Template App **`POST /api/v1/sandboxes/{id}/template/start`** A sandbox created from a template that has a start command does not wait on the app at create: nothing has started it yet, so a `wait: true` create returns as soon as the sandbox is running instead of timing out. The template's own readiness probe applies when you call `/template/start`; a probe you pass to create always wins. After your platform writes generated files into `/workspace`, call this endpoint to run the selected template lifecycle. MIOSA runs the template install command, launches the start command in the background, stores PID/log paths, and returns the preview URL. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/template/start \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"install":true}' ``` Response: ```json { "data": { "status": "started", "template_id": "nextjs", "workdir": "/workspace", "preview_port": 3000, "preview_url": "https://abc12345.sandbox.miosa.ai", "logs_path": "/tmp/miosa-run/template.log", "pid_path": "/tmp/miosa-run/template.pid", "artifact_paths": ["/workspace"] } } ``` You can override `install_command`, `start_command`, `port`, or `workdir` in the request body when your generated project needs a custom command. --- ## Get Artifacts **`GET /api/v1/sandboxes/{id}/artifacts`** Returns the template artifact contract and current lifecycle metadata. ```bash curl https://api.miosa.ai/api/v1/sandboxes/{id}/artifacts \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` The response includes `preview.url`, `preview.port`, artifact paths, and template lifecycle log/PID paths. A create request with white-label attribution and env: ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "template_id": "miosa-sandbox", "workspace_slug": "acme-corp", "workspace_name": "Acme Corp", "project_slug": "lead-magnet", "project_name": "Lead Magnet", "external_workspace_id": "acct_123", "external_user_id": "user-456", "external_project_id": "project_789", "size": "small", "env": {"MY_VAR": "hello"} }' ``` --- ## Custom templates The canonical public V1 contract exposes only promoted templates returned by `GET /api/v1/templates`. Standalone custom-template build, validation, log, retry, and cancellation endpoints are not part of that contract. Do not make a public integration depend on those operations. --- # Sandboxes API - manage URL: https://miosa.ai/docs/api-reference/sandboxes/manage Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandboxes/manage Source: src/routes/docs/api-reference/sandboxes/manage/+page.md Description: List, get, destroy, pause, resume, restart, fork, extend the timeout, and read usage for MIOSA sandboxes. # Manage sandboxes Base path: `/api/v1/sandboxes` All endpoints require `Authorization: Bearer <api_key>`. ## List Sandboxes **`GET /api/v1/sandboxes`** Returns all sandboxes belonging to the authenticated tenant. ### Query Parameters | Parameter | Type | Description | |---|---|---| | `workspace_id` | UUID | Filter to one MIOSA workspace | | `project_id` | UUID | Filter to one MIOSA project | | `external_workspace_id` | string | Filter by your customer/account ID | | `external_user_id` | string | Filter by your end-user ID | | `external_project_id` | string | Filter by your project/app/document ID | | `state` | string | Filter by lifecycle state: `provisioning`, `running`, `paused`, `destroyed`, `error`. | ### Auth Bearer token required. ### Response - `200 OK` ```json { "data": [ { "id": "sbx_01j9xr2t4fk8me3n5q", "template_id": "miosa-sandbox", "state": "running", "cpu_count": 2, "memory_mb": 4096, "created_at": "2026-04-25T10:00:00Z" } ] } ``` ```bash curl "https://api.miosa.ai/api/v1/sandboxes?state=running" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get a Sandbox **`GET /api/v1/sandboxes/{id}`** ### Auth Bearer token required. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. | ### Response - `200 OK` Full sandbox object (same shape as the create response with current `state`). ### Errors | Status | Code | Cause | |--------|------|-------| | 404 | `NOT_FOUND` | Sandbox does not exist or belongs to a different tenant. | ```bash curl https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Destroy a Sandbox **`DELETE /api/v1/sandboxes/{id}`** Permanently deletes the sandbox, removes saved state, and settles billing. Use `POST /pause` when the user is done for now but may resume later. ### Auth Bearer token required. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. | ### Response - `200 OK` ```json { "id": "sbx_01j9xr2t4fk8me3n5q", "state": "destroyed", "total_runtime_sec": 42 } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 404 | `NOT_FOUND` | Sandbox does not exist or belongs to a different tenant. | ```bash curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Pause and Resume **`POST /api/v1/sandboxes/{id}/pause`** suspends guest vCPU execution for a running persistent sandbox while preserving its workspace. It returns `409` if the sandbox is not running. **`POST /api/v1/sandboxes/{id}/resume`** transitions a paused sandbox back to `running` and returns `409` if it is not paused. Command and file operations can automatically resume a paused persistent sandbox before dispatch. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/pause \ -H "Authorization: Bearer $MIOSA_API_KEY" curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/resume \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Idempotency-Key: resume-session-001" ``` --- ## Restart **`POST /api/v1/sandboxes/{id}/restart`** power-cycles a running sandbox under the same id. It stops the sandbox keeping its disk, then boots that disk again. Files, packages and enabled services survive; running processes do not. Use it when a sandbox stops answering. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/restart \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` | Status | Code | Meaning | |---|---|---| | `200` | | Restarted: `{"id", "state": "running", "restarted": true}`. | | `202` | | Still booting: `state` is `resuming` or `starting`; poll `GET /sandboxes/{id}`. | | `409` | `SANDBOX_NOT_RUNNING` | Only a running sandbox restarts; use `POST /resume`. | | `409` | `RESTART_NOT_POWER_CYCLED` | Its host stopped it with a memory checkpoint, so it was resumed from memory: running again, processes not restarted. | | `502` | `RESTART_BOOT_FAILED` | It stopped and kept its disk but did not boot; `POST /resume` boots it again. | | `503` | `RESTART_STOP_IN_PROGRESS` | It is still stopping; resume it once it is `stopped`. | --- ## Fork a Sandbox **`POST /api/v1/sandboxes/{id}/fork`** creates a new sandbox from a copy-on-write snapshot of a running sandbox. The optional body accepts `timeout_sec` and `template_id` for the new sandbox. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/fork \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: fork-experiment-001" \ -d '{"timeout_sec":3600}' ``` Canonical public V1 exposes standalone snapshot create, list, inspect, delete, and restore operations. Use fork when a customer needs an independent branch of current running state without managing the intermediate snapshot directly. --- ## Extend the Timeout **`POST /api/v1/sandboxes/{id}/extend`** Replaces the active-session timeout with `timeout_sec` from 1 through 86400. The operation does not add duration to the old value. Omitting the body preserves the current timeout. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/extend \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"timeout_sec":7200}' ``` --- ## Get Sandbox Usage **`GET /api/v1/sandboxes/{id}/usage`** Returns measured runtime, provisioned vCPU time, active CPU, network traffic, estimated cost, and timeout visibility. ```json { "data": { "sandbox_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "state": "running", "runtime_sec": 47, "provisioned_vcpu_ms": 94000, "active_cpu_ms": 12000, "network_ingress_bytes": 2048, "network_egress_bytes": 4096, "measurement_status": { "active_cpu": "measured", "network": "measured", "provisioned_resources": "measured" }, "estimated_cost_cents": 1, "timeout_sec": 3600, "timeout_remaining_ms": 3553000 } } ``` Interpret nullable measurements through `measurement_status`. `unavailable` means no trustworthy value exists and the related field is `null`, not zero. `stale` means the retained value is not current and must be labeled as such. `timeout_remaining_ms` can be `null` before start or when timeout enforcement is disabled. --- # Services API URL: https://miosa.ai/docs/api-reference/services Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/services Source: src/routes/docs/api-reference/services/+page.md Description: API reference for managing long-running systemd services inside MIOSA computers. Services are persistent processes managed by systemd inside a computer. Use them for web servers, background workers, databases, or any daemon that should survive across exec calls and restart automatically on failure. Base path: `/api/v1/computers/{id}/services` Services map directly to systemd unit files (`/etc/systemd/system/miosa-{name}.service`) managed by the computer command service. The computer must be **running** to interact with services. --- ## Quick Start ```typescript const client = new Miosa(); // Register and start a web server const svc = await client.services.create(computerId, { name: 'webserver', command: 'python3 -m http.server 8080', cwd: '/workspace/app', restart: 'always', }); console.log(svc.status); // "starting" // Check status later const updated = await client.services.get(computerId, svc.id); console.log(updated.status); // "running" ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "webserver", "command": "python3 -m http.server 8080", "cwd": "/workspace/app", "restart": "always" }' ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/computers/{id}/services` | Create and start a service | | `GET` | `/computers/{id}/services` | List all services on a computer | | `GET` | `/computers/{id}/services/{name}` | Get a service by name | | `POST` | `/computers/{id}/services/{name}/start` | Start a stopped service | | `POST` | `/computers/{id}/services/{name}/stop` | Stop a running service | | `POST` | `/computers/{id}/services/{name}/restart` | Restart a service | | `DELETE` | `/computers/{id}/services/{name}` | Remove a service | --- ## Create a Service **`POST /api/v1/computers/{id}/services`** Writes the systemd unit file and starts the service. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Service name. Pattern: `[a-z][a-z0-9-]{0,62}` | | `command` | string | Yes | Full command to run | | `env` | object | No | Environment variables as `{ "KEY": "value" }` | | `cwd` | string | No | Working directory (default: `/workspace`) | | `restart` | string | No | `"always"`, `"on-failure"` (default), or `"no"` | ### Response - `201 Created` ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "computer_id": "...", "tenant_id": "...", "name": "webserver", "command": "python3 -m http.server 8080", "env": {}, "cwd": "/workspace/app", "restart": "always", "status": "starting", "pid": null, "exit_code": null, "last_started_at": null, "last_exited_at": null, "inserted_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } } ``` ### Status Values | Status | Description | |--------|-------------| | `stopped` | Registered but not started | | `starting` | Start dispatched to systemd | | `running` | systemd reports `ActiveState=active` | | `failed` | systemd reports `ActiveState=failed` | | `removed` | Unit file deleted; record kept for audit | ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `a service with this name already exists` | Name must be unique per computer | | 422 | Validation error | Invalid name format or restart policy | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "api-server", "command": "/workspace/app/server", "env": {"PORT": "3000", "NODE_ENV": "production"}, "cwd": "/workspace/app", "restart": "on-failure" }' ``` --- ## List Services **`GET /api/v1/computers/{id}/services`** ### Response - `200 OK` ```json { "data": [ { "id": "...", "name": "webserver", "status": "running", "pid": 12345, "restart": "always", "inserted_at": "2026-04-11T00:00:00Z" } ] } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/services \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get a Service **`GET /api/v1/computers/{id}/services/{name}`** ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Computer ID | | `name` | string | Service name | ### Response - `200 OK` Full service object (same as create response). ### Errors | Status | Error | Cause | |--------|-------|-------| | 404 | `service not found` | Does not exist on this computer | --- ## Start, Stop, Restart **`POST /api/v1/computers/{id}/services/{name}/start`** **`POST /api/v1/computers/{id}/services/{name}/stop`** **`POST /api/v1/computers/{id}/services/{name}/restart`** All return a `200 OK` with the updated service object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `service is already running` | Cannot start a running service | | 409 | `service is not running` | Cannot stop a non-running service | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash # Stop a service curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services/webserver/stop \ -H "Authorization: Bearer $MIOSA_API_KEY" # Restart it curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services/webserver/restart \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Remove a Service **`DELETE /api/v1/computers/{id}/services/{name}`** Stops the service and removes the systemd unit file. The database record transitions to `"removed"` status. ### Response - `200 OK` ```json { "data": { "deleted": true } } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/services/webserver \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Common Recipes ### Deploy a Node.js app as a service ```typescript // Upload app files first await client.files.write(computerId, { path: '/workspace/app/server.js', content: serverJsContent, }); // Register and start const svc = await client.services.create(computerId, { name: 'node-app', command: 'node /workspace/app/server.js', env: { PORT: '3000' }, restart: 'on-failure', }); ``` ### Wait until a service is running ```python svc = client.services.create(computer_id, name="worker", command="/usr/bin/worker") while svc.status == "starting": time.sleep(1) svc = client.services.get(computer_id, svc.name) if svc.status != "running": raise RuntimeError(f"Service failed to start: {svc.status}") ``` ### Check for failed services in a fleet ```typescript const computers = await client.computers.list(); for (const computer of computers.data) { if (computer.status !== 'running') continue; const services = await client.services.list(computer.id); const failed = services.data.filter(s => s.status === 'failed'); if (failed.length > 0) { console.log(`${computer.name}: ${failed.length} failed services`); } } ``` --- # Snapshots API URL: https://miosa.ai/docs/api-reference/snapshots Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/snapshots Source: src/routes/docs/api-reference/snapshots/+page.md Description: API reference for creating, listing, and restoring microVM snapshots (checkpoints). Snapshots capture the full in-memory state of a running computer - CPU state, RAM, and filesystem - and store it durably. Restore a snapshot to spin up an identical computer in seconds. This page documents desktop Computer snapshots only. Canonical sandbox V1 does not expose direct snapshot CRUD; use [Persistence, pause, and forks](/docs/develop/snapshots/) and the [fork endpoint](/docs/api-reference/sandboxes/#fork-a-sandbox). Base path: `/api/v1/computers/{id}/snapshots` Snapshots require the computer to be **running**. Creating a snapshot does not stop the computer. --- ## Quick Start ```typescript const client = new Miosa(); // Create a snapshot const snap = await client.snapshots.create(computerId, { comment: 'before-deploy-v2', }); // Poll until ready let status = snap.status; while (status !== 'ready') { await new Promise(r => setTimeout(r, 3000)); const updated = await client.snapshots.get(computerId, snap.id); status = updated.status; } // Restore - creates a new computer from the snapshot const restored = await client.snapshots.restore(computerId, snap.id); console.log(`Restored computer: ${restored.id}`); ``` ```bash # Create a snapshot curl -X POST https://api.miosa.ai/api/v1/computers/{id}/snapshots \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"comment": "before-deploy-v2"}' ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/computers/{id}/snapshots` | Create a snapshot | | `GET` | `/computers/{id}/snapshots` | List snapshots for a computer | | `GET` | `/computers/{id}/snapshots/{snap_id}` | Get a snapshot | | `DELETE` | `/computers/{id}/snapshots/{snap_id}` | Delete a snapshot | | `POST` | `/computers/{id}/restore/{snap_id}` | Restore from snapshot | | `GET` | `/computers/{id}/snapshots/{snap_id}/events` | SSE progress stream | --- ## Create a Snapshot **`POST /api/v1/computers/{id}/snapshots`** Initiates an asynchronous snapshot. The returned status will be `"creating"`. Poll `GET /snapshots/{snap_id}` or subscribe to SSE events for progress. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `comment` | string | No | Human-readable label (max 500 chars) | ### Response - `201 Created` ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "computer_id": "...", "tenant_id": "...", "comment": "before-deploy-v2", "status": "creating", "state_size_bytes": null, "memory_size_bytes": null, "rootfs_size_bytes": null, "compressed_size_bytes": null, "s3_bucket": null, "s3_prefix": null, "error": null, "created_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } } ``` ### Status State Machine ### Errors | Status | Error | Cause | |--------|-------|-------| | 404 | `computer not found` | Computer does not exist or wrong tenant | | 409 | `computer is not running` | Snapshot requires running VM | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/snapshots \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"comment": "checkpoint-before-risky-op"}' ``` --- ## List Snapshots **`GET /api/v1/computers/{id}/snapshots`** ### Response - `200 OK` ```json { "data": [ { "id": "...", "comment": "before-deploy-v2", "status": "ready", "compressed_size_bytes": 524288000, "created_at": "2026-04-11T00:00:00Z" } ] } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/snapshots \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get a Snapshot **`GET /api/v1/computers/{id}/snapshots/{snap_id}`** ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Computer ID | | `snap_id` | UUID | Snapshot ID | ### Response - `200 OK` Full snapshot object (same shape as create response). ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Snapshot belongs to a different tenant | | 404 | `snapshot not found` | Does not exist | ```bash curl https://api.miosa.ai/api/v1/computers/{id}/snapshots/{snap_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Delete a Snapshot **`DELETE /api/v1/computers/{id}/snapshots/{snap_id}`** Soft-deletes the snapshot (transitions to `"deleted"` status). S3 objects are cleaned up asynchronously. ### Response - `200 OK` ```json { "data": { "id": "...", "status": "deleted" } } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 404 | `snapshot not found` | Does not exist | | 409 | `snapshot is not in a deletable state` | Snapshot is being restored | ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/snapshots/{snap_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Restore from Snapshot **`POST /api/v1/computers/{id}/restore/{snap_id}`** Creates a new computer with the state from the snapshot. The original computer is unchanged. ### Response - `201 Created` ```json { "data": { "id": "new-computer-uuid", "name": "my-computer-restored", "status": "provisioning", "size": "small" }, "snapshot": { "id": "snap-uuid", "status": "restoring" } } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 404 | `snapshot not found` | Does not exist | | 409 | `snapshot is not ready` | Snapshot must be in `ready` state | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/restore/{snap_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## SSE Progress Stream **`GET /api/v1/computers/{id}/snapshots/{snap_id}/events`** Subscribe to real-time snapshot progress. Requires a short-lived ticket rather than a Bearer token (browsers cannot set Authorization on EventSource connections). ### Event Types | Event | Description | |-------|-------------| | `snapshot_creating` | VM writing memory and VM state to disk | | `snapshot_uploading` | Compressing and uploading to object storage | | `snapshot_ready` | Upload complete; snapshot is usable | | `snapshot_failed` | Unrecoverable error - see `error` field | --- ## Common Recipes ### Automated checkpoint before risky operations ```python from miosa import Miosa client = Miosa() snap = client.snapshots.create(computer_id, comment="pre-migration") while snap.status not in ("ready", "failed"): time.sleep(2) snap = client.snapshots.get(computer_id, snap.id) if snap.status == "failed": raise RuntimeError(f"Snapshot failed: {snap.error}") # Proceed with risky operation knowing you can restore do_migration(computer_id) ``` ### Snapshot-based parallelism (clone a baseline) ```typescript // Build a baseline environment once, clone it N times for parallel jobs const baseline = await client.snapshots.create(setupComputerId, { comment: 'baseline-env', }); // Each restore creates an independent computer from the same state const workers = await Promise.all( Array.from({ length: 5 }, () => client.snapshots.restore(setupComputerId, baseline.id) ) ); ``` --- ## See also - [Computers API](/docs/api-reference/computers/) - computer lifecycle - [Events (SSE)](/docs/api-reference/events/) - stream snapshot progress events - [Error Codes](/docs/api-reference/errors/) - snapshot-specific error codes --- # Streaming Exec API URL: https://miosa.ai/docs/api-reference/streaming-exec Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/streaming-exec Source: src/routes/docs/api-reference/streaming-exec/+page.md Description: API reference for streaming command output in real-time from MIOSA computers via SSE. Streaming exec runs a command on a computer and delivers output line-by-line via Server-Sent Events. Use it for long-running builds, installs, or any command where you want incremental output rather than waiting for completion. Base path: `/api/v1/computers/{id}/exec/stream` Standard `POST /computers/{id}/exec` waits for the command to finish before returning. Streaming exec is better for commands that run longer than a few seconds or that you want to monitor interactively. --- ## Quick Start ```typescript const client = new Miosa(); // Stream a long-running build for await (const event of client.exec.stream(computerId, { command: 'pip install torch', timeout: 300, })) { if (event.type === 'output') { process.stdout.write(event.data.line); } else if (event.type === 'exit') { console.log(`Exit code: ${event.data.exitCode}`); break; } } ``` ```bash # Obtain a streaming ticket first, then connect TICKET=$(curl -s -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/stream \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "pip install torch", "timeout": 300}' | jq -r .ticket) curl -N "https://api.miosa.ai/api/v1/computers/{id}/exec/stream/events?ticket=$TICKET" \ -H "Accept: text/event-stream" ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/computers/{id}/exec/stream` | Start a streaming exec and obtain a ticket | | `GET` | `/computers/{id}/exec/stream/events` | SSE stream (requires ticket) | | `POST` | `/computers/{id}/exec` | Non-streaming exec (waits for completion) | | `POST` | `/computers/{id}/exec/python` | Non-streaming Python exec | --- ## Start Streaming Exec **`POST /api/v1/computers/{id}/exec/stream`** Starts the command and returns a short-lived ticket for the SSE stream. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `command` | string | Yes | Shell command to execute | | `timeout` | integer | No | Timeout in seconds (default: 30, max: 86400) | | `shell` | string | No | Shell to use (default: `/bin/bash`) | ### Response - `202 Accepted` ```json { "ticket": "exec_short_lived_token", "expires_at": 1712700060, "stream_url": "/api/v1/computers/{id}/exec/stream/events?ticket=exec_short_lived_token" } ``` The ticket expires in 60 seconds - open the EventSource connection immediately. ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `COMPUTER_NOT_RUNNING` | Computer is not running | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/stream \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "npm install", "timeout": 120}' ``` --- ## Connect to SSE Stream **`GET /api/v1/computers/{id}/exec/stream/events?ticket={ticket}`** ### Event Types | Event | Payload Fields | Description | |-------|---------------|-------------| | `output` | `line`, `stream` | A line of stdout or stderr | | `exit` | `exit_code`, `duration_ms` | Command finished; stream closes after this | | `timeout` | `timeout_seconds` | Command killed after timeout | | `error` | `message` | Internal error before command started | ### Event Payload Examples ``` event: output data: {"line": "Collecting torch\n", "stream": "stdout"} event: output data: {"line": " Downloading torch-2.3.0-...\n", "stream": "stdout"} event: exit data: {"exit_code": 0, "duration_ms": 45312} ``` ### `stream` Values | Value | Description | |-------|-------------| | `stdout` | Standard output | | `stderr` | Standard error | ```javascript // Browser const es = new EventSource(`/api/v1/computers/${id}/exec/stream/events?ticket=${ticket}`); es.addEventListener('output', e => { const { line, stream } = JSON.parse(e.data); console.log(`[${stream}] ${line}`); }); es.addEventListener('exit', e => { const { exit_code } = JSON.parse(e.data); console.log(`Exited with code ${exit_code}`); es.close(); }); es.addEventListener('error', () => { // Connection dropped or ticket expired es.close(); }); ``` --- ## Comparison: Streaming vs Non-Streaming | Aspect | `POST /exec` | `POST /exec/stream` | |--------|-------------|---------------------| | Response timing | After command completes | Immediate (ticket) | | Output delivery | All at once | Line by line via SSE | | Max timeout | 86400 s | 86400 s | | Best for | Quick commands | Long builds, installs | | SDK method | `exec.run()` | `exec.stream()` | --- ## Common Recipes ### Monitor a build ```python for event in client.exec.stream(computer_id, command="make build", timeout=300): if event.type == "output": print(event.data["line"], end="") elif event.type == "exit": if event.data["exit_code"] != 0: raise RuntimeError(f"Build failed with exit code {event.data['exit_code']}") print("Build succeeded") break ``` ### Capture both streams separately ```typescript const stdout: string[] = []; const stderr: string[] = []; for await (const event of client.exec.stream(computerId, { command: 'myapp 2>&1 | tee /tmp/output.log' })) { if (event.type === 'output') { if (event.data.stream === 'stdout') stdout.push(event.data.line); else stderr.push(event.data.line); } else if (event.type === 'exit') { break; } } ``` ### Timeout handling ```python try: for event in client.exec.stream(computer_id, command="long-task", timeout=60): if event.type == "timeout": print(f"Command killed after {event.data['timeout_seconds']}s") break elif event.type == "exit": break except Exception as e: print(f"Stream error: {e}") ``` --- ## Sandbox streaming exec Sandboxes stream command output directly over SSE, without a separate ticket step. **`POST /api/v1/sandboxes/{id}/exec/stream`** Runs a command and streams stdout/stderr events as Server-Sent Events. Use this for installer output, long-running agent tasks, and build logs that should show progress before the command exits. Request body is the same as `/exec`. ```bash curl -N -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/exec/stream \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Accept: text/event-stream" \ -H "Content-Type: application/json" \ -d '{"command":"npm install && npm run build","working_dir":"/workspace","timeout":300}' ``` Example events: ```text event: stdout data: {"line":"added 342 packages"} event: exit data: {"exit_code":0} ``` --- # Tenant Branding API URL: https://miosa.ai/docs/api-reference/tenant-branding Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/tenant-branding Source: src/routes/docs/api-reference/tenant-branding/+page.md Description: Customize the logo, name, and colors shown on sandbox 404 and 502 error pages. The branding API controls the appearance of proxy-generated error pages (404, 502, gateway timeout) served within your custom preview domain. Set your logo, product name, support URL, and primary accent color so end-users see your brand - not MIOSA's. Base path: `/api/v1/tenant/branding` Branding changes are served immediately from cache. There is no propagation delay. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/tenant/branding` | Get current branding | | `PUT` | `/api/v1/tenant/branding` | Set or replace branding | | `DELETE` | `/api/v1/tenant/branding` | Reset to MIOSA defaults | --- ## Get Branding **`GET /api/v1/tenant/branding`** ### Auth ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` ```json { "branding": { "logo_url": "https://cdn.example.com/logo.png", "name": "Lumen", "support_url": "https://example.com/support", "primary_color": "#0066FF" } } ``` Returns `{"branding": null}` if no custom branding has been set. --- ## Set Branding **`PUT /api/v1/tenant/branding`** Replaces all branding fields. Omitted fields are cleared to their defaults. ### Request Body Wrap fields under a `"branding"` key. | Field | Type | Required | Description | |-------|------|----------|-------------| | `logo_url` | string | No | HTTPS URL to your logo image | | `name` | string | No | Product or company name | | `support_url` | string | No | Link shown on error pages for user support | | `primary_color` | string | No | Hex color for CTA buttons (e.g. `"#0066FF"`) | ```json { "branding": { "logo_url": "https://cdn.example.com/logo.png", "name": "Lumen", "support_url": "https://example.com/support", "primary_color": "#0066FF" } } ``` ### Response - `200 OK` ```json { "branding": { "logo_url": "https://cdn.example.com/logo.png", "name": "Lumen", "support_url": "https://example.com/support", "primary_color": "#0066FF" } } ``` --- ## Delete Branding **`DELETE /api/v1/tenant/branding`** Resets all branding to MIOSA defaults. Error pages will show the MIOSA logo and color scheme. ### Response - `200 OK` ```json { "branding": null } ``` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 404 | `tenant not found` | Tenant does not exist | | 422 | validation error | A field failed validation (e.g. invalid URL, invalid hex color) | --- ## Examples ```bash # Set branding curl -X PUT https://api.miosa.ai/api/v1/tenant/branding \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{ "branding": { "logo_url": "https://cdn.example.com/logo.png", "name": "Lumen", "support_url": "https://example.com/support", "primary_color": "#0066FF" } }' # Get curl https://api.miosa.ai/api/v1/tenant/branding \ -H "Authorization: Bearer msk_u_..." # Reset curl -X DELETE https://api.miosa.ai/api/v1/tenant/branding \ -H "Authorization: Bearer msk_u_..." ``` ```python client = miosa.Miosa() client.tenant.set_branding( logo_url="https://cdn.example.com/logo.png", name="Lumen", support_url="https://example.com/support", primary_color="#0066FF", ) branding = client.tenant.get_branding() print(branding.name) ``` ```typescript const client = new Miosa(); await client.tenant.setBranding({ logoUrl: 'https://cdn.example.com/logo.png', name: 'Lumen', supportUrl: 'https://example.com/support', primaryColor: '#0066FF', }); const branding = await client.tenant.getBranding(); console.log(branding?.name); ``` --- # Tenant Events (SSE) API URL: https://miosa.ai/docs/api-reference/tenant-events Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/tenant-events Source: src/routes/docs/api-reference/tenant-events/+page.md Description: Subscribe to a real-time server-sent event stream of all tenant-wide platform events. The tenant events stream delivers real-time Server-Sent Events (SSE) for all activity across your tenant - sandbox lifecycle changes, deployments, webhook deliveries, and more. Subscribe once and receive events for all your resources. Base path: `/api/v1/events/stream` This endpoint uses `text/event-stream` (SSE). Set `Accept: text/event-stream` on your request, or use a native `EventSource` client. The `Authorization` header cannot be set by browser `EventSource`. Use a server-side client or pass an SSE ticket obtained from `POST /api/v1/auth/sse-ticket` for browser usage. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/events/stream` | Open tenant-wide SSE event stream | --- ## Open Event Stream **`GET /api/v1/events/stream`** Upgrades to an SSE stream. The connection stays open until the client disconnects or the server closes it. A `: heartbeat` comment is sent every 15 seconds to keep the connection alive through proxies. ### Auth ``` Authorization: Bearer msk_... ``` ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `types` | string | No | Comma-separated glob patterns to filter events (e.g. `sandbox.*,webhook.delivered`) | ### Response - `200 OK` (`text/event-stream`) Each event is a standard SSE frame: ``` id: evt_01hwxyz... event: sandbox.ready data: {"type":"sandbox.ready","id":"sbx_abc123","status":"running","tenant_id":"...","ts":"2026-05-26T10:14:23Z"} id: evt_01hwyza... event: deployment.succeeded data: {"type":"deployment.succeeded","id":"dep_xyz456","url":"https://my-app.miosa.app","ts":"2026-05-26T10:15:01Z"} : heartbeat ``` ### Event Shape All events share a common envelope: ```json { "type": "sandbox.ready", "id": "sbx_abc123", "tenant_id": "ten_...", "ts": "2026-05-26T10:14:23Z" } ``` Additional fields vary by event type (see table below). --- ## Event Types ### Sandbox Events | Type | Description | Extra Fields | |------|-------------|-------------| | `sandbox.created` | Sandbox provisioned | `image`, `workspace_id` | | `sandbox.ready` | Sandbox reached `running` status | `status` | | `sandbox.error` | Sandbox failed to start | `error`, `reason` | | `sandbox.destroyed` | Sandbox deleted | - | | `sandbox.paused` | Sandbox vCPUs suspended | - | | `sandbox.resumed` | Sandbox vCPUs resumed | - | ### Computer Events | Type | Description | Extra Fields | |------|-------------|-------------| | `computer.created` | Computer provisioned | `size`, `workspace_id` | | `computer.running` | Computer reached `running` status | - | | `computer.stopped` | Computer stopped | - | | `computer.deleted` | Computer deleted | - | ### Deployment Events | Type | Description | Extra Fields | |------|-------------|-------------| | `deployment.build_started` | Build job enqueued | `version_id` | | `deployment.succeeded` | Build completed and promoted | `url`, `version_id` | | `deployment.failed` | Build or promotion failed | `error` | | `deployment.rollback` | Version rolled back | `from_version`, `to_version` | ### Webhook Events | Type | Description | Extra Fields | |------|-------------|-------------| | `webhook.delivered` | Outgoing webhook POST succeeded | `webhook_id`, `status_code` | | `webhook.failed` | Outgoing webhook POST failed | `webhook_id`, `attempt`, `error` | ### Filtering with `types` Pass a comma-separated list of glob patterns. The `*` wildcard matches any suffix segment: ``` ?types=sandbox.* # all sandbox events ?types=sandbox.*,computer.running ?types=deployment.succeeded,deployment.failed ``` An empty `types` parameter (or no parameter) delivers all events. --- ## Reconnection On disconnect, reconnect using the `Last-Event-ID` header set to the last `id` you received. The server will resume from that cursor, replaying up to 60 seconds of backlogged events. ``` GET /api/v1/events/stream Authorization: Bearer msk_... Last-Event-ID: evt_01hwxyz... ``` --- ## Examples ```bash # All events curl -N -H "Authorization: Bearer msk_u_..." \ -H "Accept: text/event-stream" \ "https://api.miosa.ai/api/v1/events/stream" # Sandbox events only curl -N -H "Authorization: Bearer msk_u_..." \ -H "Accept: text/event-stream" \ "https://api.miosa.ai/api/v1/events/stream?types=sandbox.*" ``` ```python headers = { "Authorization": "Bearer msk_u_...", "Accept": "text/event-stream", } with httpx.stream("GET", "https://api.miosa.ai/api/v1/events/stream", params={"types": "sandbox.*"}, headers=headers) as r: for line in r.iter_lines(): if line.startswith("data:"): event = json.loads(line[5:].strip()) print(event["type"], event["id"]) ``` ```typescript // Server-side (Node.js / Deno) - EventSource doesn't support custom headers in browsers const es = new EventSource( 'https://api.miosa.ai/api/v1/events/stream?types=sandbox.*', { headers: { Authorization: 'Bearer msk_u_...' } } ); es.addEventListener('sandbox.ready', (e) => { const data = JSON.parse(e.data); console.log('Sandbox ready:', data.id); }); es.addEventListener('sandbox.error', (e) => { const data = JSON.parse(e.data); console.error('Sandbox failed:', data.id, data.error); }); es.onerror = () => es.close(); ``` --- # Tenant Preview Domain API URL: https://miosa.ai/docs/api-reference/tenant-preview-domain Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/tenant-preview-domain Source: src/routes/docs/api-reference/tenant-preview-domain/+page.md Description: Configure a custom apex domain for sandbox preview URLs (white-label). The preview domain API lets you map your own apex domain (e.g. `example.com`) to sandbox preview URLs. When configured, sandboxes created under your tenant will be accessible at `{sandbox-slug}.{your-domain}` instead of the default `*.sandbox.miosa.ai`. **Auth note:** These endpoints accept both `msk_*` API keys and JWT tokens. This matters for white-label integrations where you programmatically manage the domain from a backend service. Only apex domains are accepted (e.g. `example.com`, not `app.example.com` or `https://example.com`). Wildcard and subdomain inputs are rejected with `422 invalid_domain`. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/tenant/preview-domain` | Get current domain and verification status | | `PUT` | `/api/v1/tenant/preview-domain` | Set or replace the preview domain | | `POST` | `/api/v1/tenant/preview-domain/verify` | Recheck DNS verification | | `DELETE` | `/api/v1/tenant/preview-domain` | Remove the custom domain | --- ## Get Preview Domain **`GET /api/v1/tenant/preview-domain`** Returns the configured domain and its DNS verification status. ### Auth ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` (domain configured) ```json { "preview_domain": "example.com", "status": "configured", "dns_instructions": { "type": "CNAME", "name": "*.example.com", "value": "proxy.miosa.ai" } } ``` ### Response - `200 OK` (not configured) ```json { "preview_domain": null, "default_domain": "miosa.ai", "status": "not_configured", "dns_instructions": null } ``` | Field | Type | Description | |-------|------|-------------| | `preview_domain` | string\|null | Configured apex domain, or `null` | | `default_domain` | string | Fallback domain used when no custom domain is set | | `status` | string | `"not_configured"`, `"pending_dns"`, or `"configured"` | | `dns_instructions` | object\|null | CNAME record to add at your DNS registrar | --- ## Set Preview Domain **`PUT /api/v1/tenant/preview-domain`** Sets or replaces the custom preview domain. DNS verification is asynchronous - status transitions from `pending_dns` to `configured` once the CNAME resolves correctly. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `preview_domain` | string | Yes | Apex domain (e.g. `"example.com"`) | ```json { "preview_domain": "example.com" } ``` ### Response - `200 OK` ```json { "preview_domain": "example.com", "status": "pending_dns", "dns_status": "pending", "dns_instructions": { "type": "CNAME", "name": "*.example.com", "value": "proxy.miosa.ai" } } ``` ### DNS Setup Add a wildcard CNAME at your registrar: | Type | Name | Value | |------|------|-------| | `CNAME` | `*` (or `*.example.com`) | `proxy.miosa.ai` | --- ## Verify DNS **`POST /api/v1/tenant/preview-domain/verify`** Triggers an immediate DNS re-check. Returns the current verification status without waiting for the background poller. ### Response - `200 OK` ```json { "preview_domain": "example.com", "status": "configured", "dns_status": "verified" } ``` --- ## Delete Preview Domain **`DELETE /api/v1/tenant/preview-domain`** Removes the custom domain. Sandbox preview URLs revert to `*.sandbox.miosa.ai`. ### Response - `200 OK` ```json { "preview_domain": null, "status": "removed" } ``` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 400 | `preview_domain is required` | Missing body field | | 422 | `invalid_domain` | Protocol prefix, wildcard, path, or subdomain detected | | 404 | `tenant not found` | Tenant does not exist | --- ## Examples ```bash # Set curl -X PUT https://api.miosa.ai/api/v1/tenant/preview-domain \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{"preview_domain": "example.com"}' # Verify curl -X POST https://api.miosa.ai/api/v1/tenant/preview-domain/verify \ -H "Authorization: Bearer msk_u_..." # Get curl https://api.miosa.ai/api/v1/tenant/preview-domain \ -H "Authorization: Bearer msk_u_..." # Delete curl -X DELETE https://api.miosa.ai/api/v1/tenant/preview-domain \ -H "Authorization: Bearer msk_u_..." ``` ```python client = miosa.Miosa() # Set result = client.tenant.set_preview_domain("example.com") print(result.dns_instructions) # Poll until verified while True: domain = client.tenant.get_preview_domain() if domain.status == "configured": break time.sleep(30) client.tenant.verify_preview_domain() ``` ```typescript const client = new Miosa(); // Set const result = await client.tenant.setPreviewDomain('example.com'); console.log(result.dnsInstructions); // Get const domain = await client.tenant.getPreviewDomain(); console.log(domain.status); // "pending_dns" | "configured" // Force re-check await client.tenant.verifyPreviewDomain(); // Remove await client.tenant.deletePreviewDomain(); ``` --- # Usage Rollup API URL: https://miosa.ai/docs/api-reference/usage-rollup Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/usage-rollup Source: src/routes/docs/api-reference/usage-rollup/+page.md Description: Aggregate sandbox compute usage and spend by external user, project, or workspace. The usage rollup endpoint aggregates metered usage across sandboxes and computers, grouped by an identifier of your choice. Use it to build per-user billing dashboards, enforce spend policies, or attribute costs to projects. Base path: `/api/v1/usage` Results are limited to 1000 rows per request. For high-cardinality tenants, narrow the period or use a more specific `group_by`. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/usage` | Aggregate usage for a period | --- ## Get Usage Rollup **`GET /api/v1/usage`** ### Auth ``` Authorization: Bearer msk_... ``` ### Query Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `group_by` | string | No | `external_user_id` | Dimension to aggregate by | | `period` | string | No | `30d` | Predefined period or `custom` | | `start` | string | No | - | ISO 8601 start (required when `period=custom`) | | `end` | string | No | - | ISO 8601 end (required when `period=custom`) | **`group_by` values:** | Value | Description | |-------|-------------| | `external_user_id` | Group by the `external_user_id` you set on sandboxes | | `external_project_id` | Group by `external_project_id` | | `workspace_id` | Group by MIOSA workspace | **`period` values:** | Value | Description | |-------|-------------| | `7d` | Last 7 days | | `30d` | Last 30 days | | `mtd` | Month-to-date (calendar month, resets on the 1st) | | `custom` | Arbitrary range - requires `start` and `end` | ### Response - `200 OK` ```json { "period_start": "2026-04-26T00:00:00Z", "period_end": "2026-05-26T00:00:00Z", "results": [ { "external_user_id": "user_42", "sandbox_seconds": 14400, "computer_seconds": 0, "storage_gb_hours": 2.5, "credit_cents": 312 }, { "external_user_id": "user_99", "sandbox_seconds": 3600, "computer_seconds": 7200, "storage_gb_hours": 0.0, "credit_cents": 108 } ] } ``` | Field | Type | Description | |-------|------|-------------| | `period_start` | string | ISO 8601 start of aggregation window | | `period_end` | string | ISO 8601 end of aggregation window | | `results[].{group_by}` | string | Value of the grouping dimension | | `results[].sandbox_seconds` | number | vCPU-seconds used by sandboxes | | `results[].computer_seconds` | number | vCPU-seconds used by computers | | `results[].storage_gb_hours` | number | GiB-hours of attached storage | | `results[].credit_cents` | number | Total cost in platform credit cents | Results are ordered by `credit_cents` descending. --- ## Errors | Status | Code | Cause | |--------|------|-------| | 422 | `invalid group_by` | `group_by` is not one of the accepted values | | 422 | `invalid period` | `period` is not one of the accepted values | | 422 | `invalid_custom_range` | `start` or `end` is missing or not valid ISO 8601 when `period=custom` | --- ## Examples ```bash # 30-day rollup by user curl "https://api.miosa.ai/api/v1/usage?group_by=external_user_id&period=30d" \ -H "Authorization: Bearer msk_u_..." # Custom range curl "https://api.miosa.ai/api/v1/usage?group_by=external_user_id&period=custom&start=2026-05-01T00:00:00Z&end=2026-05-26T00:00:00Z" \ -H "Authorization: Bearer msk_u_..." # Group by project curl "https://api.miosa.ai/api/v1/usage?group_by=external_project_id&period=mtd" \ -H "Authorization: Bearer msk_u_..." ``` ```python client = miosa.Miosa() # Monthly rollup per user results = client.usage.rollup( group_by="external_user_id", period="30d", ) for row in results: print(f"{row.external_user_id}: {row.credit_cents / 100:.2f} USD") ``` ```typescript const client = new Miosa(); const { results } = await client.usage.rollup({ groupBy: 'external_user_id', period: '30d', }); for (const row of results) { console.log(`${row.externalUserId}: $${(row.creditCents / 100).toFixed(2)}`); } ``` --- # API Reference / Versions URL: https://miosa.ai/docs/api-reference/versions Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/versions Source: src/routes/docs/api-reference/versions/+page.md Description: Immutable deployment version sub-resource. List, get, promote, archive. A **Deployment Version** is the immutable record of one publish. See [Versions](/docs/docker-host/versions/) for the conceptual model. ## Endpoints ```http GET /api/v1/deployments/:id/versions GET /api/v1/deployments/:id/versions/:version_id POST /api/v1/deployments/:id/versions/:version_id/promote ``` ## List ```http GET /api/v1/deployments/:id/versions GET /api/v1/deployments/:id/versions?state=ready GET /api/v1/deployments/:id/versions?workspace_id=550e8400-e29b-41d4-a716-446655440000 GET /api/v1/deployments/:id/versions?external_workspace_id=acct_123 GET /api/v1/deployments/:id/versions?limit=50 ``` Response: ```json { "data": [ { "id": "ver_...", "deployment_id": "dep_...", "version_number": 17, "kind": "static", "state": "ready", "artifact_sha256": "a5e6f0c1...", "source_sha256": "f1a3b8c2...", "promoted_at": "2026-05-14T18:20:00Z", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "external_workspace_id": "acct_123", "external_user_id": "user-456", "created_at": "2026-05-14T18:19:48Z" } ], "total": 17 } ``` ## Get ```http GET /api/v1/deployments/:id/versions/:version_id ``` Returns the full version row including `artifact_manifest`, `runtime_image`, `runtime_command`, `runtime_port`, `health_check_path`, `build_log_uri`. ## Promote Make a specific ready version the active one for the deployment. ```http POST /api/v1/deployments/:id/versions/:version_id/promote ``` Body: empty, or optionally `{ "environment": "production" }`. Scopes: `deployments:write`. Requires `Idempotency-Key` for safety on retries. Promotion is different from publish: - **Publish** creates a new version from a sandbox source. - **Promote** points an existing ready version as active in an environment. Use promote for canary / staged rollouts: publish to a non-production environment first, validate, then promote to production. ## States | State | Meaning | |---|---| | `created` | Row exists, build not started | | `building` | Build in progress | | `ready` | Build succeeded, artifact uploaded, promotable | | `failed` | Build or health check failed | | `archived` | Promoted-out-of; still bootable for rollback | State transitions are one-way. Once `ready` or `failed`, a version stays that way. ## Permissions | Action | Scope | |---|---| | List, Get | `deployments:read` | | Promote | `deployments:write` | ## See also - [Deployments](/docs/api-reference/deployments/) - parent resource - [Releases](/docs/api-reference/releases/) - artifact reference - [Versions](/docs/docker-host/versions/) - conceptual model - [Rollback](/docs/docker-host/rollback/) - uses promote internally --- # Webhooks API URL: https://miosa.ai/docs/api-reference/webhooks Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/webhooks Source: src/routes/docs/api-reference/webhooks/+page.md Description: Subscribe to platform lifecycle events via outgoing HTTP callbacks with HMAC signature verification. Webhooks fire outgoing HTTP POST requests to your endpoint when platform events occur - sandbox lifecycle changes, deployment completions, billing events, and more. Each delivery is signed with HMAC-SHA256 so you can verify authenticity. Base path: `/api/v1/webhooks` The webhook `secret` is returned **only once** - in the response to `POST /webhooks`. Store it immediately in a secure location. It cannot be retrieved later (only a masked preview is shown afterward). --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/webhooks` | List all webhooks | | `POST` | `/api/v1/webhooks` | Create a webhook | | `GET` | `/api/v1/webhooks/{id}` | Get a webhook | | `PATCH` | `/api/v1/webhooks/{id}` | Update a webhook | | `DELETE` | `/api/v1/webhooks/{id}` | Delete a webhook | | `POST` | `/api/v1/webhooks/{id}/test` | Send a test delivery | | `GET` | `/api/v1/webhooks/{id}/deliveries` | List delivery attempts | --- ## List Webhooks **`GET /api/v1/webhooks`** ### Auth ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` ```json { "data": [ { "id": "wh_01hwxyz...", "name": "My Webhook", "url": "https://api.myapp.com/hooks/miosa", "events": ["sandbox.created", "sandbox.destroyed"], "secret_preview": "sk_live_••••••••abcd", "retry_count": 3, "created_at": "2026-05-01T00:00:00Z", "updated_at": "2026-05-01T00:00:00Z" } ] } ``` --- ## Create a Webhook **`POST /api/v1/webhooks`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `url` | string | Yes | HTTPS endpoint to POST events to | | `name` | string | No | Human-readable label | | `events` | string[] | No | Event type filter; empty = all events | | `headers` | object | No | Extra headers to include in deliveries | | `retry_count` | integer | No | Consecutive failed deliveries before the webhook is marked `failed` (0-10, default `3`) | | `metadata` | object | No | Arbitrary key-value metadata | ```json { "url": "https://api.myapp.com/hooks/miosa", "name": "Sandbox lifecycle", "events": ["sandbox.created", "sandbox.ready", "sandbox.error", "sandbox.destroyed"], "retry_count": 3 } ``` ### Response - `201 Created` ```json { "data": { "id": "wh_01hwxyz...", "name": "Sandbox lifecycle", "url": "https://api.myapp.com/hooks/miosa", "events": ["sandbox.created", "sandbox.ready", "sandbox.error", "sandbox.destroyed"], "secret": "sk_live_abc123def456...", "secret_preview": "sk_live_••••••••3456", "retry_count": 3, "created_at": "2026-05-26T10:14:23Z", "updated_at": "2026-05-26T10:14:23Z" } } ``` The `secret` field appears only in this response. Save it securely. --- ## Update a Webhook **`PATCH /api/v1/webhooks/{id}`** Partial update - include only fields you want to change. ### Request Body Same fields as create, all optional. ### Response - `200 OK` Webhook object without the `secret` field. --- ## Delete a Webhook **`DELETE /api/v1/webhooks/{id}`** ### Response - `204 No Content` --- ## Send a Test Delivery **`POST /api/v1/webhooks/{id}/test`** Sends a synthetic `webhook.test` event to the configured URL to verify connectivity and signature verification. The attempt is recorded in the delivery log like any other delivery, marked as a test, so what the endpoint received is always visible at `GET /api/v1/webhooks/{id}/deliveries`. A test does not count toward `retry_count` and never moves the webhook to `failed`: a test against an endpoint that is temporarily down must not pause a live subscription. ### Response - `200 OK` ```json { "delivered": true, "status_code": 200, "duration_ms": 143 } ``` --- ## List Deliveries **`GET /api/v1/webhooks/{id}/deliveries`** Returns recent delivery attempts with status codes and timing. ### Response - `200 OK` ```json { "data": [ { "id": "del_01hwyza...", "event_type": "sandbox.ready", "status": "delivered", "status_code": 200, "attempt": 1, "duration_ms": 98, "delivered_at": "2026-05-26T10:14:24Z" }, { "id": "del_01hwyzb...", "event_type": "sandbox.created", "status": "failed", "status_code": 500, "attempt": 3, "duration_ms": 5012, "delivered_at": "2026-05-26T10:14:20Z" } ] } ``` --- ## Signature Verification Every delivery includes an `X-Miosa-Signature` header. Verify it on your server before trusting the payload: ``` X-Miosa-Signature: sha256= ``` Compute the expected signature: ``` hmac = HMAC-SHA256(secret, raw_request_body_bytes) expected = "sha256=" + hex(hmac) assert constant_time_equals(expected, request.headers["X-Miosa-Signature"]) ``` Always compare signatures using a constant-time function to prevent timing attacks. Never use `==` for string comparison. ### Example verification (Python) ```python def verify_signature(secret: str, body: bytes, header: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, header) ``` ### Example verification (TypeScript) ```typescript function verifySignature(secret: string, body: Buffer, header: string): boolean { const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex'); return timingSafeEqual(Buffer.from(expected), Buffer.from(header)); } ``` --- ## Retry Policy Failed deliveries are retried on a fixed backoff schedule: | Attempt | Delay | |---------|-------| | 1 (initial) | Immediate | | 2 | 1 minute | | 3 | 5 minutes | | 4 | 30 minutes | | 5 | 1 hour | | 6 | 2 hours | | 7 | 4 hours | | 8 | 8 hours | MIOSA makes up to eight delivery attempts over roughly 15.5 hours (within a 24-hour window), then drops the event. A delivery is considered failed if your endpoint returns a non-2xx status code or does not respond within 10 seconds. `retry_count` (0-10, default 3) is the number of consecutive failed deliveries after which MIOSA marks the webhook `failed`; it is not the per-delivery retry cap. Set it to `0` to mark the webhook failed on the first failed delivery. --- ## Subscribed Event Types `GET /api/v1/webhooks/events` returns this list, grouped by product surface. It is the same list the API validates a subscription against, so an event that is not here cannot be subscribed to. | Event | Description | |-------|-------------| | `sandbox.created` | Sandbox created | | `sandbox.starting` | Sandbox boot started | | `sandbox.ready` | Sandbox became usable: `ready` turned true, whether that was decided on boot or by a readiness probe | | `sandbox.started` | Sandbox runtime is up and reachable | | `sandbox.paused` | Sandbox left `running` for `paused` | | `sandbox.resumed` | A paused sandbox came back up | | `sandbox.idle_paused` | Sandbox was paused because it sat idle past its timeout | | `sandbox.error` | Sandbox failed to start or crashed | | `sandbox.destroyed` | Sandbox deleted | | `computer.created` | Computer created | | `computer.started` | Computer finished booting and is usable | | `computer.stopped` | Computer was asked to stop | | `computer.deleted` | Computer destroyed | | `database.created` | Database provisioned | | `database.ready` | Database accepting connections | | `database.stopped` | Database stopped | | `database.deleted` | Database deleted | | `managed_database.provisioning` | Managed database provisioning started | | `managed_database.running` | Managed database running | | `managed_database.failed` | Managed database provisioning failed | | `managed_database.destroyed` | Managed database destroyed | | `managed_service.provisioning` | Managed service provisioning started | | `managed_service.running` | Managed service running | | `managed_service.failed` | Managed service provisioning failed | | `managed_service.destroyed` | Managed service destroyed | | `deployment.created` | Deployment created | | `deployment.building` | Build started | | `deployment.build_started` | Build step began executing | | `deployment.build_succeeded` | Build produced an artifact | | `deployment.build_failed` | Build failed | | `deployment.ready` | Deployment serving traffic | | `deployment.error` | Deployment failed or crashed | | `deployment.canceled` | Deployment canceled | | `deployment.rolled_back` | Deployment rolled back to a previous version | | `preview.created` | Pull-request preview created | | `preview.active` | Preview serving traffic | | `preview.destroyed` | Preview torn down | | `health_check.healthy` | Endpoint health check passed | | `health_check.unhealthy` | Endpoint health check failed | | `cron.execution_started` | Scheduled job started | | `cron.execution_completed` | Scheduled job finished | | `cron.execution_failed` | Scheduled job failed | | `billing.credits_low` | Credit balance below the configured threshold | | `billing.credits_exhausted` | Credit balance reached zero | | `billing.sandbox.spend_threshold_reached` | Sandbox spend crossed its threshold | | `run.created` | Agent run created | | `run.queued` | Agent run queued | | `run.running` | Agent run started | | `run.progress` | Agent run reported progress | | `run.succeeded` | Agent run succeeded | | `run.failed` | Agent run failed | | `run.canceled` | Agent run canceled | | `run.file_ready` | A file produced by an agent run is ready | | `forge.push` | A branch in a Forge repository received commits | | `forge.pull_request` | A Forge pull request was opened, reopened, closed or merged | | `forge.check` | A check reported a result on a Forge commit | `forge.*` payloads carry `repository_id`, `repository_slug` and `organization_slug`, so one subscription can route each event to the right repository. `webhook.test` is the synthetic event sent by `POST /api/v1/webhooks/:id/test`. It is not subscribable and appears in the delivery log as a test delivery. Subscribe to all events by omitting the `events` field. Subscribe to a subset by listing specific event types. --- ## Examples ```bash # Create curl -X POST https://api.miosa.ai/api/v1/webhooks \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.myapp.com/hooks/miosa", "events": ["sandbox.created", "sandbox.destroyed"] }' # Test curl -X POST https://api.miosa.ai/api/v1/webhooks/wh_01hwxyz/test \ -H "Authorization: Bearer msk_u_..." # Deliveries curl https://api.miosa.ai/api/v1/webhooks/wh_01hwxyz/deliveries \ -H "Authorization: Bearer msk_u_..." ``` ```python client = miosa.Miosa() hook = client.webhooks.create( url="https://api.myapp.com/hooks/miosa", events=["sandbox.created", "sandbox.destroyed"], retry_count=3, ) # Save hook.secret securely - only available now print(hook.secret) # Test result = client.webhooks.test(hook.id) print(result.delivered, result.status_code) ``` ```typescript const client = new Miosa(); const hook = await client.webhooks.create({ url: 'https://api.myapp.com/hooks/miosa', events: ['sandbox.created', 'sandbox.ready', 'sandbox.destroyed'], retryCount: 3, }); // Store hook.secret in your secrets manager - only returned once console.log(hook.secret); // Test connectivity const result = await client.webhooks.test(hook.id); console.log(result.delivered, result.statusCode); ``` --- # Workspaces API URL: https://miosa.ai/docs/api-reference/workspaces Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/workspaces Source: src/routes/docs/api-reference/workspaces/+page.md Description: API reference for creating and managing MIOSA workspaces - the customer/client layer inside an organization. Workspaces group resources inside an organization. In a white-label platform, a workspace usually represents one downstream customer or client, such as `Acme Corp` inside the `Lumen` organization. Projects live inside workspaces, and sandboxes, computers, deployments, databases, storage buckets, volumes, functions, jobs, and domains belong to projects. Base path: `/api/v1/workspaces` A **default** workspace is created automatically when you sign up. Resources are assigned to the default workspace and its default project unless you specify workspace/project ownership at creation time. --- ## Quick Start ```typescript const client = new Miosa(); // reads MIOSA_API_KEY // Create a workspace for a downstream customer const ws = await client.workspaces.create({ name: 'Acme Corp', slug: 'acme-corp', externalWorkspaceId: 'acct_123', }); // Create a computer inside a project in that workspace const computer = await client.computers.create({ name: 'workspace-builder', template_type: 'miosa-desktop', workspaceId: ws.id, projectSlug: 'lead-magnet', projectName: 'Lead Magnet', }); // List computers in the workspace const { data } = await client.workspaces.listComputers(ws.id); ``` ```bash # curl equivalent curl -X POST https://api.miosa.ai/api/v1/workspaces \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Acme Corp", "slug": "acme-corp", "external_workspace_id": "acct_123"}' ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/workspaces` | List workspaces for the tenant | | `POST` | `/workspaces` | Create a workspace | | `GET` | `/workspaces/{id}` | Get a workspace | | `PATCH` | `/workspaces/{id}` | Update a workspace | | `PUT` | `/workspaces/{id}/settings` | Update workspace-level resource limits | | `DELETE` | `/workspaces/{id}` | Delete a workspace | | `GET` | `/workspaces/{id}/computers` | List computers in a workspace | | `GET` | `/workspaces/{id}/stats` | Computer counts and resource totals | | `GET` | `/workspaces/{id}/usage` | Time-series credit and compute usage | | `GET` | `/workspaces/{id}/projects` | List projects in a workspace | --- ## List Workspaces **`GET /api/v1/workspaces`** Returns all workspaces belonging to the authenticated tenant. ### Response - `200 OK` ```json { "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "tenant_id": "...", "name": "default", "slug": "default", "external_workspace_id": null, "description": null, "metadata": {}, "is_default": true, "computer_count": 3, "created_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Create a Workspace **`POST /api/v1/workspaces`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Human-readable name (1-120 chars) | | `slug` | string | No | URL-safe identifier. Auto-derived from `name` if omitted. Pattern: `[a-z0-9][a-z0-9-]{0,79}` | | `external_workspace_id` | string | No | Your customer/account/workspace ID. Globally unique inside this MIOSA organization. | | `description` | string | No | Optional description | | `metadata` | object | No | Caller metadata stored on the workspace. | ### Response - `201 Created` Full workspace object (same shape as list items above). ### Errors | Status | Error | Cause | |--------|-------|-------| | 422 | `has already been taken` | Slug already exists in tenant | | 422 | `must start with alphanumeric and contain only a-z 0-9 -` | Invalid slug format | ```bash curl -X POST https://api.miosa.ai/api/v1/workspaces \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Corp", "slug": "acme-corp", "external_workspace_id": "acct_123", "description": "Client workspace for Acme Corp" }' ``` --- ## Get a Workspace **`GET /api/v1/workspaces/{id}`** ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Workspace ID | ### Response - `200 OK` Full workspace object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Workspace belongs to a different tenant | | 404 | `workspace not found` | Does not exist | ```bash curl https://api.miosa.ai/api/v1/workspaces/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update a Workspace **`PATCH /api/v1/workspaces/{id}`** Update mutable workspace fields. Prefer treating the slug as stable once resources and URLs exist. ### Request Body | Field | Type | Description | |-------|------|-------------| | `name` | string | New display name | | `slug` | string | New URL-safe slug. Must remain unique inside the organization. | | `description` | string | New description (`null` to clear) | | `external_workspace_id` | string | Your customer/account/workspace ID | | `metadata` | object | Replacement metadata map | ### Response - `200 OK` Updated workspace object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Wrong tenant | | 404 | `workspace not found` | Does not exist | ```bash curl -X PATCH https://api.miosa.ai/api/v1/workspaces/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "production-us-east"}' ``` --- ## Delete a Workspace **`DELETE /api/v1/workspaces/{id}`** Deletes an empty workspace. Fails if the workspace contains computers or projects. ### Response - `200 OK` ```json { "deleted": true } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Wrong tenant | | 404 | `workspace not found` | Does not exist | | 409 | `workspace has computers` | Remove or move resources first | ```bash curl -X DELETE https://api.miosa.ai/api/v1/workspaces/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update Workspace Settings **`PUT /api/v1/workspaces/{id}/settings`** Applies resource limits and default preferences for the workspace. These limits cap what any computer inside the workspace can do. Tenant-level limits still apply as a ceiling. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `max_computers` | integer | No | Maximum number of computers allowed in this workspace. `null` = no workspace-level cap (tenant limit applies). | | `max_computer_size` | string | No | Maximum `size` value a computer in this workspace may be created with: `"small"`, `"medium"`, or `"large"`. | | `default_computer_size` | string | No | Default `size` applied when creating a computer without an explicit `size`. | | `default_auto_stop_seconds` | integer | No | Auto-stop idle timeout (seconds) applied to new computers by default. `0` = no auto-stop. | | `allowed_template_types` | string[] | No | Allowlist of template names that can be used in this workspace. Empty array = all templates allowed. | | `monthly_spend_cap_cents` | integer | No | Monthly cap, in credits (1 credit = 1 US cent), on hosted sandbox spend in this workspace. `null` clears it. | | `monthly_total_spend_cap_cents` | integer | No | Monthly cap, in credits, on everything settled against this workspace: sandboxes, computers, deployments, databases, storage, snapshots, backups and functions. `null` clears it. | | `spend_cap_action` | string | No | What reaching a cap does: `"pause"` (the default) refuses new resources in the workspace with HTTP 402 and stops what is running there, `"alert"` only sends threshold alerts. | | `entity_spend_caps` | object | No | Monthly caps per `entity` tag, for example `{"customer-a": 5000}`. | Nothing is destroyed and nothing resumes by itself. The setting is named `spend_cap_action`, not `action`. ### Response - `200 OK` ```json { "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "settings": { "max_computers": 10, "max_computer_size": "large", "default_computer_size": "small", "default_auto_stop_seconds": 3600, "allowed_template_types": [] }, "updated_at": "2026-05-17T10:00:00Z" } ``` --- ## Domains Workspaces do not own a preview domain. Preview domains are an organization setting, and per-deployment custom domains are attached to the deployment: - [Custom Domains API](/docs/api-reference/custom-domains/) - the organization preview domain and deployment/computer custom domains. --- ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `FORBIDDEN` | The caller is not an organization owner or admin (`Only workspace admins may update workspace limits`) | | 403 | `forbidden` | Wrong tenant | | 404 | `workspace not found` | Does not exist | | 422 | `VALIDATION_FAILED` | Unknown size value or invalid limit | | 422 | `UNKNOWN_WORKSPACE_SETTING` | A setting name the API does not know. The `message` names the closest accepted key (sending `action` answers `Did you mean "spend_cap_action"?`) and `allowed` lists every accepted setting. | ```bash curl -X PUT https://api.miosa.ai/api/v1/workspaces/{id}/settings \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "max_computers": 10, "max_computer_size": "large", "default_auto_stop_seconds": 3600 }' ``` --- ## List Computers in a Workspace **`GET /api/v1/workspaces/{id}/computers`** ### Response - `200 OK` ```json { "data": [ { "id": "...", "name": "runner-1", "status": "running", "size": "small", "template_type": "miosa-desktop", "created_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/computers \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## List Projects in a Workspace **`GET /api/v1/workspaces/{id}/projects`** Returns the projects owned by a workspace. Use this for client dashboards before listing the sandboxes, computers, deployments, and databases inside a project. ### Response - `200 OK` ```json { "data": [ { "id": "660e8400-e29b-41d4-a716-446655440001", "tenant_id": "...", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "external_workspace_id": "acct_123", "external_project_id": "project_789", "name": "Lead Magnet", "slug": "lead-magnet", "description": null, "metadata": {}, "created_at": "2026-05-18T10:00:00Z", "updated_at": "2026-05-18T10:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/projects \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Workspace Stats **`GET /api/v1/workspaces/{id}/stats`** Returns a snapshot of computer counts grouped by status and the aggregate resource footprint for the workspace. ### Response - `200 OK` ```json { "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "computers": { "total": 5, "by_status": { "running": 3, "stopped": 2, "provisioning": 0, "error": 0 } }, "resources": { "total_vcpus": 7, "total_memory_mb": 28672, "total_disk_gb": 160 }, "settings": { "max_computers": 10, "max_computer_size": "large", "default_auto_stop_seconds": 3600 } } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/stats \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Workspace Usage **`GET /api/v1/workspaces/{id}/usage`** Returns time-series credit consumption and compute-hour totals for the workspace over a given window. Use this for per-workspace cost attribution dashboards. ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `window` | string | `"24h"` (default), `"7d"`, `"30d"` | | `granularity` | string | `"hour"` (default), `"day"` - bucket size for the time-series data | ### Response - `200 OK` ```json { "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "window": "7d", "granularity": "day", "total_credits_used": 1284, "total_compute_hours": 42.5, "series": [ { "ts": "2026-05-10T00:00:00Z", "credits_used": 198, "compute_hours": 6.6 }, { "ts": "2026-05-11T00:00:00Z", "credits_used": 211, "compute_hours": 7.0 } ] } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Wrong tenant | | 404 | `workspace not found` | Does not exist | | 422 | `VALIDATION_FAILED` | Unknown `window` or `granularity` value | ```bash curl "https://api.miosa.ai/api/v1/workspaces/{id}/usage?window=7d&granularity=day" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Common Recipes ### Move all computers to a new workspace Computers do not yet support a `move` operation on workspace assignment via `PATCH /computers/{id}`. Use the `POST /computers/{id}/move` endpoint instead: ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{computer_id}/move \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id": "target-workspace-id"}' ``` ### Enumerate all workspaces and their sizes ```typescript const { data: workspaces } = await client.workspaces.list(); for (const ws of workspaces) { console.log(`${ws.name}: ${ws.computerCount} computers`); } ``` ### Default workspace ID lookup If you need the default workspace ID programmatically: ```typescript const { data } = await client.workspaces.list(); const defaultWs = data.find(ws => ws.isDefault); console.log(defaultWs.id); ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 403 | `forbidden` | Workspace belongs to a different tenant | | 404 | `workspace not found` | Does not exist | | 409 | `workspace has computers` | Cannot delete a workspace that contains computers | | 422 | `VALIDATION_FAILED` | Name or slug failed schema validation | --- ## See also - [Computers API](/docs/api-reference/computers/) - computers are scoped to workspaces - [Projects API](/docs/api-reference/projects/) - projects live inside workspaces - [Error Codes](/docs/api-reference/errors/) - `VALIDATION_FAILED` and general errors --- # Authentication URL: https://miosa.ai/docs/authentication Fallback URL: https://miosa.roberto-c49.workers.dev/docs/authentication Source: src/routes/docs/authentication/+page.md Description: How to authenticate with the MIOSA API using msk_ API keys or JWT tokens. # Authentication Every MIOSA API request carries a single `Authorization: Bearer <token>` header. The token is either an **API key** (prefix `msk_`) or a **JWT** from `POST /auth/login`. One key covers the entire platform - computers, desktop, files, exec, CUA agent, OSA, credits, and (with the right role) admin. No separate keys for AI, no per-product configuration. ## Credential hierarchy Diagram: graph TB MSK[msk_* workspace key] -->|server-side only| API[MIOSA API calls] MSK -->|mints via POST /api/v1/browser-tokens| BT[Browser token - scoped + short-lived] BT -->|browser only| EP[Specific sandbox endpoint] MSK -->|mints via POST /api/v1/auth/sse-ticket| ST[SSE ticket - single use] ST -->|EventSource| SS[SSE stream] MSK -->|mints via POST /api/v1/sandboxes/:id/preview-token| MP[mp_ Preview token] MP -->|iframe embed - no msk_ exposed| Preview[MiosaPreview component] MSK -->|mints via POST /api/v1/sandboxes/:id/shares| MS[ms_ Share token] MS -->|URL param - public read-only| ShareURL[Public share URL] ## API Keys ### Key format ``` msk__ ``` | Prefix | Role | Capabilities | |--------|----------|---------------------------------------------------| | `msk_u_...` | user | Compute, desktop, files, CUA, OSA, credits | | `msk_a_...` | admin | Everything above **+** `/api/v1/admin/*` | | `msk_p_...` | platform | Tenant-wide automation issued by the MIOSA platform | The public self-service endpoint creates normal API-purpose keys. Specialized operator or intelligence credentials use separate controlled flows. ### Creating an API key ```bash curl -X POST https://api.miosa.ai/api/v1/api-keys \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Production SDK Key", "key_type": "user" }' ``` ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "key": "msk_u_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "key_prefix": "msk_u_a1b2c3", "name": "Production SDK Key", "key_type": "user", "key_purpose": "api", "rate_limit_rpm": 600, "status": "active", "created_at": "2026-04-17T00:00:00Z" } } ``` The raw key is **only returned once** at creation. Store it securely. If lost, revoke it and create a new one - the original cannot be recovered. Users can mint personal `user` keys. Tenant owners and administrators can also mint `platform` keys for backend automation. Admin keys remain operator-internal and are not created through the public self-service endpoint. ### Using an API key Pass the key in the `Authorization` header: ```bash curl https://api.miosa.ai/api/v1/computers \ -H "Authorization: Bearer msk_u_a1b2c3d4e5f6..." ``` All five SDKs read `MIOSA_API_KEY` from the environment by default: ```python from miosa import Miosa # Explicit key client = Miosa(api_key="msk_u_...") # Or from environment: export MIOSA_API_KEY="msk_u_..." client = Miosa() ``` ```ts // Explicit key const miosa = new Miosa({ apiKey: 'msk_u_...' }); ``` ```go // Explicit key client := miosa.NewClient("msk_u_...") // Or from environment client := miosa.NewClient(os.Getenv("MIOSA_API_KEY")) ``` ```elixir # Explicit key client = Miosa.client("msk_u_...") # Or from environment client = Miosa.client(System.fetch_env!("MIOSA_API_KEY")) ``` ```java MiosaClient miosa = new MiosaClient("msk_u_..."); ``` ### Managing keys | Action | Endpoint | |--------|----------| | List your keys | `GET /api/v1/api-keys` | | Create a key | `POST /api/v1/api-keys` | | Revoke a key | `DELETE /api/v1/api-keys/{id}` | Revoking a key flips its status to `revoked`. The change is cached and enforced within seconds - subsequent requests return `401 Unauthorized`. Revocation is irreversible; create a new key if you need access restored. ### Admin keys `msk_a_*` keys unlock operator-admin endpoints. `msk_p_*` keys are trusted tenant automation credentials, but they do not automatically make a caller a MIOSA operator. User-role callers hitting an admin endpoint receive `403 Forbidden`. Admin endpoints are operator-internal and not documented publicly. ## Preview tokens Preview tokens (`mp_`) are short-lived signed tokens for embedding a sandbox preview in an ` ``` MIOSA handles pixel streaming and input forwarding inside the iframe. The user clicks or types in the iframe and those events go to the desktop. ## What's streamed - **Pixels** - encoded video tuned for desktop content. - **Cursor position** - overlay rendered client-side. - **Audio** - optional, off by default. Pass `?audio=1` if your app needs it. - **Clipboard** - bidirectional, gated by the iframe's `allow="clipboard-read; clipboard-write"`. ## Read-only mode For "user watches the agent" UI where the end-user shouldn't interact with the desktop: Use your own application permissions to decide whether to render an interactive iframe or a watch-only surface. Public read-only stream token controls are plan-dependent and should not be assumed unless enabled for your tenant. ## Bandwidth Default codec / bitrate is tuned for typical desktop content, usually about 500 Kbps to 2 Mbps. For high-motion content, such as video playback inside the VM, the codec adapts but expect higher bitrate. The stream gateway is geo-routed to the user's nearest region for lower latency. ## Lifetime Stream URLs are short-lived. The default lifetime is 1 hour, and max lifetime is plan-dependent. When a token expires, the stream disconnects. Your frontend should watch `expires_at` and re-mint before that. Multiple stream URLs per computer are fine. Multiple browsers can watch the same desktop simultaneously. They all see the same pixels. ## Custom domains / branded streaming White-label customers can configure a branded stream domain so end users see your brand in the iframe URL. The DNS and TLS flow follows the same contract as [Deployment Domains](/docs/docker-host/domains/), with a different routing target. Contact support to set up branded streaming. ## White-label desktops Computers support per-tenant desktop branding. After starting a computer, push a wallpaper and apply it with two SDK calls. The wallpaper persists across stop/start cycles because it is stored in the VM's snapshotted filesystem. ```python client = miosa.Miosa(api_key=os.environ["MIOSA_API_KEY"]) computer = client.computers.create( name="acme-agent", template_type="miosa-desktop", size="small", external_workspace_id="acme-corp", ) computer.start() # Push the tenant wallpaper from your server's filesystem computer.write_file("/tmp/acme-wallpaper.png", open("assets/acme-bg.png", "rb").read()) computer.set_wallpaper("/tmp/acme-wallpaper.png") # Optionally verify the DE de = computer.get_desktop_environment() print(de) # {"name": "xfce4", "version": "4.18"} ``` ```typescript const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }) const computer = await miosa.computers.create({ name: "acme-agent", template_type: "miosa-desktop", size: "small", externalWorkspaceId: "acme-corp", }) await computer.start() // Push and apply the tenant wallpaper const bg = fs.readFileSync("assets/acme-bg.png") await computer.writeFile("/tmp/acme-wallpaper.png", bg) await computer.setWallpaper("/tmp/acme-wallpaper.png") ``` For production deployments, bake the wallpaper directly into a custom template snapshot so every new computer boots with the correct branding. No post-boot setup is required. See the Templates documentation for details. ## CSP The stream gateway emits `Content-Security-Policy` allowing the stream to be embedded from approved origins: - Default: `*.miosa.app`, `*.miosa.ai`, `localhost:4000`. - For white-label: add your platform origin via tenant config. If your iframe doesn't render, check the parent page's CSP isn't blocking the iframe and that the stream gateway CSP includes your origin. ## Audit Each stream URL issuance emits an audit event with: - The computer ID - The token prefix - Issuing API key - External attribution If a token is leaked, rotate the user session or stop the Computer while you investigate. Stream tokens are short-lived by default. ## See also - [Overview](/docs/computers/overview/) - what you're embedding - [Computer use](/docs/computer-use/) - programmatic control of the same desktop - [Browser Tokens](/docs/platform/browser-tokens/) - analogous pattern for sandbox previews --- # Fast start & readiness URL: https://miosa.ai/docs/computers/fast-start Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/fast-start Source: src/routes/docs/computers/fast-start/+page.md Description: How MIOSA starts managed desktop Computers quickly, defines browser readiness, and exposes failures without false Live states. # Fast start & readiness MIOSA Computers use ready capacity on the managed fleet so a new desktop can be assigned without waiting for a full cold boot. The product target is simple: click **Launch**, then receive an interactive desktop in about 800 to 900 ms on the warm path. ## Verified result The production canary measured on 2026-08-06 completed **10 of 10** launches and reached a browser-ready desktop in a median **740 ms**. | Phase | Time | |---|---:| | Successful launches | `10 / 10` | | Create response median | `348 ms` | | Create request to browser-ready desktop median | **`740 ms`** | | Create request to browser-ready desktop p90 | **`809 ms`** | | Slowest sample | `942 ms` | Every canary used the warm path and did not require the restart fallback. The browser used the bundled direct noVNC client, and `Live` required its real RFB connection event. The launch path warmed the future viewer transport concurrently with the create request and retained bounded retry for route-registration races. The 10-sample result is a dated production canary, not a universal latency guarantee. Region, fleet capacity, network latency, image choice, and cold-path fallback can change startup time. ## What browser-ready means MIOSA does not call a Computer ready merely because its database record exists or its lifecycle says `Running`. Browser readiness requires the complete viewing path to work. 1. The create request claims ready desktop capacity. 2. The stream API returns a short-lived authenticated viewer URL. 3. The bundled viewer client connects directly to the authenticated VNC WebSocket. 4. The VNC WebSocket upgrades with the stream token. 5. The browser renders a usable desktop frame. 6. Opening the Computer workspace keeps the desktop mounted and connects its terminal. The managed fleet keeps separate reserves on every host: 20 ready desktop slots and 20 ready sandbox slots. Desktop slots enter the ready pool only after the default MIOSA wallpaper is visible. Tenant wallpaper and desktop branding are applied during claim, before the Computer is handed to the user. A browser error document or failed iframe navigation must not be labeled as a live desktop. The stream URL is short-lived and scoped to the selected Computer. The gallery-to-workspace transition preserves the viewer handoff instead of requesting a conflicting session. ## Application integration Request the Computer stream URL from your backend and pass only the short-lived URL to the browser. Embed the authenticated viewer URL returned by the API instead of rebuilding the desktop URL from the Computer slug. Your UI should represent these states separately: | State | Meaning | Recommended UI | |---|---|---| | `provisioning` | Capacity is being assigned or the guest is becoming ready | Show bounded progress | | `running` with viewer ready | Desktop and stream path are usable | Show `Live` and reveal the viewer | | `paused` | Durable state exists but the active desktop is stopped | Offer Resume | | viewer unavailable | Lifecycle and viewer reachability disagree | Show an actionable error and retry control | | failed | Provisioning or startup failed | Show the backend error and recovery action | Do not promote an iframe to `Live` from its generic `load` event alone. Browsers also fire that event for their own network-error documents. ## Troubleshooting ### The card says Running but the viewer says Host unavailable Treat viewer reachability as authoritative for the user-visible state. Refresh the Computer lifecycle, then retry the stream request. If the mismatch persists, contact support with the Computer ID, region, and approximate creation time. ### The viewer page returns 503 The public viewer gateway cannot reach the active compute proxy or assigned host. This is a platform routing failure rather than proof that your files or Computer record were lost. Retry once, then contact support if it persists. ### The stream request returns 409 The Computer is not currently in a streamable lifecycle state. Refresh its status and resume it when the API reports `paused`. Do not retry indefinitely without re-reading the lifecycle. ### The iframe loads but no desktop appears Confirm that you embedded the authenticated viewer URL returned by the API. Do not substitute a raw desktop entry URL or wait for an undocumented cross-frame event. ## Related documentation - [Computers overview](/docs/computers/overview/) - [Embedding & Streaming](/docs/computers/embedding/) - [Computer use](/docs/computer-use/) - [Computers API](/docs/api-reference/computers/) --- # Lifecycle and sizing URL: https://miosa.ai/docs/computers/lifecycle Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/lifecycle Source: src/routes/docs/computers/lifecycle/+page.md Description: Computer fast startup, the desktop state machine, resource sizes, and workspace-scoped creation for per-tenant attribution. # Lifecycle and sizing A Computer is a long-lived machine. This page covers how fast it starts, the states it moves through, the sizes it runs on, and how to scope it to a workspace. ## Fast startup MIOSA keeps ready desktop capacity on the managed fleet so a new Computer can be assigned instead of cold-booted from scratch. The production acceptance test measures the complete user-visible path from creation to the first usable desktop frame. The verified 2026-08-06 canary completed **10 of 10** warm launches and reached a browser-ready desktop in a median **740 ms**: | Phase | Time | |---|---:| | Create response median | `348 ms` | | Create request to browser-ready desktop median | **`740 ms`** | | Create request to browser-ready desktop p90 | **`809 ms`** | | Slowest sample | `942 ms` | This is a dated warm-path canary, not a guarantee that every region and capacity condition will complete inside the sampled range. See [Fast start & readiness](/docs/computers/fast-start/) for the readiness contract, measurement boundary, and troubleshooting guidance. ## Lifecycle | State | Description | |---|---| | `provisioning` | The VM is being claimed, restored, and prepared | | `running` | Desktop is up, streamable, and accepting API actions | | `stopped` | Compute is stopped and filesystem state is preserved | | `recovering` | MIOSA is restoring a desired-running Computer after host maintenance | | `error` | Recovery did not complete; the preserved disk or snapshot remains available for retry | | `deleted` | Permanent deletion; not reversible | A stopped computer is not billed for compute; its disk is billed at $0.018 per GiB-month while it is stopped. Restarting resumes from the snapshot. During host maintenance, MIOSA preserves Computer identity, disk state, snapshots, metadata, and desired state. Computers that were running are restored and verified before the host returns to active service. Intentionally stopped Computers remain stopped. If a local resume fails, MIOSA preserves the disk and snapshot instead of replacing the Computer with a blank machine. Use `delete` only when the state should be permanently removed. ## Sizing | Size | vCPU | RAM | Disk | Typical use | |---|---|---|---|---| | `xs` | 1 | 2 GB | 10 GB | Lightweight headless tasks | | `small` | 2 | 4 GB | 20 GB | **Default** for a single agent and basic browsing | | `medium` | 4 | 8 GB | 50 GB | Heavier desktop apps, faster page loads | | `large` | 8 | 16 GB | 100 GB | Multi-app, large-context AI workloads | | `xl` | 16 | 32 GB | 200 GB | Intensive parallelism, large model weights | GPU is not productized: the public API does not currently schedule GPU-backed desktops. Computers ship with the OSA agent installed but disabled by default. Activate it to give your computer a desktop AI assistant that can run multi-step desktop tasks autonomously. OSA is controlled through documented MIOSA APIs. ## Workspace-scoped creation Computers can be created inside a named workspace so that resources are isolated and billed separately. Pass `external_workspace_id` to attribute the computer to a tenant in your platform. ```python client = miosa.Miosa(api_key=os.environ["MIOSA_API_KEY"]) computer = client.computers.create( name="agent-1", template_type="miosa-desktop", size="small", external_workspace_id="acme-corp", # your tenant identifier metadata={"agent_run": "run-2026-05-17"}, ) computer.start() ``` ```typescript const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }) const computer = await miosa.computers.create({ name: "agent-1", template_type: "miosa-desktop", size: "small", externalWorkspaceId: "acme-corp", metadata: { agentRun: "run-2026-05-17" }, }) await computer.start() ``` `external_workspace_id` is a free-form string you control. Use your own tenant, org, customer, or workspace identifier. Usage is tracked per workspace in the billing dashboard so you can attribute compute costs to individual customers. --- # Computers (Desktop) URL: https://miosa.ai/docs/computers/overview Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/overview Source: src/routes/docs/computers/overview/+page.md Description: Durable cloud desktops for browser agents, human takeover, GUI automation, and long-running computer-use workflows. # Computers A **Computer** is a durable cloud desktop. It gives each user, agent, or workflow a real Linux GUI with browser state, files, shell access, desktop streaming, and programmatic control. Use it when the work depends on pixels, sessions, browser logins, desktop apps, accessibility trees, or a human taking over the same machine an agent was using. For pure code execution, builds, and preview servers, [Sandboxes](/docs/develop/sandboxes/) are cheaper and faster. For a 24/7 production web app, use [Docker Host](/docs/docker-host/docker-deploy/). Still deciding? See [Computers vs sandbox](/docs/computers/vs-sandbox/). ## 1. Create a Computer ```bash miosa computer create --size small miosa computer list ``` ```typescript const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }) const computer = await miosa.computers.create({ name: "agent-1", template_type: "miosa-desktop", size: "small", }) await computer.start() ``` ```python client = miosa.Miosa(api_key=os.environ["MIOSA_API_KEY"]) computer = client.computers.create( name="agent-1", template_type="miosa-desktop", size="small", ) computer.start() ``` Pass `external_workspace_id` to attribute the Computer to one of your tenants. See [Lifecycle and sizing](/docs/computers/lifecycle/) for states, sizes, and workspace scoping. ## 2. Drive the desktop ```bash miosa api GET /computers/cmp_123/desktop/screenshot > screen.png ``` ```typescript const png = await computer.screenshot() await computer.leftClick(640, 480) await computer.type("hello world") await computer.key("Return") await computer.bash("firefox &") ``` ```python png_bytes = computer.screenshot() computer.left_click(640, 480) computer.type("hello world") computer.key("Return") computer.bash("firefox &") ``` Start each loop with an observation, act once, then observe again. The full action set is in [Computer use](/docs/computer-use/). ## 3. Watch it live A Computer is also a live pixel stream. Open it from the MIOSA console, or embed the same stream in your app with a short-lived URL minted on your backend. ```typescript const urls = await computer.urls() console.log(urls.desktop_url) ``` See [Embedding and streaming](/docs/computers/embedding/) for the token and iframe flow. ## Pick what you need | You want to | Start here | |---|---| | Create, size, and manage the machine | [Lifecycle and sizing](/docs/computers/lifecycle/) | | Choose a Computer or a Sandbox | [Computers vs sandbox](/docs/computers/vs-sandbox/) | | See what ships in the box | [Inside a Computer](/docs/computers/anatomy/) | | Drive the desktop from code | [Computer use](/docs/computer-use/) | | Embed the live desktop in your UI | [Embedding and streaming](/docs/computers/embedding/) | | Ship it safely | [Production checklist](/docs/computers/production-checklist/) | | Measure startup | [Fast start and readiness](/docs/computers/fast-start/) | ## Next Pick the right machine for the workload, with the rule of thumb. What ships in the box and how the desktop, disk, stream, and API fit together. Fast startup, the state machine, resource sizes, and workspace-scoped creation. Access model, the operator workflow, and embedding the live desktop in your UI. The defaults to verify before you rely on a Computer in production. Full action reference: screenshot, click, type, key, scroll, drag, windows, accessibility tree. --- # Production checklist URL: https://miosa.ai/docs/computers/production-checklist Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/production-checklist Source: src/routes/docs/computers/production-checklist/+page.md Description: The defaults to verify before relying on a Computer in production: workspace attribution, access, secrets, state, observability, and recovery. # Production checklist Before you rely on a Computer in production, verify these points. | Area | Check | |---|---| | Workspace attribution | Set `external_workspace_id`, `external_user_id`, or workspace/project fields when building a white-label app | | Access | Use platform auth or stream tickets for normal users | | Secrets | Put API keys in backend code or encrypted env, not inside frontend bundles | | State | Use stop/start for durable sessions and delete only when state should be removed | | Observability | Log the Computer ID, external IDs, and task/run ID together | | Recovery | Design the app so a user can reopen the same running Computer after refreshing the browser | [Lifecycle and sizing](/docs/computers/lifecycle/) covers the states and sizes these checks assume, and [Embedding & streaming](/docs/computers/embedding/) covers the access model and stream tickets. --- # Computers vs sandbox URL: https://miosa.ai/docs/computers/vs-sandbox Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/vs-sandbox Source: src/routes/docs/computers/vs-sandbox/+page.md Description: Choose a Computer or a Sandbox for a workload, with the rule of thumb and a side-by-side table. # Computers vs sandbox If the user or agent needs to see and operate a real screen, use a Computer. If the workload only needs a shell, filesystem, process, or preview port, use a Sandbox. ## Pick by workload | Workload | Pick | Why | |---|---|---| | Run tests, install packages, build code, expose a dev server | **Sandbox** | Faster and cheaper shell-first compute | | Computer-use agent that must click through a website | **Computer** | Needs pixels, browser state, and input events | | Logged-in browser session that must survive across tasks | **Computer** | Browser profile persists with the VM | | Headless Playwright, API tests, CLI workflows | **Sandbox** | No visible desktop needed | | Screenshot-based QA or visual inspection | **Computer** | Uses the rendered screen as truth | | Desktop apps, extensions, file pickers, or manual takeover | **Computer** | Needs a full GUI and streamable session | | Public 24/7 app URL for customers | **Docker Host** | Built for durable deployments and custom domains | ## What a Computer is for | Use | What it gives you | |---|---| | Agent browsers | Run a browser session that stays logged in, exposes pixels to agents, and can be resumed by a human. | | Human handoff | Let an operator open the same live desktop, inspect the state, and continue from where automation stopped. | | GUI automation | Drive desktop apps with mouse, keyboard, clipboard, windows, accessibility, and shell commands. | | Persistent workspaces | Keep files, browser profiles, installed tools, and local state across stop/start cycles. | ## See also Full action reference: screenshot, click, type, key, scroll, drag, hotkey, windows, accessibility tree, and more. [Open](/docs/computer-use/) Embed the live pixel stream in your own UI using short-lived stream tokens. [Open](/docs/computers/embedding/) Bring your own hardware (Mac, Linux, Windows). One command registers your machine as a MIOSA computer. [Open](/docs/opencomputers/) --- # Core Concepts URL: https://miosa.ai/docs/concepts Fallback URL: https://miosa.roberto-c49.workers.dev/docs/concepts Source: src/routes/docs/concepts/+page.md Description: The canonical MIOSA product model, from mutable sandboxes to immutable releases, stable deployments, persistent data, and customer-owned infrastructure. # Core Concepts MIOSA gives developers and agents a safe development loop without making production mutable. You work in a Sandbox, inspect the result through a Preview, and publish a frozen candidate Release. After MIOSA verifies that candidate, promotion moves the Deployment's stable URL to the new Version. If you read only one line, remember this: **Sandbox (mutable) → Preview → Publish → Release (immutable) → Verify → Promote → Deployment (stable URL).** The old Version remains available for rollback. Databases, storage, secrets, and domains remain attached to the Deployment instead of being copied into each release. Changing a Sandbox changes development state. It does not change production. Production changes only when an exact immutable candidate is promoted. This page is the **resource model**: what the MIOSA resources are and how they relate. For the **API protocol** - authentication, token types, idempotency, rate limits, errors, and events - see [API fundamentals](/docs/fundamentals/). ## Pick what you need | You want to | Read | |---|---| | The ownership chain and the development and production resources | [Resource model](/docs/concepts/resource-model/) | | How a publish becomes a live release | [The publish pipeline](/docs/concepts/publish-pipeline/) | | Databases, storage, and bindings | [Data and bindings](/docs/concepts/data-and-bindings/) | | The desktop product | [Computers](/docs/concepts/computers-desktop/) | | White-label tenancy and attribution | [Tenancy](/docs/concepts/tenancy/) | | Browser tokens for previews and shares | [Preview and share tokens](/docs/concepts/tokens/) | | Tenant preview domain and branding | [Tenant configuration](/docs/concepts/tenant-config/) | New to the platform vocabulary? Keep the [MIOSA Glossary](/docs/glossary/) open while you follow the resource and publishing flows below. ## Next The ownership chain, Sandboxes, and Previews - the mutable side of the platform. Builder, Release, Runtime Instance, Domain, Deployment, and Version: how source becomes a live release. Postgres, Redis, object storage, volumes, auth, and how they attach to a Deployment. The cloud desktop product, and the difference between a Computer and a Sandbox. White-label tenancy and external attribution for platforms built on MIOSA. Preview tokens and share tokens for browser access to a sandbox preview. Tenant preview domain and tenant branding. ## Deep dives **Computers (desktop product)** now lives in [Computers](/docs/concepts/computers-desktop/). **Tenant preview domain** now lives in [Tenant configuration](/docs/concepts/tenant-config/). --- # Computers (desktop product) URL: https://miosa.ai/docs/concepts/computers-desktop Fallback URL: https://miosa.roberto-c49.workers.dev/docs/concepts/computers-desktop Source: src/routes/docs/concepts/computers-desktop/+page.md Description: The MIOSA cloud desktop product: full GUI environments with screenshot, click, type, and keyboard APIs, and how they differ from Sandboxes. # Computers (desktop product) **Computers** are full desktop environments running in the cloud with screenshot, click, type, and keyboard APIs. They are a separate product surface from Sandboxes - different lifecycle, different use cases. | | Sandbox | Computer | |---|---|---| | Primary use | Code execution, file I/O, dev servers, agent tool-use | Desktop GUI, browser automation, computer-use agents | | Display | None (headless) | Desktop stream | | Accessibility tree | No | Yes | | Lifecycle | Short-to-medium lived, auto-suspend | Session-oriented | | Template | `miosa-sandbox` (the default) | `miosa-desktop` | Use Sandboxes for building apps. Use Computers when your agent or workflow needs a real screen. You can also bring your own hardware (Mac, Linux, Windows) and register it as a Computer host via BYOC. ```bash curl -X POST https://api.miosa.ai/api/v1/computers \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"my-computer","template_type":"miosa-desktop","size":"small"}' ``` [Computers overview →](/docs/computers/overview/) ## Three naming layers One thing trips up every new reader: "Computer" has a specific meaning in MIOSA. | Layer | Term | Meaning | |-------|------|---------| | Brand | **MIOSA** | The wordmark and platform. | | Resource | **Computer** | The desktop GUI product. Not a generic synonym for "VM." | | AI agent | **OSA agent** | The agent runtime. Optional; installable inside a Sandbox or Computer. | Sandboxes are not Computers. Runtime Instances are not Computers. Computers are Computers. When you encounter older docs or code that uses "Computer" generically, substitute "Sandbox" or "Runtime Instance" based on context. ## Next - [Resource model](/docs/concepts/resource-model/): what a Sandbox is. - [Computers overview](/docs/computers/overview/): the full Computers reference. - [Core Concepts](/docs/concepts/): the one-screen overview. --- # Data and bindings URL: https://miosa.ai/docs/concepts/data-and-bindings Fallback URL: https://miosa.roberto-c49.workers.dev/docs/concepts/data-and-bindings Source: src/routes/docs/concepts/data-and-bindings/+page.md Description: Managed Postgres, Redis, object storage, volumes, and auth - the durable resources a Deployment attaches to, and how bindings inject credentials. # Data and bindings Sandboxes and Runtime Instances are replaceable. Data Services are durable resources with an independent lifecycle. Replacing a Sandbox, publishing a Release, or rolling back a Version does not delete the Deployment's database or object storage. | Service | What it is | |---------|-----------| | Managed Postgres | Fully managed PostgreSQL database. Schema migrations are yours to run; MIOSA provides the connection string. | | Managed Redis | Persistent Redis cluster for caching, queues, pub/sub. | | Object Storage | S3-compatible bucket. Useful for uploads, assets, generated files. | | Volumes | Block storage mounted into Runtime Instances. Persists across restarts. | | Auth | JWT-based signup/login as a service. One env var gives your project a full auth system. | Bindings connect a Sandbox or Deployment to those durable resources. MIOSA resolves the binding and injects the appropriate credentials at runtime. ``` DATABASE_URL=postgresql://user:pass@host/dbname REDIS_URL=redis://host:6379 STORAGE_BUCKET=miosa-ws123-uploads AUTH_URL=https://auth.miosa.app/proj_abc AUTH_JWT_SECRET=... ``` Destroying a Sandbox does not destroy its database. Publishing a Version does not automatically make an incompatible schema safe. Use backward-compatible expand, migrate, and contract changes so the active and previous Versions can both operate during promotion or rollback. Managed Postgres and Auth are part of the platform Phase 6-8 rollout. The doc surface is published now so you can plan integration; some endpoints are still being wired in production. ```bash miosa api POST /databases -d '{"name":"app-production","engine":"postgresql"}' ``` [Data overview →](/docs/data/overview/) ## Next - [The publish pipeline](/docs/concepts/publish-pipeline/): how a Release attaches to these resources. - [Data overview](/docs/data/overview/): the full data-services reference. - [Core Concepts](/docs/concepts/): the one-screen overview. --- # The publish pipeline URL: https://miosa.ai/docs/concepts/publish-pipeline Fallback URL: https://miosa.roberto-c49.workers.dev/docs/concepts/publish-pipeline Source: src/routes/docs/concepts/publish-pipeline/+page.md Description: Builder, Release, Runtime Instance, Domain, Deployment, and Version - how a mutable Sandbox becomes an immutable, verified production release. # The publish pipeline This page is the **production side** of MIOSA: the resources a publish creates and how they relate. For the ownership chain and the mutable Sandbox, see [Resource model](/docs/concepts/resource-model/). ## Production resources ### Builder A **Builder** is the isolated worker for one publish operation. It freezes the Sandbox source, runs the selected build pipeline, stores the result, and exits. Later edits to the Sandbox cannot change that build. Three build modes: | Mode | Trigger | Output | |------|---------|--------| | Static packager | `/workspace` has only static files | Tarball served from MIOSA's edge | | Auto-detect | Node/Python/Ruby/Go project detected | Dynamic release served by production runtimes | | Dockerfile | `Dockerfile` present in `/workspace` | Dynamic release served by production runtimes | The Builder is internal and short-lived. Its job is to make production builds reproducible while the mutable Sandbox remains available for continued work. ### Release A **Release** is the immutable artifact produced by a successful Builder run. Its content is addressed by a SHA-256 digest and stored outside the Sandbox. The same bytes can be started repeatedly, placed on different hosts, and verified independently. MIOSA never edits a Release in place. Publishing changed source creates a new Release. That rule prevents a URL from silently serving bytes that do not match its release record. Two shapes: - **Static release** - a tarball of files (`index.html`, `app.js`, assets). MIOSA's edge serves them directly. No Runtime Instances required. - **Dynamic release** - a server app artifact. Served by production runtimes. [Releases reference →](/docs/docker-host/releases/) ### Runtime Instance A **Runtime Instance** runs a dynamic Release in production. MIOSA's scheduler places it, the health system checks it, and the reconciler replaces it when the actual runtime no longer matches the desired state. Key properties: - **Placement.** The scheduler selects a compatible host using capacity, region, and policy. - **Scaling.** Multiple instances can serve the same Version. - **Recovery.** Failed instances can be replaced from the immutable Release. - **Isolation.** Production runtimes are isolated from other tenants and from the build Sandbox. Static deployments do not need Runtime Instances. MIOSA serves their immutable files from the edge. [Runtime Instances reference →](/docs/docker-host/runtime-instances/) ### Domain A **Domain** is a hostname routed to a Deployment's active Version. The Deployment owns the route, so a new Version does not require a new customer-facing URL. - `my-app.my-workspace.miosa.app` - MIOSA-managed fallback URL, instant, no DNS change needed. - `my-app.apps.yourplatform.com` - tenant deployment domain for generated apps. - `app.yourcustomer.com` - custom domain your customer brings; MIOSA auto-provisions a TLS certificate via Let's Encrypt. Changing a route does not rebuild an application. Promotion and rollback change which verified Version receives traffic behind the same Deployment URL. [Domains reference →](/docs/docker-host/domains/) ## The immutable publish pipeline A **Deployment** is the durable identity of a production app. It owns the stable URL, version history, routes, environment configuration, and data bindings. A **Version** connects one Deployment to one immutable Release plus the configuration needed to run it. Publishing creates a candidate Version. It does not make that candidate live merely because the build finished. Promotion must prove that the candidate is ready, its required configuration is present, its runtime is healthy, and its route serves the expected artifact. Only then does MIOSA update `active_version_id`. Diagram: flowchart LR S["Sandbox
mutable"] -->|publish| B["Builder
isolated job"] B -->|produces| R["Release
immutable bytes"] R -->|creates| V["Candidate Version"] V -->|verify| G{"Promotion gates pass?"} G -->|yes| D["Deployment
stable identity"] G -->|no| F["Candidate remains inactive"] Dom["Domain"] -->|routes to| D D -->|runs| RI["Runtime Instances"] The production transition is intentionally narrow: 1. Freeze the exact Sandbox source. 2. Build an immutable Release. 3. Create a candidate Version. 4. Verify health, routes, secrets, data bindings, and declared requirements. 5. Promote that exact Version. 6. Prove the public URL serves it. 7. Keep the previous Version available for rollback. The object relationship remains stable across releases: Diagram: graph TD Proj["Project"] Dep["Deployment
team-portal"] V17["Version 17
(active)"] V16["Version 16
(archived)"] V15["Version 15
(archived)"] Rel["Release sha:abc"] RI["Runtime Instances"] D1["team-portal.miosa.app"] D2["example.net"] Proj --> Dep Dep -->|active_version_id| V17 Dep --> V16 Dep --> V15 V17 --> Rel Rel --> RI Dep --> D1 Dep --> D2 Rollback selects an earlier ready Version as `active_version_id`. The Deployment and its domains do not change identity. For dynamic applications, MIOSA routes traffic to healthy instances of the selected Version and retires instances that no longer belong to the active state. A production update is accepted only when the release record, running artifact, public route, required bindings, and declared capabilities agree. A successful build by itself is only a candidate. ```bash miosa deploy create --from-sandbox \ --name my-app \ --dir /workspace \ --type docker_deploy \ --wait \ --json ``` Builder, Release, Runtime Instance, Domains - the full publish pipeline. [Deploy overview →](/docs/docker-host/overview/) ## Next - [Data and bindings](/docs/concepts/data-and-bindings/): the durable resources a Deployment attaches to. - [Deploy overview](/docs/docker-host/overview/): the full pipeline in the deploy section. - [Core Concepts](/docs/concepts/): the one-screen overview. --- # Resource model URL: https://miosa.ai/docs/concepts/resource-model Fallback URL: https://miosa.roberto-c49.workers.dev/docs/concepts/resource-model Source: src/routes/docs/concepts/resource-model/+page.md Description: The MIOSA ownership chain and the mutable development resources - Organization, Workspace, Project, Sandbox, and Preview. # Resource model This page is the **resource model** of the mutable side of MIOSA: the ownership chain and the development resources. For the production resources and the publish pipeline, see [The publish pipeline](/docs/concepts/publish-pipeline/). ## The resource hierarchy MIOSA resolves every operation through a clear ownership chain: ```text Organization → Workspace → Project → Sandbox or Deployment ``` - An **Organization** is the top-level customer or platform account. - A **Workspace** isolates a team, customer, or business boundary. - A **Project** groups the development and production resources for one product. - A **Sandbox** is the mutable place where work happens. - A **Deployment** is the durable production identity that owns versions, routes, and bindings. Every mutation must resolve one unambiguous organization, workspace, project, and target resource. This prevents an agent or CLI command from publishing into the wrong customer or environment. ## Development resources ### Sandbox A **Sandbox** is a mutable, isolated environment where an agent or developer builds an app. It includes a complete development environment with Node, Python, git, package managers, file APIs, and a terminal. Key properties: - **Mutable and stateful.** Files in `/workspace` persist across commands until the sandbox is destroyed or suspended. - **Executable.** Run commands such as `npm install`, `python app.py`, or `git clone`, with structured stdout, stderr, and exit status. - **Agent-friendly file access.** Read, write, and synchronize source, configuration, and assets. - **Previewable.** Expose a running port through a temporary Preview URL. - **Snapshot-aware.** Save, fork, and restore development state without treating that snapshot as a production release. - **Suspendable.** Idle compute can pause and later resume from durable state. A Sandbox is not production. Its files can change at any time, its process can stop, and its Preview can disappear while suspended. Use it to create and test the source that will become a production candidate. ```bash miosa create my-box --wait # provision a sandbox and wait until it is ready miosa exec my-box -- echo "hello from MIOSA" ``` Deep-dive on Sandboxes, file I/O, exec, previews, and the dev loop. [Sandboxes reference →](/docs/develop/sandboxes/) ### Preview A **Preview** is a temporary public URL that tunnels to one port inside a running Sandbox. ``` https://5173-abc12345.sandbox.miosa.ai ``` When you create a Preview, you specify the port, usually `3000` or `5173`. MIOSA routes that hostname to the Sandbox process. As files change and the development server reloads, the Preview changes too. Key properties: - **Mutable.** Every request reflects the current Sandbox process and files. - **Ephemeral.** The URL depends on a running Sandbox and is not a durable customer URL. - **Shareable.** Short-lived tokens allow browser access without exposing a workspace API key. ```bash miosa preview create my-box 3000 --visibility public ``` [Previews reference →](/docs/develop/previews/) ## Next - [The publish pipeline](/docs/concepts/publish-pipeline/): Builder, Release, Runtime Instance, Domain, Deployment, and Version. - [Data and bindings](/docs/concepts/data-and-bindings/): durable resources that outlive a Sandbox. - [Core Concepts](/docs/concepts/): the one-screen overview. --- # Tenancy URL: https://miosa.ai/docs/concepts/tenancy Fallback URL: https://miosa.roberto-c49.workers.dev/docs/concepts/tenancy Source: src/routes/docs/concepts/tenancy/+page.md Description: White-label tenancy and external attribution - how platforms build a product on MIOSA and map MIOSA resources to their own customers. # Tenancy MIOSA is designed for platforms - you build a product on top of MIOSA, and your customers never know MIOSA exists. The tenancy model: - **One MIOSA organization per customer platform.** A platform account has one organization slug and server-side `msk_*` keys. - **Workspaces and projects for downstream customers.** Use MIOSA workspaces for clients and projects for the apps, sites, documents, and workflows they create. - **External attribution for your IDs.** Pass `external_workspace_id`, `external_user_id`, and `external_project_id` when you need your database IDs on usage, audit, and list filters. - **Server-side key, never in the browser.** For browser-side access (embedding a preview, opening a terminal), your backend mints a short-lived scoped token. The `msk_*` key stays server-side. Diagram: graph TD Plat["Your Platform"] Org["MIOSA Organization
one slug · one bill"] WS["Workspace
Acme Corp"] Proj["Project
Lead Magnet"] Sbx["Sandbox"] Dep["Deployment"] Plat --> Org Org --> WS WS --> Proj Proj --> Sbx Proj --> Dep Filters are always organization-scoped server-side. You cannot cross organizations by passing different workspace, project, or external IDs. White-label tenancy, browser tokens, attribution, and API keys. [Platform / White-label overview →](/docs/platform/overview/) ## External attribution **External attribution** is how you map MIOSA resources to your own users, projects, and tenants without creating MIOSA accounts for them. Three opaque string fields are accepted on every resource that supports creation: | Field | Purpose | |-------|---------| | `external_user_id` | The end-user inside your system who owns or triggered this resource | | `external_project_id` | The project, document, or workflow in your database that this resource belongs to | | `metadata` | Arbitrary key-value map for any additional context your platform needs | MIOSA never interprets or validates the content of external attribution fields. They are always stored as-is. ```python sandbox = client.sandboxes.create( template_id="miosa-sandbox", external_workspace_id="acme-co", external_user_id="alice@example.net", external_project_id="invoice-bot", ) ``` For the request fields, list filters, and per-customer chargeback rollup, see [Ownership and attribution](/docs/fundamentals/attribution/) and the [Attribution reference](/docs/platform/attribution/). ## Next - [Preview and share tokens](/docs/concepts/tokens/): browser access without exposing a workspace key. - [Attribution reference](/docs/platform/attribution/): the full filter spec and billing split. - [Core Concepts](/docs/concepts/): the one-screen overview. --- # Tenant configuration URL: https://miosa.ai/docs/concepts/tenant-config Fallback URL: https://miosa.roberto-c49.workers.dev/docs/concepts/tenant-config Source: src/routes/docs/concepts/tenant-config/+page.md Description: Tenant preview domain and tenant branding - the apex hostname that replaces the MIOSA fallback preview domain and the error pages your end users see. # Tenant configuration ## Tenant preview domain A **tenant preview domain** is an apex hostname that replaces the MIOSA fallback preview domain in every sandbox preview URL your end users see. It is set per tenant. When you set and verify a preview domain (for example, `yourproduct.com`), sandbox previews for your tenant are served at: ``` https://.yourproduct.com ``` Setup is a two-step DNS verification: add a wildcard CNAME (`*.yourproduct.com`) pointing to `proxy.miosa.ai`, then call `POST /api/v1/tenant/preview-domain/verify`. MIOSA auto-provisions a wildcard TLS certificate once the CNAME resolves. Only apex domains are accepted - subdomains and protocol prefixes are rejected. One preview domain per tenant. The domain applies to all sandboxes under that tenant. ```bash # Set curl -X PUT https://api.miosa.ai/api/v1/tenant/preview-domain \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{"preview_domain": "yourproduct.com"}' # Verify curl -X POST https://api.miosa.ai/api/v1/tenant/preview-domain/verify \ -H "Authorization: Bearer msk_u_..." ``` [Tenant Preview Domain API →](/docs/api-reference/tenant-preview-domain/) ## Tenant branding **Tenant branding** controls what your end users see on MIOSA-rendered error pages - the 404, 502, and gateway-timeout pages displayed when a sandbox is unavailable, a preview URL is invalid, or a custom domain is not yet verified. Configurable fields: | Field | Description | |-------|-------------| | `logo_url` | HTTPS URL to your logo image | | `name` | Product or company name | | `support_url` | Optional link shown on error pages for end-user support | | `primary_color` | Hex color for CTA buttons (for example `#0066FF`) | Branding is set via `PUT /api/v1/tenant/branding` and served immediately from cache - there is no propagation delay. Deleting it resets the error pages to the MIOSA defaults. ```bash # Set branding curl -X PUT https://api.miosa.ai/api/v1/tenant/branding \ -H "Authorization: Bearer msk_u_..." \ -H "Content-Type: application/json" \ -d '{ "branding": { "logo_url": "https://cdn.example.com/logo.png", "name": "Lumen", "primary_color": "#0066FF" } }' ``` [Tenant Branding API →](/docs/api-reference/tenant-branding/) ## Next - [Preview and share tokens](/docs/concepts/tokens/): browser access to a sandbox preview. - [Tenant Preview Domain API](/docs/api-reference/tenant-preview-domain/): the full request schema. - [Core Concepts](/docs/concepts/): the one-screen overview. --- # Preview and share tokens URL: https://miosa.ai/docs/concepts/tokens Fallback URL: https://miosa.roberto-c49.workers.dev/docs/concepts/tokens Source: src/routes/docs/concepts/tokens/+page.md Description: Short-lived preview tokens (mp_*) and revocable share tokens (ms_*) for loading a sandbox preview in a browser without exposing a workspace key. # Preview and share tokens ## Preview tokens A **preview token** (prefix `mp_*`) is a short-lived signed token that authorizes a browser to load a sandbox preview without receiving the workspace API key. The typical flow: 1. Your backend calls `POST /api/v1/sandboxes/{id}/preview-token` with the sandbox ID and a TTL (max 24 hours). 2. MIOSA returns an iframe-ready URL with the token embedded. 3. The browser loads that URL, for example `https://.sandbox.miosa.ai?mt=mp_...`. 4. MIOSA's edge verifies the token server-side and proxies the request to the sandbox port. The token value never grants broader access. Preview tokens are non-renewable and non-transferable. When the TTL expires the URL returns `401`. Your backend mints a new token to restore access. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/$SBX/previews/$PREVIEW/share \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -d '{"ttl_seconds": 3600}' ``` [Browser Tokens reference →](/docs/platform/browser-tokens/) ## Share tokens A **share token** (prefix `ms_*`) is a revocable public share URL for a sandbox preview - analogous to a "share link" in a document editor. Key differences from preview tokens: | | Preview token (`mp_*`) | Share token (`ms_*`) | |--|------------------------|----------------------| | Intended recipient | Your authenticated user | Anyone with the link | | Minted by | Your backend | Your backend | | Revocable | Yes (delete the token) | Yes (delete the token) | | Password protection | No | Optional | | Expiry | Required TTL | Optional | Share tokens are useful for demos, client reviews, or collaborative previews where the recipient does not have a user account in your system. The link can be embedded as a plain URL - no `Authorization` header needed. [Previews reference →](/docs/develop/previews/) ## Next - [Tenant configuration](/docs/concepts/tenant-config/): preview domains and error-page branding. - [Browser Tokens reference](/docs/platform/browser-tokens/): minting, scopes, and revocation. - [Core Concepts](/docs/concepts/): the one-screen overview. --- # Cookbook URL: https://miosa.ai/docs/cookbook Fallback URL: https://miosa.roberto-c49.workers.dev/docs/cookbook Source: src/routes/docs/cookbook/+page.md Description: Every MIOSA recipe in one place, grouped by area. Sandbox procedures and platform-builder walkthroughs, each with a working code path. # Cookbook Practical walkthroughs for the things customers build on MIOSA. Each recipe starts with a product goal and ends with a running result, and every one is grounded in the current SDK and API. The recipes live next to the product they belong to in the sidebar, under **Sandboxes > Recipes** and **Platform builders > Recipes**. This page is the index of all of them. ## Sandbox recipes For running code, agents, and browsers inside a sandbox. The full lifecycle: user prompt to files, dev server, live preview, feedback loop, publish, and a stable production URL. Inject API keys and OAuth credentials into a sandbox without exposing them in code, logs, or the browser. Run code and stream stdout to your UI in real time: graders, workflow builders, data jobs, and AI tool execution. Drive the sandbox browser with an LLM over CDP: screenshot, decide, act, and watch the same session live. ## Platform builder recipes For embedding MIOSA into your own product and charging your customers for it. Tag every sandbox with your own workspace and user IDs, query usage per tenant, and hand the browser a short-lived preview share token. Cap end-users with max_sandboxes and credit limits, pull monthly rollups keyed by external_user_id, and wire the totals into Stripe. Your server mints a preview token; your frontend drops it into an iframe. React, Vue, Svelte, and vanilla variants, with TTL refresh. React to sandbox lifecycle events through HMAC-signed webhooks or the SSE stream, with idempotency patterns. A product blueprint for exposing a device per user: MIOSA mapping, architecture, data model, and jobs. ## Before you start All recipes share the same prerequisites: - A MIOSA workspace API key (`msk_u_*`) - see [API Keys](/docs/platform/api-keys/) - Node 22+ **or** Python 3.11+ installed locally - The MIOSA SDK installed for your language: ```bash # Python pip install miosa # TypeScript / Node npm install @miosa/sdk ``` Set the key in your environment once - every recipe reads it from there: ```bash export MIOSA_API_KEY="msk_u_..." ``` --- # Agent builds an app URL: https://miosa.ai/docs/cookbook/agent-builds-app Fallback URL: https://miosa.roberto-c49.workers.dev/docs/cookbook/agent-builds-app Source: src/routes/docs/cookbook/agent-builds-app/+page.md Description: Your AI agent receives a prompt, writes code into a sandbox, iterates visually with live preview, checkpoints progress, branches experiments, shares with stakeholders, and publishes to production. # Agent builds an app ~15 min Python TypeScript > **Goal**: Wire an LLM agent to MIOSA so it can write code, boot a live dev server, iterate on feedback with real-time preview, checkpoint progress, branch experiments, share with stakeholders, track usage, and finally publish to production from a single user prompt. > > **What you'll use**: Sandboxes, Processes, Previews, Snapshots, Shares, Deployments, Usage ## What you'll build A full-lifecycle function `agent_app_lifecycle(prompt)` that: 1. Generates initial files with an LLM and writes them atomically 2. Boots a dev server and mints a preview token for iframe embed 3. Iterates on user feedback: patches files, watches the file tree, preview auto-refreshes 4. Checkpoints between iterations so experiments can branch safely 5. Forks into parallel branches for A/B comparison without losing the working version 6. Creates a shareable public URL for stakeholder review 7. Tracks usage so you can bill the end-user 8. Promotes to an immutable production deployment The pattern is model-agnostic: MIOSA is the execution layer, your agent loop is the reasoning layer. ## Flow diagram Diagram: sequenceDiagram participant U as User participant A as Your Agent participant M as MIOSA Sandbox U->>A: "Build a dog-walking app landing page" A->>M: sandboxes.create(template_id="nextjs") A->>M: files.writeMany([page.tsx, styles.css, ...]) M->>A: ACK A->>M: processes.start("pnpm run dev") A->>M: sbx.previewToken(expires_in=3600) M-->>U: iframe embed with live preview loop Iterate on feedback U->>A: "Make the hero bigger" A->>M: files.writeMany([updated page.tsx]) M-->>U: Preview auto-refreshes A->>M: sbx.fork() for the next risky iteration end A->>M: sbx.fork() → branch sandbox Note over A,M: A/B experiment in parallel branch A->>M: sbx.previews.create(port=3000) A->>M: sbx.previews.share(preview_id, ttl_seconds=86400) M-->>U: Public stakeholder URL U->>A: "Ship it" A->>M: sbx.deploy(name="dogwalk-landing") M-->>U: Production URL ## Prerequisites - A MIOSA workspace API key (`msk_u_*`). See [API Keys](/docs/platform/api-keys/). - An Anthropic API key (or substitute your preferred provider) - Node 22+ or Python 3.11+ ## Step 1: Install and configure ```bash pip install miosa anthropic export MIOSA_API_KEY="msk_u_..." export ANTHROPIC_API_KEY="sk-ant-..." ``` ```bash npm install @miosa/sdk @anthropic-ai/sdk export MIOSA_API_KEY="msk_u_..." export ANTHROPIC_API_KEY="sk-ant-..." ``` ## Step 2: Boot a sandbox and write initial files Create a Next.js sandbox. The template already has Node, pnpm, and a scaffold, so skip `npx create-next-app`. Use `files.writeMany` to push all initial files in a single atomic call. ```python from miosa import Miosa client = Miosa(api_key=os.environ["MIOSA_API_KEY"]) ai = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) # Boot the sandbox sbx = client.sandboxes.create( template_id="nextjs", timeout_sec=3600, external_user_id="user-42", # attribute usage to your end-user ) # Ask the LLM to generate the initial file set def generate_files(prompt: str) -> list[dict]: msg = ai.messages.create( model="claude-opus-4-5", max_tokens=8096, system=( "You are a Next.js developer. " "Return a JSON array of {path, content} objects for every file you create. " "Paths are relative to /workspace. Content is the full file text. " "No explanation, just the JSON array." ), messages=[{"role": "user", "content": prompt}], ) return json.loads(msg.content[0].text) user_prompt = "Build a landing page for a dog-walking app with a hero, feature list, and CTA." file_list = generate_files(user_prompt) # Write all files atomically result = sbx.files.write_many(file_list) print(f"Written: {[f['path'] for f in result['written']]}") ``` ```typescript const client = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }); const ai = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }); // Boot the sandbox const sbx = await client.sandboxes.create({ templateId: 'nextjs', timeoutSec: 3600, externalUserId: 'user-42', }); async function generateFiles(prompt: string): Promise> { const msg = await ai.messages.create({ model: 'claude-opus-4-5', max_tokens: 8096, system: 'You are a Next.js developer. Return a JSON array of {path, content} objects for every file you create. ' + 'Paths are relative to /workspace. Content is the full file text. No explanation, just the JSON array.', messages: [{ role: 'user', content: prompt }], }); const block = msg.content[0]; return JSON.parse(block.type === 'text' ? block.text : '[]'); } const userPrompt = 'Build a landing page for a dog-walking app with a hero, feature list, and CTA.'; const fileList = await generateFiles(userPrompt); // Write all files atomically const result = await sbx.files.writeMany(fileList); console.log('Written:', result.written.map(f => f.path)); ``` ## Step 3: Start the dev server and expose a preview Use `processes.start` to boot the dev server as a long-running background process. Then call `sbx.previewToken` to get a short-lived token your frontend uses in an iframe. No public URL is required, and no `msk_*` key ever reaches the browser. ```python # Start the dev server as a long-running process proc = sbx.processes.start( command="pnpm run dev --port 3000", name="nextjs-dev", ) print(f"Dev server pid: {proc['pid']}") # Mint a 1-hour preview token for iframe embed token_info = sbx.preview_token(expires_in=3600) print(f"Embed URL: {token_info['url']}") # Send token_info['url'] to your frontend and use it as ``` Never put `msk_*` in a `