1
0
forked from wrenn/wrenn
Files
wrenn-releases/internal/api/openapi.yaml

2622 lines
68 KiB
YAML

openapi: "3.1.0"
info:
title: Wrenn 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
capsulees. 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/capsules:
post:
summary: Create a capsule
operationId: createCapsule
tags: [capsules]
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateCapsuleRequest"
responses:
"201":
description: Capsule created
content:
application/json:
schema:
$ref: "#/components/schemas/Capsule"
"502":
description: Host agent error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
get:
summary: List capsulees for your team
operationId: listCapsules
tags: [capsules]
security:
- apiKeyAuth: []
responses:
"200":
description: List of capsulees
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Capsule"
/v1/capsules/stats:
get:
summary: Get capsule usage stats for your team
operationId: getCapsuleStats
tags: [capsules]
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: Capsule stats for the team
content:
application/json:
schema:
$ref: "#/components/schemas/CapsuleStats"
"400":
$ref: "#/components/responses/BadRequest"
/v1/capsules/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
summary: Get capsule details
operationId: getCapsule
tags: [capsules]
security:
- apiKeyAuth: []
responses:
"200":
description: Capsule details
content:
application/json:
schema:
$ref: "#/components/schemas/Capsule"
"404":
description: Capsule not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
delete:
summary: Destroy a capsule
operationId: destroyCapsule
tags: [capsules]
security:
- apiKeyAuth: []
responses:
"204":
description: Capsule destroyed
/v1/capsules/{id}/exec:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Execute a command
operationId: execCommand
tags: [capsules]
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: Capsule not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/ping:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Reset capsule inactivity timer
operationId: pingCapsule
tags: [capsules]
security:
- apiKeyAuth: []
description: |
Resets the last_active_at timestamp for a running capsule, preventing
the auto-pause TTL from expiring. Use this as a keepalive for capsulees
that are idle but should remain running.
responses:
"204":
description: Ping acknowledged, inactivity timer reset
"404":
description: Capsule not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/metrics:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
summary: Get per-capsule resource metrics
operationId: getCapsuleMetrics
tags: [capsules]
security:
- apiKeyAuth: []
- bearerAuth: []
description: |
Returns time-series CPU, memory, and disk metrics for a capsule.
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 capsulees, data comes from the host agent's in-memory
ring buffer. For paused capsulees, data is read from persisted
snapshots in the database. Stopped/destroyed capsulees 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/CapsuleMetrics"
"400":
description: Invalid range parameter
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Capsule not found or metrics not available
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/pause:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Pause a running capsule
operationId: pauseCapsule
tags: [capsules]
security:
- apiKeyAuth: []
description: |
Takes a snapshot of the capsule (VM state + memory + rootfs), then
destroys all running resources. The capsule exists only as files on
disk and can be resumed later.
responses:
"200":
description: Capsule paused (snapshot taken, resources released)
content:
application/json:
schema:
$ref: "#/components/schemas/Capsule"
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/resume:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Resume a paused capsule
operationId: resumeCapsule
tags: [capsules]
security:
- apiKeyAuth: []
description: |
Restores a paused capsule 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: Capsule resumed (new VM booted from snapshot)
content:
application/json:
schema:
$ref: "#/components/schemas/Capsule"
"409":
description: Capsule 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 capsule, takes a full snapshot, copies the snapshot
files to the images directory as a reusable template, then destroys
the capsule. The template can be used to create new capsulees.
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 capsule 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/capsules/{id}/files/write:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Upload a file
operationId: uploadFile
tags: [capsules]
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 capsule
file:
type: string
format: binary
description: File content
responses:
"204":
description: File uploaded
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"413":
description: File too large
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/files/read:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Download a file
operationId: downloadFile
tags: [capsules]
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: Capsule or file not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/files/list:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: List directory contents
operationId: listDir
tags: [capsules]
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: Capsule not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/files/mkdir:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Create a directory
operationId: makeDir
tags: [capsules]
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: Capsule not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/files/remove:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Remove a file or directory
operationId: removePath
tags: [capsules]
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RemoveRequest"
responses:
"204":
description: File or directory removed
"404":
description: Capsule not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/exec/stream:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
summary: Stream command execution via WebSocket
operationId: execStream
tags: [capsules]
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: Capsule not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/pty:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
summary: Interactive PTY session via WebSocket
operationId: ptySession
tags: [capsules]
security:
- apiKeyAuth: []
description: |
Opens a WebSocket connection for an interactive PTY (terminal) session.
Supports creating new sessions, sending input, resizing, killing, and
reconnecting to existing sessions.
**Client sends** (first message — start a new PTY):
```json
{
"type": "start",
"cmd": "/bin/bash",
"args": [],
"cols": 80,
"rows": 24,
"envs": {"TERM": "xterm-256color"},
"cwd": "/home/user",
"user": "user"
}
```
All fields except `type` are optional. Defaults: cmd="/bin/bash", cols=80, rows=24.
**Client sends** (first message — reconnect to existing PTY):
```json
{"type": "connect", "tag": "pty-abc123de"}
```
**Client sends** (after session is established):
```json
{"type": "input", "data": "<base64-encoded bytes>"}
{"type": "resize", "cols": 120, "rows": 40}
{"type": "kill"}
```
**Server sends**:
```json
{"type": "started", "tag": "pty-abc123de", "pid": 42}
{"type": "output", "data": "<base64-encoded PTY bytes>"}
{"type": "exit", "exit_code": 0}
{"type": "error", "data": "description", "fatal": true}
{"type": "ping"}
```
PTY data (input and output) is base64-encoded because it contains raw
terminal bytes (escape sequences, control codes) that are not valid UTF-8.
Sessions have a 120-second inactivity timeout (reset on input/resize).
Sessions persist across WebSocket disconnections — the process keeps
running in the capsule. Use the `tag` from the "started" response to
reconnect later.
responses:
"101":
description: WebSocket upgrade
"404":
description: Capsule not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/files/stream/write:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Upload a file (streaming)
operationId: streamUploadFile
tags: [capsules]
security:
- apiKeyAuth: []
description: |
Streams file content to the capsule 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 capsule
file:
type: string
format: binary
description: File content
responses:
"204":
description: File uploaded
"404":
description: Capsule not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Capsule not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/capsules/{id}/files/stream/read:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Download a file (streaming)
operationId: streamDownloadFile
tags: [capsules]
security:
- apiKeyAuth: []
description: |
Streams file content from the capsule 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: Capsule or file not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Capsule 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 capsulees. With `?force=true`, destroys all capsulees first.
parameters:
- name: force
in: query
required: false
schema:
type: boolean
description: If true, destroy all capsulees 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 capsulees (only when force is not set)
content:
application/json:
schema:
$ref: "#/components/schemas/HostHasCapsulesError"
/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 capsule 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 capsule 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
CreateCapsuleRequest:
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 capsule is automatically paused
after this duration of inactivity (no exec or ping). 0 means
no auto-pause.
CapsuleStats:
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
Capsule:
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 capsule 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 capsule
ListDirRequest:
type: object
required: [path]
properties:
path:
type: string
description: Directory path inside the capsule
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 capsule
MakeDirResponse:
type: object
properties:
entry:
$ref: "#/components/schemas/FileEntry"
RemoveRequest:
type: object
required: [path]
properties:
path:
type: string
description: Path to remove inside the capsule
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 capsulees that would be destroyed on force-delete.
HostHasCapsulesError:
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 capsulees 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"
CapsuleMetrics:
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