Add sandbox filesystem methods (list_dir, mkdir, remove, upload, download, stream_upload, stream_download) and interactive PTY sessions (PtySession, AsyncPtySession) with reconnect support per FILE_TERMINAL.md spec. Refactor error handling into exceptions.py as shared handle_response(). Replace API-key-only proxy auth with unified _proxy_headers() supporting both API key and JWT. Fix stream_upload to build multipart manually instead of relying on httpx files= with generators. Switch Makefile SPEC_URL from main to dev branch. Regenerate models from updated OpenAPI spec (adds teams, channels, metrics, PTY endpoints). Add comprehensive unit and integration tests. Trim AGENTS.md to verified facts only.
2622 lines
68 KiB
YAML
2622 lines
68 KiB
YAML
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}/pty:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
|
|
get:
|
|
summary: Interactive PTY session via WebSocket
|
|
operationId: ptySession
|
|
tags: [sandboxes]
|
|
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 sandbox. Use the `tag` from the "started" response to
|
|
reconnect later.
|
|
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
|