openapi: "3.1.0" info: title: Wrenn Sandbox API description: MicroVM-based code execution platform API. version: "0.1.0" servers: - url: http://localhost:8080 description: Local development security: [] paths: /v1/auth/signup: post: summary: Create a new account operationId: signup tags: [auth] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SignupRequest" responses: "201": description: Account created content: application/json: schema: $ref: "#/components/schemas/AuthResponse" "400": description: Invalid request (bad email, short password) content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Email already registered content: application/json: schema: $ref: "#/components/schemas/Error" /v1/auth/switch-team: post: summary: Switch active team operationId: switchTeam tags: [auth] security: - bearerAuth: [] description: | Re-issues a JWT scoped to a different team. The user must be a member of the target team (verified from DB). Use the returned token for subsequent requests to that team's resources. requestBody: required: true content: application/json: schema: type: object required: [team_id] properties: team_id: type: string responses: "200": description: New JWT issued for the target team content: application/json: schema: $ref: "#/components/schemas/AuthResponse" "403": description: Not a member of this team content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Team not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/auth/login: post: summary: Log in with email and password operationId: login tags: [auth] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LoginRequest" responses: "200": description: Login successful content: application/json: schema: $ref: "#/components/schemas/AuthResponse" "401": description: Invalid credentials content: application/json: schema: $ref: "#/components/schemas/Error" /v1/auth/oauth/{provider}: parameters: - name: provider in: path required: true schema: type: string enum: [github] description: OAuth provider name get: summary: Start OAuth login flow operationId: oauthRedirect tags: [auth] description: | Redirects the user to the OAuth provider's authorization page. Sets a short-lived CSRF state cookie for validation on callback. responses: "302": description: Redirect to provider authorization URL "404": description: Provider not found or not configured content: application/json: schema: $ref: "#/components/schemas/Error" /v1/auth/oauth/{provider}/callback: parameters: - name: provider in: path required: true schema: type: string enum: [github] description: OAuth provider name get: summary: OAuth callback operationId: oauthCallback tags: [auth] description: | Handles the OAuth provider's callback after user authorization. Exchanges the authorization code for a user profile, creates or logs in the user, and redirects to the frontend with a JWT token. **On success:** redirects to `{OAUTH_REDIRECT_URL}/auth/{provider}/callback?token=...&user_id=...&team_id=...&email=...` **On error:** redirects to `{OAUTH_REDIRECT_URL}/auth/{provider}/callback?error=...` Possible error codes: `access_denied`, `invalid_state`, `missing_code`, `exchange_failed`, `email_taken`, `internal_error`. parameters: - name: code in: query schema: type: string description: Authorization code from the OAuth provider - name: state in: query schema: type: string description: CSRF state parameter (must match the cookie) responses: "302": description: Redirect to frontend with token or error /v1/api-keys: post: summary: Create an API key operationId: createAPIKey tags: [api-keys] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateAPIKeyRequest" responses: "201": description: API key created (plaintext key only shown once) content: application/json: schema: $ref: "#/components/schemas/APIKeyResponse" "401": description: Unauthorized content: application/json: schema: $ref: "#/components/schemas/Error" get: summary: List API keys for your team operationId: listAPIKeys tags: [api-keys] security: - bearerAuth: [] responses: "200": description: List of API keys (plaintext keys are never returned) content: application/json: schema: type: array items: $ref: "#/components/schemas/APIKeyResponse" /v1/api-keys/{id}: parameters: - name: id in: path required: true schema: type: string delete: summary: Delete an API key operationId: deleteAPIKey tags: [api-keys] security: - bearerAuth: [] responses: "204": description: API key deleted /v1/users/search: get: summary: Search users by email prefix operationId: searchUsers tags: [users] security: - bearerAuth: [] description: | Returns up to 10 users whose email starts with the given prefix. The prefix must contain "@". Intended for the add-member UI autocomplete. parameters: - name: email in: query required: true schema: type: string description: Email prefix (must contain "@", e.g. "alice@") responses: "200": description: Matching users content: application/json: schema: type: array items: $ref: "#/components/schemas/UserSearchResult" "400": description: Prefix does not contain "@" content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams: get: summary: List teams for the authenticated user operationId: listTeams tags: [teams] security: - bearerAuth: [] responses: "200": description: Teams the user belongs to, each with their role content: application/json: schema: type: array items: $ref: "#/components/schemas/TeamWithRole" post: summary: Create a new team operationId: createTeam tags: [teams] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string description: 1-128 chars; A-Z a-z 0-9 space _ responses: "201": description: Team created (caller is owner) content: application/json: schema: $ref: "#/components/schemas/TeamWithRole" "400": description: Invalid team name content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams/{id}: parameters: - name: id in: path required: true schema: type: string description: Team ID (must match the JWT's team_id) get: summary: Get team info and member list operationId: getTeam tags: [teams] security: - bearerAuth: [] responses: "200": description: Team details with members content: application/json: schema: $ref: "#/components/schemas/TeamDetail" "403": description: JWT team does not match requested team content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Team not found content: application/json: schema: $ref: "#/components/schemas/Error" patch: summary: Rename the team operationId: renameTeam tags: [teams] security: - bearerAuth: [] description: Admin or owner role required (verified from DB). requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string responses: "204": description: Renamed "400": description: Invalid team name content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Insufficient role content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Delete the team operationId: deleteTeam tags: [teams] security: - bearerAuth: [] description: | Owner only. Soft-deletes the team and destroys all running/paused/starting sandboxes. All DB records are preserved. The team slug is permanently reserved. responses: "204": description: Team deleted "403": description: Caller is not the owner content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams/{id}/members: parameters: - name: id in: path required: true schema: type: string get: summary: List team members operationId: listTeamMembers tags: [teams] security: - bearerAuth: [] responses: "200": description: Members with roles content: application/json: schema: type: array items: $ref: "#/components/schemas/TeamMember" post: summary: Add a member by email operationId: addTeamMember tags: [teams] security: - bearerAuth: [] description: Admin or owner role required. User is added instantly as a member. requestBody: required: true content: application/json: schema: type: object required: [email] properties: email: type: string format: email responses: "201": description: Member added content: application/json: schema: $ref: "#/components/schemas/TeamMember" "403": description: Insufficient role content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: No account with that email content: application/json: schema: $ref: "#/components/schemas/Error" "400": description: User is already a member content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams/{id}/members/{uid}: parameters: - name: id in: path required: true schema: type: string - name: uid in: path required: true schema: type: string description: Target user ID patch: summary: Update member role operationId: updateMemberRole tags: [teams] security: - bearerAuth: [] description: | Admin or owner required. Valid target roles: admin, member. The owner's role cannot be changed. requestBody: required: true content: application/json: schema: type: object required: [role] properties: role: type: string enum: [admin, member] responses: "204": description: Role updated "403": description: Insufficient role or attempt to modify owner content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: User is not a member content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Remove a member operationId: removeTeamMember tags: [teams] security: - bearerAuth: [] description: Admin or owner required. Owner cannot be removed. responses: "204": description: Member removed "403": description: Insufficient role or attempt to remove owner content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: User is not a member content: application/json: schema: $ref: "#/components/schemas/Error" /v1/teams/{id}/leave: parameters: - name: id in: path required: true schema: type: string post: summary: Leave the team operationId: leaveTeam tags: [teams] security: - bearerAuth: [] description: The owner cannot leave; they must delete the team instead. responses: "204": description: Left the team "403": description: Owner cannot leave content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes: post: summary: Create a sandbox operationId: createSandbox tags: [sandboxes] security: - apiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSandboxRequest" responses: "201": description: Sandbox created content: application/json: schema: $ref: "#/components/schemas/Sandbox" "502": description: Host agent error content: application/json: schema: $ref: "#/components/schemas/Error" get: summary: List sandboxes for your team operationId: listSandboxes tags: [sandboxes] security: - apiKeyAuth: [] responses: "200": description: List of sandboxes content: application/json: schema: type: array items: $ref: "#/components/schemas/Sandbox" /v1/sandboxes/stats: get: summary: Get sandbox usage stats for your team operationId: getSandboxStats tags: [sandboxes] security: - apiKeyAuth: [] parameters: - name: range in: query required: false schema: type: string enum: [5m, 1h, 6h, 24h, 30d] default: 1h description: Time window for the time-series data. responses: "200": description: Sandbox stats for the team content: application/json: schema: $ref: "#/components/schemas/SandboxStats" "400": $ref: "#/components/responses/BadRequest" /v1/sandboxes/{id}: parameters: - name: id in: path required: true schema: type: string get: summary: Get sandbox details operationId: getSandbox tags: [sandboxes] security: - apiKeyAuth: [] responses: "200": description: Sandbox details content: application/json: schema: $ref: "#/components/schemas/Sandbox" "404": description: Sandbox not found content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Destroy a sandbox operationId: destroySandbox tags: [sandboxes] security: - apiKeyAuth: [] responses: "204": description: Sandbox destroyed /v1/sandboxes/{id}/exec: parameters: - name: id in: path required: true schema: type: string post: summary: Execute a command operationId: execCommand tags: [sandboxes] security: - apiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExecRequest" responses: "200": description: Command output content: application/json: schema: $ref: "#/components/schemas/ExecResponse" "404": description: Sandbox not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/ping: parameters: - name: id in: path required: true schema: type: string post: summary: Reset sandbox inactivity timer operationId: pingSandbox tags: [sandboxes] security: - apiKeyAuth: [] description: | Resets the last_active_at timestamp for a running sandbox, preventing the auto-pause TTL from expiring. Use this as a keepalive for sandboxes that are idle but should remain running. responses: "204": description: Ping acknowledged, inactivity timer reset "404": description: Sandbox not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/metrics: parameters: - name: id in: path required: true schema: type: string get: summary: Get per-sandbox resource metrics operationId: getSandboxMetrics tags: [sandboxes] security: - apiKeyAuth: [] - bearerAuth: [] description: | Returns time-series CPU, memory, and disk metrics for a sandbox. Three tiers are available with different granularity and retention: - `10m`: 500ms samples, last 10 minutes - `2h`: 30-second averages, last 2 hours - `24h`: 5-minute averages, last 24 hours For running sandboxes, data comes from the host agent's in-memory ring buffer. For paused sandboxes, data is read from persisted snapshots in the database. Stopped/destroyed sandboxes return 404. parameters: - name: range in: query required: false schema: type: string enum: ["5m", "10m", "1h", "2h", "6h", "12h", "24h"] default: "10m" description: Time range filter to query responses: "200": description: Metrics retrieved content: application/json: schema: $ref: "#/components/schemas/SandboxMetrics" "400": description: Invalid range parameter content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Sandbox not found or metrics not available content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/pause: parameters: - name: id in: path required: true schema: type: string post: summary: Pause a running sandbox operationId: pauseSandbox tags: [sandboxes] security: - apiKeyAuth: [] description: | Takes a snapshot of the sandbox (VM state + memory + rootfs), then destroys all running resources. The sandbox exists only as files on disk and can be resumed later. responses: "200": description: Sandbox paused (snapshot taken, resources released) content: application/json: schema: $ref: "#/components/schemas/Sandbox" "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/resume: parameters: - name: id in: path required: true schema: type: string post: summary: Resume a paused sandbox operationId: resumeSandbox tags: [sandboxes] security: - apiKeyAuth: [] description: | Restores a paused sandbox from its snapshot using UFFD for lazy memory loading. Boots a fresh Firecracker process, sets up a new network slot, and waits for envd to become ready. responses: "200": description: Sandbox resumed (new VM booted from snapshot) content: application/json: schema: $ref: "#/components/schemas/Sandbox" "409": description: Sandbox not paused content: application/json: schema: $ref: "#/components/schemas/Error" /v1/snapshots: post: summary: Create a snapshot template operationId: createSnapshot tags: [snapshots] security: - apiKeyAuth: [] description: | Pauses a running sandbox, takes a full snapshot, copies the snapshot files to the images directory as a reusable template, then destroys the sandbox. The template can be used to create new sandboxes. parameters: - name: overwrite in: query required: false schema: type: string enum: ["true"] description: Set to "true" to overwrite an existing snapshot with the same name. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSnapshotRequest" responses: "201": description: Snapshot created content: application/json: schema: $ref: "#/components/schemas/Template" "409": description: Name already exists or sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" get: summary: List templates for your team operationId: listSnapshots tags: [snapshots] security: - apiKeyAuth: [] parameters: - name: type in: query required: false schema: type: string enum: [base, snapshot] description: Filter by template type. responses: "200": description: List of templates content: application/json: schema: type: array items: $ref: "#/components/schemas/Template" /v1/snapshots/{name}: parameters: - name: name in: path required: true schema: type: string delete: summary: Delete a snapshot template operationId: deleteSnapshot tags: [snapshots] security: - apiKeyAuth: [] description: Removes the snapshot files from disk and deletes the database record. responses: "204": description: Snapshot deleted "404": description: Template not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/files/write: parameters: - name: id in: path required: true schema: type: string post: summary: Upload a file operationId: uploadFile tags: [sandboxes] security: - apiKeyAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [path, file] properties: path: type: string description: Absolute destination path inside the sandbox file: type: string format: binary description: File content responses: "204": description: File uploaded "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" "413": description: File too large content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/files/read: parameters: - name: id in: path required: true schema: type: string post: summary: Download a file operationId: downloadFile tags: [sandboxes] security: - apiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReadFileRequest" responses: "200": description: File content content: application/octet-stream: schema: type: string format: binary "404": description: Sandbox or file not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/files/list: parameters: - name: id in: path required: true schema: type: string post: summary: List directory contents operationId: listDir tags: [sandboxes] security: - apiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ListDirRequest" responses: "200": description: Directory listing content: application/json: schema: $ref: "#/components/schemas/ListDirResponse" "404": description: Sandbox not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/files/mkdir: parameters: - name: id in: path required: true schema: type: string post: summary: Create a directory operationId: makeDir tags: [sandboxes] security: - apiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MakeDirRequest" responses: "200": description: Directory created content: application/json: schema: $ref: "#/components/schemas/MakeDirResponse" "404": description: Sandbox not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/files/remove: parameters: - name: id in: path required: true schema: type: string post: summary: Remove a file or directory operationId: removePath tags: [sandboxes] security: - apiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RemoveRequest" responses: "204": description: File or directory removed "404": description: Sandbox not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/exec/stream: parameters: - name: id in: path required: true schema: type: string get: summary: Stream command execution via WebSocket operationId: execStream tags: [sandboxes] security: - apiKeyAuth: [] description: | Opens a WebSocket connection for streaming command execution. **Client sends** (first message to start the process): ```json {"type": "start", "cmd": "tail", "args": ["-f", "/var/log/syslog"]} ``` **Client sends** (to stop the process): ```json {"type": "stop"} ``` **Server sends** (process events as they arrive): ```json {"type": "start", "pid": 1234} {"type": "stdout", "data": "line of output\n"} {"type": "stderr", "data": "warning message\n"} {"type": "exit", "exit_code": 0} {"type": "error", "data": "description of error"} ``` The connection closes automatically after the process exits. responses: "101": description: WebSocket upgrade "404": description: Sandbox not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/files/stream/write: parameters: - name: id in: path required: true schema: type: string post: summary: Upload a file (streaming) operationId: streamUploadFile tags: [sandboxes] security: - apiKeyAuth: [] description: | Streams file content to the sandbox without buffering in memory. Suitable for large files. Uses the same multipart/form-data format as the non-streaming upload endpoint. requestBody: required: true content: multipart/form-data: schema: type: object required: [path, file] properties: path: type: string description: Absolute destination path inside the sandbox file: type: string format: binary description: File content responses: "204": description: File uploaded "404": description: Sandbox not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/sandboxes/{id}/files/stream/read: parameters: - name: id in: path required: true schema: type: string post: summary: Download a file (streaming) operationId: streamDownloadFile tags: [sandboxes] security: - apiKeyAuth: [] description: | Streams file content from the sandbox without buffering in memory. Suitable for large files. Returns raw bytes with chunked transfer encoding. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReadFileRequest" responses: "200": description: File content streamed in chunks content: application/octet-stream: schema: type: string format: binary "404": description: Sandbox or file not found content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Sandbox not running content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts: post: summary: Create a host operationId: createHost tags: [hosts] security: - bearerAuth: [] description: | Creates a new host record and returns a one-time registration token. Regular hosts can only be created by admins. BYOC hosts can be created by admins or team owners. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateHostRequest" responses: "201": description: Host created with registration token content: application/json: schema: $ref: "#/components/schemas/CreateHostResponse" "400": description: Invalid request content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" get: summary: List hosts operationId: listHosts tags: [hosts] security: - bearerAuth: [] description: | Admins see all hosts. Non-admins see only BYOC hosts belonging to their team. responses: "200": description: List of hosts content: application/json: schema: type: array items: $ref: "#/components/schemas/Host" /v1/hosts/{id}: parameters: - name: id in: path required: true schema: type: string get: summary: Get host details operationId: getHost tags: [hosts] security: - bearerAuth: [] responses: "200": description: Host details content: application/json: schema: $ref: "#/components/schemas/Host" "404": description: Host not found content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Delete a host operationId: deleteHost tags: [hosts] security: - bearerAuth: [] description: | Admins can delete any host. Team owners and admins can delete BYOC hosts belonging to their team. Without `?force=true`, returns 409 if the host has active sandboxes. With `?force=true`, destroys all sandboxes first. parameters: - name: force in: query required: false schema: type: boolean description: If true, destroy all sandboxes on the host before deleting. responses: "204": description: Host deleted "403": description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Host has active sandboxes (only when force is not set) content: application/json: schema: $ref: "#/components/schemas/HostHasSandboxesError" /v1/hosts/{id}/token: parameters: - name: id in: path required: true schema: type: string post: summary: Regenerate registration token operationId: regenerateHostToken tags: [hosts] security: - bearerAuth: [] description: | Issues a new registration token for a host still in "pending" status. Use this when a previous registration attempt failed after consuming the original token. Same permission model as host creation. responses: "201": description: New registration token issued content: application/json: schema: $ref: "#/components/schemas/CreateHostResponse" "403": description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Host is not in pending status content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/register: post: summary: Register a host agent operationId: registerHost tags: [hosts] description: | Called by the host agent on first startup. Validates the one-time registration token, records machine specs, sets the host status to "online", and returns a long-lived JWT for subsequent API calls (heartbeats). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegisterHostRequest" responses: "201": description: Host registered, JWT returned content: application/json: schema: $ref: "#/components/schemas/RegisterHostResponse" "400": description: Invalid request content: application/json: schema: $ref: "#/components/schemas/Error" "401": description: Invalid or expired registration token content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/{id}/heartbeat: parameters: - name: id in: path required: true schema: type: string post: summary: Host agent heartbeat operationId: hostHeartbeat tags: [hosts] security: - hostTokenAuth: [] description: | Updates the host's last_heartbeat_at timestamp. The host ID in the URL must match the host ID in the JWT. responses: "204": description: Heartbeat recorded "401": description: Invalid or missing host token content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Host ID mismatch content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/auth/refresh: post: summary: Refresh host JWT operationId: refreshHostToken tags: [hosts] description: | Exchanges a refresh token for a new JWT and rotated refresh token. The old refresh token is immediately revoked. No authentication required — the refresh token itself is the credential. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RefreshHostTokenRequest" responses: "200": description: New JWT and rotated refresh token content: application/json: schema: $ref: "#/components/schemas/RefreshHostTokenResponse" "401": description: Invalid, expired, or revoked refresh token content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/{id}/delete-preview: parameters: - name: id in: path required: true schema: type: string get: summary: Preview host deletion operationId: getHostDeletePreview tags: [hosts] security: - bearerAuth: [] description: | Returns the list of sandbox IDs that would be destroyed if the host were deleted with `?force=true`. No state is modified. responses: "200": description: Deletion preview content: application/json: schema: $ref: "#/components/schemas/HostDeletePreview" "403": description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Host not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/{id}/tags: parameters: - name: id in: path required: true schema: type: string get: summary: List host tags operationId: listHostTags tags: [hosts] security: - bearerAuth: [] responses: "200": description: List of tags content: application/json: schema: type: array items: type: string post: summary: Add a tag to a host operationId: addHostTag tags: [hosts] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AddTagRequest" responses: "204": description: Tag added "404": description: Host not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/hosts/{id}/tags/{tag}: parameters: - name: id in: path required: true schema: type: string - name: tag in: path required: true schema: type: string delete: summary: Remove a tag from a host operationId: removeHostTag tags: [hosts] security: - bearerAuth: [] responses: "204": description: Tag removed "404": description: Host not found content: application/json: schema: $ref: "#/components/schemas/Error" /v1/channels: post: summary: Create a notification channel operationId: createChannel tags: [channels] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateChannelRequest" responses: "201": description: Channel created content: application/json: schema: $ref: "#/components/schemas/ChannelResponse" "400": $ref: "#/components/responses/BadRequest" get: summary: List notification channels operationId: listChannels tags: [channels] security: - bearerAuth: [] responses: "200": description: Channels list content: application/json: schema: type: array items: $ref: "#/components/schemas/ChannelResponse" /v1/channels/test: post: summary: Test a channel configuration description: > Sends a test notification using the provided provider and config without saving anything. Use this to verify credentials before creating a channel. operationId: testChannel tags: [channels] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TestChannelRequest" responses: "200": description: Test notification sent successfully content: application/json: schema: type: object properties: status: type: string example: ok "400": $ref: "#/components/responses/BadRequest" /v1/channels/{id}: parameters: - name: id in: path required: true schema: type: string get: summary: Get a notification channel operationId: getChannel tags: [channels] security: - bearerAuth: [] responses: "200": description: Channel details content: application/json: schema: $ref: "#/components/schemas/ChannelResponse" "404": description: Channel not found content: application/json: schema: $ref: "#/components/schemas/Error" patch: summary: Update a notification channel operationId: updateChannel tags: [channels] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateChannelRequest" responses: "200": description: Channel updated content: application/json: schema: $ref: "#/components/schemas/ChannelResponse" "400": $ref: "#/components/responses/BadRequest" "404": description: Channel not found content: application/json: schema: $ref: "#/components/schemas/Error" delete: summary: Delete a notification channel operationId: deleteChannel tags: [channels] security: - bearerAuth: [] responses: "204": description: Channel deleted /v1/channels/{id}/config: parameters: - name: id in: path required: true schema: type: string put: summary: Rotate channel secrets description: > Replaces the channel's provider configuration entirely with new secrets. The previous config is discarded. Config fields must match the provider's required fields. operationId: rotateChannelConfig tags: [channels] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RotateConfigRequest" responses: "200": description: Config rotated content: application/json: schema: $ref: "#/components/schemas/ChannelResponse" "400": $ref: "#/components/responses/BadRequest" "404": description: Channel not found content: application/json: schema: $ref: "#/components/schemas/Error" components: securitySchemes: apiKeyAuth: type: apiKey in: header name: X-API-Key description: API key for sandbox lifecycle operations. Create via POST /v1/api-keys. bearerAuth: type: http scheme: bearer bearerFormat: JWT description: JWT token from /v1/auth/login or /v1/auth/signup. Valid for 6 hours. hostTokenAuth: type: apiKey in: header name: X-Host-Token description: Host JWT returned from POST /v1/hosts/register or POST /v1/hosts/auth/refresh. Valid for 7 days. schemas: SignupRequest: type: object required: [email, password, name] properties: email: type: string format: email password: type: string minLength: 8 name: type: string maxLength: 100 LoginRequest: type: object required: [email, password] properties: email: type: string format: email password: type: string AuthResponse: type: object properties: token: type: string description: JWT token (valid for 6 hours) user_id: type: string team_id: type: string email: type: string name: type: string CreateAPIKeyRequest: type: object properties: name: type: string default: Unnamed API Key APIKeyResponse: type: object properties: id: type: string team_id: type: string name: type: string key_prefix: type: string description: Display prefix (e.g. "wrn_ab12cd34...") created_at: type: string format: date-time last_used: type: string format: date-time nullable: true key: type: string description: Full plaintext key. Only returned on creation, never again. nullable: true CreateSandboxRequest: type: object properties: template: type: string default: minimal vcpus: type: integer default: 1 memory_mb: type: integer default: 512 timeout_sec: type: integer default: 0 description: > Auto-pause TTL in seconds. The sandbox is automatically paused after this duration of inactivity (no exec or ping). 0 means no auto-pause. SandboxStats: type: object properties: range: type: string enum: [5m, 1h, 6h, 24h, 30d] current: type: object properties: running_count: type: integer vcpus_reserved: type: integer memory_mb_reserved: type: integer sampled_at: type: string format: date-time nullable: true peaks: type: object description: Maximum values over the last 30 days. properties: running_count: type: integer vcpus: type: integer memory_mb: type: integer series: type: object description: Parallel arrays for chart rendering. properties: labels: type: array items: type: string format: date-time running: type: array items: type: integer vcpus: type: array items: type: integer memory_mb: type: array items: type: integer Sandbox: type: object properties: id: type: string status: type: string enum: [pending, starting, running, paused, hibernated, stopped, missing, error] template: type: string vcpus: type: integer memory_mb: type: integer timeout_sec: type: integer guest_ip: type: string host_ip: type: string created_at: type: string format: date-time started_at: type: string format: date-time nullable: true last_active_at: type: string format: date-time nullable: true last_updated: type: string format: date-time CreateSnapshotRequest: type: object required: [sandbox_id] properties: sandbox_id: type: string description: ID of the running sandbox to snapshot. name: type: string description: Name for the snapshot template. Auto-generated if omitted. Template: type: object properties: name: type: string type: type: string enum: [base, snapshot] vcpus: type: integer nullable: true memory_mb: type: integer nullable: true size_bytes: type: integer format: int64 created_at: type: string format: date-time ExecRequest: type: object required: [cmd] properties: cmd: type: string args: type: array items: type: string timeout_sec: type: integer default: 30 ExecResponse: type: object properties: sandbox_id: type: string cmd: type: string stdout: type: string stderr: type: string exit_code: type: integer duration_ms: type: integer encoding: type: string enum: [utf-8, base64] description: Output encoding. "base64" when stdout/stderr contain binary data. ReadFileRequest: type: object required: [path] properties: path: type: string description: Absolute file path inside the sandbox ListDirRequest: type: object required: [path] properties: path: type: string description: Directory path inside the sandbox depth: type: integer default: 1 description: Recursion depth (0 = non-recursive, 1 = immediate children) ListDirResponse: type: object properties: entries: type: array items: $ref: "#/components/schemas/FileEntry" FileEntry: type: object properties: name: type: string path: type: string type: type: string enum: [file, directory, symlink] size: type: integer format: int64 mode: type: integer permissions: type: string description: Human-readable permissions (e.g. "-rwxr-xr-x") owner: type: string group: type: string modified_at: type: integer format: int64 description: Unix timestamp (seconds) symlink_target: type: string nullable: true MakeDirRequest: type: object required: [path] properties: path: type: string description: Directory path to create inside the sandbox MakeDirResponse: type: object properties: entry: $ref: "#/components/schemas/FileEntry" RemoveRequest: type: object required: [path] properties: path: type: string description: Path to remove inside the sandbox CreateHostRequest: type: object required: [type] properties: type: type: string enum: [regular, byoc] description: Host type. Regular hosts are shared; BYOC hosts belong to a team. team_id: type: string description: Required for BYOC hosts. provider: type: string description: Cloud provider (e.g. aws, gcp, hetzner, bare-metal). availability_zone: type: string description: Availability zone (e.g. us-east, eu-west). CreateHostResponse: type: object properties: host: $ref: "#/components/schemas/Host" registration_token: type: string description: One-time registration token for the host agent. Expires in 1 hour. RegisterHostRequest: type: object required: [token, address] properties: token: type: string description: One-time registration token from POST /v1/hosts. arch: type: string description: CPU architecture (e.g. x86_64, aarch64). cpu_cores: type: integer memory_mb: type: integer disk_gb: type: integer address: type: string description: Host agent address (ip:port). RegisterHostResponse: type: object properties: host: $ref: "#/components/schemas/Host" token: type: string description: Host JWT for X-Host-Token header. Valid for 7 days. refresh_token: type: string description: Refresh token for obtaining new JWTs. Valid for 60 days; rotated on each use. Host: type: object properties: id: type: string type: type: string enum: [regular, byoc] team_id: type: string nullable: true provider: type: string nullable: true availability_zone: type: string nullable: true arch: type: string nullable: true cpu_cores: type: integer nullable: true memory_mb: type: integer nullable: true disk_gb: type: integer nullable: true address: type: string nullable: true status: type: string enum: [pending, online, offline, draining, unreachable] last_heartbeat_at: type: string format: date-time nullable: true created_by: type: string created_at: type: string format: date-time updated_at: type: string format: date-time RefreshHostTokenRequest: type: object required: [refresh_token] properties: refresh_token: type: string description: Refresh token obtained from registration or a previous refresh. RefreshHostTokenResponse: type: object properties: host: $ref: "#/components/schemas/Host" token: type: string description: New host JWT. Valid for 7 days. refresh_token: type: string description: New refresh token. Valid for 60 days; old token is revoked. HostDeletePreview: type: object properties: host: $ref: "#/components/schemas/Host" sandbox_ids: type: array items: type: string description: IDs of sandboxes that would be destroyed on force-delete. HostHasSandboxesError: type: object properties: error: type: object properties: code: type: string example: host_has_sandboxes message: type: string sandbox_ids: type: array items: type: string description: IDs of active sandboxes blocking deletion. AddTagRequest: type: object required: [tag] properties: tag: type: string UserSearchResult: type: object properties: user_id: type: string email: type: string Team: type: object properties: id: type: string name: type: string slug: type: string description: Immutable 12-char hex slug (e.g. a1b2c3-d1e2f3) created_at: type: string format: date-time TeamWithRole: allOf: - $ref: "#/components/schemas/Team" - type: object properties: role: type: string enum: [owner, admin, member] TeamMember: type: object properties: user_id: type: string email: type: string role: type: string enum: [owner, admin, member] joined_at: type: string format: date-time TeamDetail: type: object properties: team: $ref: "#/components/schemas/Team" members: type: array items: $ref: "#/components/schemas/TeamMember" SandboxMetrics: type: object properties: sandbox_id: type: string range: type: string enum: ["5m", "10m", "1h", "2h", "6h", "12h", "24h"] points: type: array items: $ref: "#/components/schemas/MetricPoint" MetricPoint: type: object properties: timestamp_unix: type: integer format: int64 cpu_pct: type: number format: double description: "CPU utilization percentage (0-100), normalized to vCPU count" mem_bytes: type: integer format: int64 description: "Resident memory in bytes (VmRSS of Firecracker process)" disk_bytes: type: integer format: int64 description: "Allocated disk bytes for the CoW sparse file" CreateChannelRequest: type: object required: [name, provider, config, events] properties: name: type: string description: Unique channel name within the team. provider: type: string enum: [discord, slack, teams, googlechat, telegram, matrix, webhook] config: type: object additionalProperties: type: string description: > Provider-specific configuration fields. Discord/Slack/Teams/Google Chat: {"webhook_url": "..."}. Telegram: {"bot_token": "...", "chat_id": "..."}. Matrix: {"homeserver_url": "...", "access_token": "...", "room_id": "..."}. Webhook: {"url": "...", "secret": "..."} (secret is auto-generated if omitted). events: type: array items: type: string enum: - capsule.created - capsule.running - capsule.paused - capsule.destroyed - template.snapshot.created - template.snapshot.deleted - host.up - host.down TestChannelRequest: type: object required: [provider, config] properties: provider: type: string enum: [discord, slack, teams, googlechat, telegram, matrix, webhook] config: type: object additionalProperties: type: string description: Provider-specific configuration fields (same as CreateChannelRequest.config). RotateConfigRequest: type: object required: [config] properties: config: type: object additionalProperties: type: string description: > New provider configuration fields. Must include all required fields for the channel's provider. Replaces the existing config entirely. UpdateChannelRequest: type: object required: [name, events] properties: name: type: string events: type: array items: type: string enum: - capsule.created - capsule.running - capsule.paused - capsule.destroyed - template.snapshot.created - template.snapshot.deleted - host.up - host.down ChannelResponse: type: object properties: id: type: string team_id: type: string name: type: string provider: type: string enum: [discord, slack, teams, googlechat, telegram, matrix, webhook] events: type: array items: type: string created_at: type: string format: date-time updated_at: type: string format: date-time secret: type: string nullable: true description: Webhook secret. Only returned on creation, never again. Error: type: object properties: error: type: object properties: code: type: string message: type: string