All checks were successful
ci/woodpecker/push/unit Pipeline was successful
## What's New? - Updated the SDK to support v0.2.0 - Improved the test suite - Minor bugfix - No breaking changes Co-authored-by: Tasnim Kabir Sadik <tksadik92@gmail.com> Reviewed-on: #9 Co-authored-by: pptx704 <rafeed@omukk.dev> Co-committed-by: pptx704 <rafeed@omukk.dev>
4309 lines
116 KiB
YAML
4309 lines
116 KiB
YAML
openapi: "3.1.0"
|
|
info:
|
|
title: Wrenn API
|
|
description: AI agent execution platform API.
|
|
version: "0.2.0"
|
|
|
|
servers:
|
|
- url: http://localhost:8080
|
|
description: Local development
|
|
|
|
security: []
|
|
|
|
paths:
|
|
/v1/auth/signup:
|
|
post:
|
|
summary: Create a new account
|
|
operationId: signup
|
|
tags: [auth]
|
|
description: |
|
|
Creates an inactive user account and sends an activation email.
|
|
The user must activate their account within 30 minutes.
|
|
Does not return a JWT — the user must activate first, then sign in.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SignupRequest"
|
|
responses:
|
|
"201":
|
|
description: Account created, activation email sent
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SignupResponse"
|
|
"400":
|
|
description: Invalid request (bad email, short password)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
"409":
|
|
description: Email already registered or signup cooldown active
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/v1/auth/activate:
|
|
post:
|
|
summary: Activate account via email token
|
|
operationId: activate
|
|
tags: [auth]
|
|
description: |
|
|
Consumes the activation token sent via email and activates the user account.
|
|
Creates a default team and sets a session cookie to log the user in.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [token]
|
|
properties:
|
|
token:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Account activated, session cookie set
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SessionResponse"
|
|
"400":
|
|
description: Invalid or expired token
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/v1/auth/logout:
|
|
post:
|
|
summary: Revoke the current session
|
|
operationId: logout
|
|
tags: [auth]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"204":
|
|
description: Session revoked; cookies cleared
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
|
|
/v1/auth/logout-all:
|
|
post:
|
|
summary: Revoke every session for the current user
|
|
operationId: logoutAll
|
|
tags: [auth]
|
|
description: |
|
|
Revokes every active session for the calling user across all devices,
|
|
including the caller's own. Returns 204 and clears cookies on the
|
|
response. Triggered automatically by password change, password add,
|
|
and password reset.
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"204":
|
|
description: All sessions revoked
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
|
|
/v1/me/sessions:
|
|
get:
|
|
summary: List the caller's active sessions
|
|
operationId: listSessions
|
|
tags: [me]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: Sessions list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
sessions:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
user_agent:
|
|
type: string
|
|
ip_address:
|
|
type: string
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
last_seen_at:
|
|
type: string
|
|
format: date-time
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
current:
|
|
type: boolean
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
|
|
/v1/me/sessions/{id}:
|
|
delete:
|
|
summary: Revoke a single session
|
|
operationId: revokeSession
|
|
tags: [me]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"204":
|
|
description: Session revoked
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
|
|
/v1/auth/switch-team:
|
|
post:
|
|
summary: Switch active team
|
|
operationId: switchTeam
|
|
tags: [auth]
|
|
security:
|
|
- sessionAuth: []
|
|
description: |
|
|
Rotates the session SID and updates its team scope. The user must be a
|
|
member of the target team (verified from DB). The new wrenn_sid and
|
|
wrenn_csrf cookies are set on the response.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [team_id]
|
|
properties:
|
|
team_id:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: New session issued for the target team; cookies refreshed
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SessionResponse"
|
|
"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/SessionResponse"
|
|
"401":
|
|
description: Invalid credentials
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/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"
|
|
|
|
/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, sets the wrenn_sid + wrenn_csrf cookies, and
|
|
redirects to the SPA callback page.
|
|
|
|
**On success:** redirects to `{OAUTH_REDIRECT_URL}/auth/{provider}/callback` (no tokens in URL).
|
|
|
|
**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/me:
|
|
get:
|
|
summary: Get current user profile
|
|
operationId: getMe
|
|
tags: [account]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: User profile
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/MeResponse"
|
|
|
|
patch:
|
|
summary: Update display name
|
|
operationId: updateName
|
|
tags: [account]
|
|
security:
|
|
- sessionAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
responses:
|
|
"204":
|
|
description: Name updated; session caches refreshed
|
|
"400":
|
|
description: Invalid name
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
delete:
|
|
summary: Delete current account
|
|
operationId: deleteAccount
|
|
tags: [account]
|
|
security:
|
|
- sessionAuth: []
|
|
description: |
|
|
Soft-deletes the account (sets status=deleted, deleted_at=now).
|
|
The account is permanently removed after 15 days. Blocked if the user
|
|
owns any team that has other members.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [confirmation]
|
|
properties:
|
|
confirmation:
|
|
type: string
|
|
description: Must match the user's email address (case-insensitive)
|
|
responses:
|
|
"204":
|
|
description: Account scheduled for deletion
|
|
"400":
|
|
description: Confirmation does not match email
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
"409":
|
|
description: User owns teams with other members
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/v1/me/password:
|
|
post:
|
|
summary: Change or add password
|
|
operationId: changePassword
|
|
tags: [account]
|
|
security:
|
|
- sessionAuth: []
|
|
description: |
|
|
For users with an existing password: requires `current_password` and `new_password`.
|
|
For OAuth-only users adding a password: requires `new_password` and `confirm_password`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ChangePasswordRequest"
|
|
responses:
|
|
"204":
|
|
description: Password updated
|
|
"400":
|
|
description: Invalid request (short password, mismatch, etc.)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
"401":
|
|
description: Current password is incorrect
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/v1/me/password/reset:
|
|
post:
|
|
summary: Request a password reset email
|
|
operationId: requestPasswordReset
|
|
tags: [account]
|
|
description: |
|
|
Sends a password reset link to the given email. Always returns 200
|
|
regardless of whether the email exists, to prevent account enumeration.
|
|
The reset token expires in 15 minutes.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [email]
|
|
properties:
|
|
email:
|
|
type: string
|
|
format: email
|
|
responses:
|
|
"204":
|
|
description: Request accepted (email sent if account exists)
|
|
|
|
/v1/me/password/reset/confirm:
|
|
post:
|
|
summary: Confirm password reset
|
|
operationId: confirmPasswordReset
|
|
tags: [account]
|
|
description: |
|
|
Consumes a password reset token and sets a new password. The token is
|
|
single-use and expires after 15 minutes.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [token, new_password]
|
|
properties:
|
|
token:
|
|
type: string
|
|
description: Raw reset token from the email link
|
|
new_password:
|
|
type: string
|
|
minLength: 8
|
|
responses:
|
|
"204":
|
|
description: Password reset successful
|
|
"400":
|
|
description: Invalid or expired token, or password too short
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/v1/me/providers/{provider}/connect:
|
|
parameters:
|
|
- name: provider
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum: [github]
|
|
description: OAuth provider name
|
|
|
|
get:
|
|
summary: Initiate OAuth provider link
|
|
operationId: connectProvider
|
|
tags: [account]
|
|
security:
|
|
- sessionAuth: []
|
|
description: |
|
|
Sets OAuth state and link cookies, then returns the provider's
|
|
authorization URL. The frontend navigates to this URL to start the
|
|
OAuth flow. On callback, the provider is linked to the current account
|
|
(not a new registration).
|
|
responses:
|
|
"200":
|
|
description: Authorization URL
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
auth_url:
|
|
type: string
|
|
format: uri
|
|
"404":
|
|
description: Provider not found or not configured
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/v1/me/providers/{provider}:
|
|
parameters:
|
|
- name: provider
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum: [github]
|
|
description: OAuth provider name
|
|
|
|
delete:
|
|
summary: Disconnect an OAuth provider
|
|
operationId: disconnectProvider
|
|
tags: [account]
|
|
security:
|
|
- sessionAuth: []
|
|
description: |
|
|
Unlinks the OAuth provider from the current account. Blocked if this
|
|
is the user's only login method (no password and no other providers).
|
|
responses:
|
|
"204":
|
|
description: Provider disconnected
|
|
"400":
|
|
description: Cannot disconnect last login method
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
"404":
|
|
description: Provider not connected
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/v1/api-keys:
|
|
post:
|
|
summary: Create an API key
|
|
operationId: createAPIKey
|
|
tags: [api-keys]
|
|
security:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
responses:
|
|
"204":
|
|
description: API key deleted
|
|
|
|
/v1/users/search:
|
|
get:
|
|
summary: Search users by email prefix
|
|
operationId: searchUsers
|
|
tags: [users]
|
|
security:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
description: |
|
|
Owner only. Soft-deletes the team and destroys all running/paused/starting
|
|
capsules. 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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateCapsuleRequest"
|
|
responses:
|
|
"202":
|
|
description: Capsule creation initiated (status will be "starting")
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Capsule"
|
|
"502":
|
|
description: Host agent error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
get:
|
|
summary: List capsules for your team
|
|
operationId: listCapsules
|
|
tags: [capsules]
|
|
security:
|
|
- apiKeyAuth: []
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: List of capsules
|
|
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: []
|
|
- sessionAuth: []
|
|
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/usage:
|
|
get:
|
|
summary: Get daily CPU and RAM usage for your team
|
|
operationId: getCapsuleUsage
|
|
tags: [capsules]
|
|
security:
|
|
- apiKeyAuth: []
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: from
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
description: Start date (YYYY-MM-DD). Defaults to 30 days ago.
|
|
- name: to
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
description: End date (YYYY-MM-DD). Defaults to today.
|
|
responses:
|
|
"200":
|
|
description: Daily usage data for the team
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UsageResponse"
|
|
"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: []
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
responses:
|
|
"202":
|
|
description: Capsule destruction initiated
|
|
|
|
/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: []
|
|
- sessionAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ExecRequest"
|
|
responses:
|
|
"200":
|
|
description: Command output (foreground exec)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ExecResponse"
|
|
"202":
|
|
description: Background process started
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BackgroundExecResponse"
|
|
"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}/processes:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
|
|
get:
|
|
summary: List running processes
|
|
operationId: listProcesses
|
|
tags: [capsules]
|
|
security:
|
|
- apiKeyAuth: []
|
|
- sessionAuth: []
|
|
description: |
|
|
Returns all running processes inside the capsule, including background
|
|
processes and any processes started by templates or init scripts.
|
|
responses:
|
|
"200":
|
|
description: Process list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ProcessListResponse"
|
|
"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}/processes/{selector}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- name: selector
|
|
in: path
|
|
required: true
|
|
description: Process PID (numeric) or tag (string)
|
|
schema:
|
|
type: string
|
|
|
|
delete:
|
|
summary: Kill a process
|
|
operationId: killProcess
|
|
tags: [capsules]
|
|
security:
|
|
- apiKeyAuth: []
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: signal
|
|
in: query
|
|
required: false
|
|
description: Signal to send (SIGKILL or SIGTERM, default SIGKILL)
|
|
schema:
|
|
type: string
|
|
enum: [SIGKILL, SIGTERM]
|
|
default: SIGKILL
|
|
responses:
|
|
"204":
|
|
description: Process killed
|
|
"404":
|
|
description: Capsule or process 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}/processes/{selector}/stream:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- name: selector
|
|
in: path
|
|
required: true
|
|
description: Process PID (numeric) or tag (string)
|
|
schema:
|
|
type: string
|
|
|
|
get:
|
|
summary: Stream process output via WebSocket
|
|
operationId: connectProcess
|
|
tags: [capsules]
|
|
security:
|
|
- apiKeyAuth: []
|
|
- sessionAuth: []
|
|
description: |
|
|
Opens a WebSocket connection to stream stdout/stderr from a running
|
|
background process. The selector can be a numeric PID or a string tag.
|
|
|
|
Server sends JSON messages:
|
|
- `{"type": "start", "pid": 42}` — connected to process
|
|
- `{"type": "stdout", "data": "..."}` — stdout output
|
|
- `{"type": "stderr", "data": "..."}` — stderr output
|
|
- `{"type": "exit", "exit_code": 0}` — process exited
|
|
- `{"type": "error", "data": "..."}` — error message
|
|
responses:
|
|
"101":
|
|
description: WebSocket upgrade
|
|
|
|
/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: []
|
|
- sessionAuth: []
|
|
description: |
|
|
Resets the last_active_at timestamp for a running capsule, preventing
|
|
the auto-pause TTL from expiring. Use this as a keepalive for capsules
|
|
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: []
|
|
- sessionAuth: []
|
|
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 capsules, data comes from the host agent's in-memory
|
|
ring buffer. For paused capsules, data is read from persisted
|
|
snapshots in the database. Stopped/destroyed capsules 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: []
|
|
- sessionAuth: []
|
|
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:
|
|
"202":
|
|
description: Capsule pause initiated (status will be "pausing")
|
|
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: []
|
|
- sessionAuth: []
|
|
description: |
|
|
Restores a paused capsule from its snapshot. Cloud Hypervisor is
|
|
relaunched in --restore mode with memory_restore_mode=ondemand so
|
|
guest pages fault in lazily via userfaultfd. The original network
|
|
slot (and host-reachable IP) is preserved across pause/resume.
|
|
responses:
|
|
"202":
|
|
description: Capsule resume initiated (status will be "resuming")
|
|
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: []
|
|
- sessionAuth: []
|
|
description: |
|
|
Live snapshot: briefly pauses the capsule, writes its VM state +
|
|
memory + flattened rootfs to a new template directory, then resumes
|
|
the capsule. The source capsule keeps running after the snapshot;
|
|
the resulting template can be used to create new capsules.
|
|
|
|
Snapshots are immutable: each call must use a fresh name. Re-using
|
|
an existing name returns 409 Conflict.
|
|
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: []
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
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, or a directory already exists at the
|
|
target path (error code `already_exists`).
|
|
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: []
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
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 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: []
|
|
- sessionAuth: []
|
|
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: []
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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 capsules. With `?force=true`, destroys all capsules first.
|
|
parameters:
|
|
- name: force
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: boolean
|
|
description: If true, destroy all capsules 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 capsules (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:
|
|
- sessionAuth: []
|
|
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/sandbox-events:
|
|
post:
|
|
summary: Sandbox lifecycle event callback
|
|
operationId: sandboxEventCallback
|
|
tags: [hosts]
|
|
security:
|
|
- hostTokenAuth: []
|
|
description: |
|
|
Receives autonomous lifecycle events from host agents (e.g. auto-pause
|
|
from the TTL reaper). The event is published to an internal Redis stream
|
|
for the control plane's event consumer to process.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [event, sandbox_id, host_id]
|
|
properties:
|
|
event:
|
|
type: string
|
|
description: |
|
|
Lifecycle event type. Known values:
|
|
* `sandbox.auto_paused` — TTL reaper paused the capsule
|
|
* `sandbox.stopped` — autonomous destroy (crash/eviction)
|
|
* `sandbox.error` — VMM/crash watcher reported error
|
|
Unknown event names are accepted and forwarded to the
|
|
stream consumer as-is (future-compatible).
|
|
sandbox_id:
|
|
type: string
|
|
host_id:
|
|
type: string
|
|
timestamp:
|
|
type: integer
|
|
format: int64
|
|
responses:
|
|
"204":
|
|
description: Event accepted
|
|
"400":
|
|
description: Invalid request
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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:
|
|
- sessionAuth: []
|
|
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"
|
|
|
|
/v1/admin/users/{id}/admin:
|
|
put:
|
|
summary: Grant or revoke platform admin
|
|
operationId: setUserAdmin
|
|
tags: [admin]
|
|
description: |
|
|
Sets the platform admin flag on a user. Cannot remove the last admin.
|
|
Requires platform admin access. Session caches for the target user
|
|
are invalidated immediately so the flag flip takes effect on the
|
|
user's next request.
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
example: "usr-a1b2c3d4"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [admin]
|
|
properties:
|
|
admin:
|
|
type: boolean
|
|
description: true to grant admin, false to revoke.
|
|
responses:
|
|
"204":
|
|
description: Admin status updated
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"403":
|
|
description: Caller is not a platform admin
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
"404":
|
|
description: User not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/v1/events/stream:
|
|
get:
|
|
summary: Real-time lifecycle event stream
|
|
operationId: streamEvents
|
|
tags: [events]
|
|
description: |
|
|
Server-Sent Events stream of capsule, template, and host lifecycle
|
|
events scoped to the caller's active team. Browsers send the
|
|
wrenn_sid cookie automatically on EventSource connections; SDKs
|
|
authenticate via X-API-Key.
|
|
|
|
Frame format follows the standard SSE protocol:
|
|
```
|
|
event: capsule.create
|
|
data: {"event":"capsule.create","outcome":"success","resource":{"id":"sb-..."},"sandbox":{...},"timestamp":"2026-05-19T02:00:00Z"}
|
|
|
|
: keepalive
|
|
```
|
|
A `: keepalive` comment is emitted every 30s.
|
|
security:
|
|
- apiKeyAuth: []
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: SSE stream opened
|
|
content:
|
|
text/event-stream:
|
|
schema:
|
|
$ref: "#/components/schemas/SSEEvent"
|
|
"401":
|
|
description: Missing or invalid auth
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
/v1/audit-logs:
|
|
get:
|
|
summary: List team audit log entries
|
|
operationId: listAuditLogs
|
|
tags: [audit]
|
|
description: Paginated cursor list of audit events for the caller's team.
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: before
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date-time
|
|
- name: before_id
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
- name: limit
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 200
|
|
default: 50
|
|
responses:
|
|
"200":
|
|
description: Audit log page
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
entries:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/AuditLogEntry"
|
|
next_cursor:
|
|
type: object
|
|
nullable: true
|
|
properties:
|
|
before:
|
|
type: string
|
|
format: date-time
|
|
before_id:
|
|
type: string
|
|
|
|
/v1/admin/events/stream:
|
|
get:
|
|
summary: Admin SSE event stream (all teams)
|
|
operationId: adminStreamEvents
|
|
tags: [admin, events]
|
|
description: |
|
|
Admin variant of /v1/events/stream that emits events across all teams.
|
|
Requires an admin session cookie.
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: SSE stream opened
|
|
content:
|
|
text/event-stream:
|
|
schema:
|
|
$ref: "#/components/schemas/SSEEvent"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
|
|
/v1/admin/audit-logs:
|
|
get:
|
|
summary: List audit log entries (all teams)
|
|
operationId: adminListAuditLogs
|
|
tags: [admin, audit]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: before
|
|
in: query
|
|
schema: {type: string, format: date-time}
|
|
- name: before_id
|
|
in: query
|
|
schema: {type: string}
|
|
- name: limit
|
|
in: query
|
|
schema: {type: integer, minimum: 1, maximum: 200, default: 50}
|
|
responses:
|
|
"200":
|
|
description: Audit log page (all teams)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
entries:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/AuditLogEntry"
|
|
|
|
/v1/admin/teams:
|
|
get:
|
|
summary: List all teams (admin)
|
|
operationId: adminListTeams
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: Teams list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: {type: object}
|
|
|
|
/v1/admin/teams/{id}/byoc:
|
|
put:
|
|
summary: Toggle BYOC for a team (admin)
|
|
operationId: adminSetTeamBYOC
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [byoc]
|
|
properties:
|
|
byoc: {type: boolean}
|
|
responses:
|
|
"204":
|
|
description: Updated
|
|
|
|
/v1/admin/teams/{id}:
|
|
delete:
|
|
summary: Delete a team (admin)
|
|
operationId: adminDeleteTeam
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
responses:
|
|
"204":
|
|
description: Deleted
|
|
|
|
/v1/admin/users:
|
|
get:
|
|
summary: List all users (admin)
|
|
operationId: adminListUsers
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: Users list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: {type: object}
|
|
|
|
/v1/admin/users/{id}/active:
|
|
put:
|
|
summary: Activate or deactivate a user (admin)
|
|
operationId: adminSetUserActive
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [active]
|
|
properties:
|
|
active: {type: boolean}
|
|
responses:
|
|
"204":
|
|
description: Updated
|
|
|
|
/v1/admin/templates:
|
|
get:
|
|
summary: List all templates (admin)
|
|
operationId: adminListTemplates
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: Templates list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Template"
|
|
|
|
/v1/admin/templates/{name}:
|
|
delete:
|
|
summary: Delete a template (admin)
|
|
operationId: adminDeleteTemplate
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: name
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
responses:
|
|
"204":
|
|
description: Deleted
|
|
|
|
/v1/admin/builds:
|
|
post:
|
|
summary: Submit a template build (admin)
|
|
operationId: adminCreateBuild
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: {type: object}
|
|
responses:
|
|
"202":
|
|
description: Build queued
|
|
content:
|
|
application/json:
|
|
schema: {type: object}
|
|
get:
|
|
summary: List builds (admin)
|
|
operationId: adminListBuilds
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: Builds list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: {type: object}
|
|
|
|
/v1/admin/builds/{id}:
|
|
get:
|
|
summary: Get build detail (admin)
|
|
operationId: adminGetBuild
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
responses:
|
|
"200":
|
|
description: Build detail
|
|
content:
|
|
application/json:
|
|
schema: {type: object}
|
|
|
|
/v1/admin/builds/{id}/cancel:
|
|
post:
|
|
summary: Cancel a build (admin)
|
|
operationId: adminCancelBuild
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
responses:
|
|
"204":
|
|
description: Cancelled
|
|
|
|
/v1/admin/capsules:
|
|
post:
|
|
summary: Create a capsule on behalf of any team (admin)
|
|
operationId: adminCreateCapsule
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateCapsuleRequest"
|
|
responses:
|
|
"201":
|
|
description: Capsule created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Capsule"
|
|
get:
|
|
summary: List capsules across all teams (admin)
|
|
operationId: adminListCapsules
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: Capsules list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Capsule"
|
|
|
|
/v1/admin/capsules/{id}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
get:
|
|
summary: Get capsule detail (admin)
|
|
operationId: adminGetCapsule
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: Capsule detail
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Capsule"
|
|
delete:
|
|
summary: Destroy capsule (admin)
|
|
operationId: adminDestroyCapsule
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"204":
|
|
description: Destroyed
|
|
|
|
/v1/admin/capsules/{id}/snapshot:
|
|
post:
|
|
summary: Create snapshot from any capsule (admin)
|
|
operationId: adminCreateSnapshotFromCapsule
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: {type: string}
|
|
responses:
|
|
"201":
|
|
description: Snapshot created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Template"
|
|
|
|
/v1/admin/capsules/{id}/exec:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
post:
|
|
summary: Execute a command on any capsule (admin)
|
|
operationId: adminExecCommand
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ExecRequest"
|
|
responses:
|
|
"200":
|
|
description: Command output (foreground exec)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ExecResponse"
|
|
"202":
|
|
description: Background process started
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BackgroundExecResponse"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/FailedPrecondition"
|
|
|
|
/v1/admin/capsules/{id}/metrics:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
get:
|
|
summary: Get per-capsule resource metrics (admin)
|
|
operationId: adminGetCapsuleMetrics
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: range
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
enum: ["5m", "10m", "1h", "2h", "6h", "12h", "24h"]
|
|
default: "10m"
|
|
responses:
|
|
"200":
|
|
description: Metrics retrieved
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CapsuleMetrics"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
/v1/admin/capsules/{id}/processes:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
get:
|
|
summary: List running processes on any capsule (admin)
|
|
operationId: adminListProcesses
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"200":
|
|
description: Process list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ProcessListResponse"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/FailedPrecondition"
|
|
|
|
/v1/admin/capsules/{id}/processes/{selector}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
- name: selector
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
description: Process PID (numeric) or tag (string)
|
|
delete:
|
|
summary: Kill a process on any capsule (admin)
|
|
operationId: adminKillProcess
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: signal
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
enum: [SIGKILL, SIGTERM]
|
|
default: SIGKILL
|
|
responses:
|
|
"204":
|
|
description: Process killed
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/FailedPrecondition"
|
|
|
|
/v1/admin/capsules/{id}/files/write:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
post:
|
|
summary: Upload a file to any capsule (admin)
|
|
operationId: adminUploadFile
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [path, file]
|
|
properties:
|
|
path: {type: string}
|
|
file: {type: string, format: binary}
|
|
responses:
|
|
"204":
|
|
description: File uploaded
|
|
"409":
|
|
$ref: "#/components/responses/FailedPrecondition"
|
|
|
|
/v1/admin/capsules/{id}/files/read:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
post:
|
|
summary: Download a file from any capsule (admin)
|
|
operationId: adminDownloadFile
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
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":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
/v1/admin/capsules/{id}/files/list:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
post:
|
|
summary: List directory contents on any capsule (admin)
|
|
operationId: adminListDir
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
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":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/FailedPrecondition"
|
|
|
|
/v1/admin/capsules/{id}/files/mkdir:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
post:
|
|
summary: Create a directory on any capsule (admin)
|
|
operationId: adminMakeDir
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
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":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/FailedPrecondition"
|
|
|
|
/v1/admin/capsules/{id}/files/remove:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
post:
|
|
summary: Remove a file or directory on any capsule (admin)
|
|
operationId: adminRemovePath
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RemoveRequest"
|
|
responses:
|
|
"204":
|
|
description: File or directory removed
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/FailedPrecondition"
|
|
|
|
/v1/admin/capsules/{id}/exec/stream:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
get:
|
|
summary: Stream command execution on any capsule via WebSocket (admin)
|
|
operationId: adminExecStream
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
description: |
|
|
Admin variant of /v1/capsules/{id}/exec/stream. Same protocol — WebSocket
|
|
upgrade, client sends `{"type":"start", "cmd":..., "args":...}` to start;
|
|
server streams stdout/stderr/exit frames.
|
|
responses:
|
|
"101":
|
|
description: WebSocket upgrade
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/FailedPrecondition"
|
|
|
|
/v1/admin/capsules/{id}/pty:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
get:
|
|
summary: Interactive PTY session on any capsule via WebSocket (admin)
|
|
operationId: adminPtySession
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
description: |
|
|
Admin variant of /v1/capsules/{id}/pty. Same protocol — base64-encoded
|
|
PTY bytes, start/connect/input/resize/kill control messages, persistent
|
|
sessions reconnectable via tag.
|
|
responses:
|
|
"101":
|
|
description: WebSocket upgrade
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/FailedPrecondition"
|
|
|
|
/v1/admin/capsules/{id}/processes/{selector}/stream:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
- name: selector
|
|
in: path
|
|
required: true
|
|
schema: {type: string}
|
|
description: Process PID (numeric) or tag (string)
|
|
get:
|
|
summary: Stream process output on any capsule via WebSocket (admin)
|
|
operationId: adminConnectProcess
|
|
tags: [admin]
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
"101":
|
|
description: WebSocket upgrade
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
components:
|
|
responses:
|
|
BadRequest:
|
|
description: Invalid request parameters
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
Unauthorized:
|
|
description: Missing or invalid auth
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
Forbidden:
|
|
description: Authenticated but not permitted (e.g. non-admin on /v1/admin/*)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
NotFound:
|
|
description: Resource not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
FailedPrecondition:
|
|
description: Resource state does not allow this operation (e.g. exec on a paused capsule)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
securitySchemes:
|
|
apiKeyAuth:
|
|
type: apiKey
|
|
in: header
|
|
name: X-API-Key
|
|
description: API key for capsule lifecycle operations. Create via POST /v1/api-keys.
|
|
|
|
sessionAuth:
|
|
type: apiKey
|
|
in: cookie
|
|
name: wrenn_sid
|
|
description: |
|
|
Opaque session cookie set by POST /v1/auth/login, /v1/auth/activate, or
|
|
the OAuth callback. HttpOnly, Secure, SameSite=Strict. Idle window 6h,
|
|
absolute lifetime 24h. State-changing requests also require an
|
|
X-CSRF-Token header matching the wrenn_csrf cookie (double-submit).
|
|
csrfHeader:
|
|
type: apiKey
|
|
in: header
|
|
name: X-CSRF-Token
|
|
description: |
|
|
Double-submit CSRF token whose value must match the wrenn_csrf cookie.
|
|
Required on all non-GET requests authenticated via session cookie.
|
|
Not required for API key auth.
|
|
|
|
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
|
|
|
|
SignupResponse:
|
|
type: object
|
|
properties:
|
|
message:
|
|
type: string
|
|
description: Confirmation message instructing user to check email
|
|
|
|
SessionResponse:
|
|
type: object
|
|
description: |
|
|
Returned by login, activate, and switch-team. The actual auth credential
|
|
is the wrenn_sid cookie set on the response. The body carries identity
|
|
data the SPA needs to bootstrap.
|
|
properties:
|
|
user_id:
|
|
type: string
|
|
team_id:
|
|
type: string
|
|
email:
|
|
type: string
|
|
name:
|
|
type: string
|
|
role:
|
|
type: string
|
|
is_admin:
|
|
type: boolean
|
|
|
|
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
|
|
disk_size_mb:
|
|
type: integer
|
|
default: 5120
|
|
description: >
|
|
Maximum size of the per-capsule copy-on-write disk in MB. Capped
|
|
at 5 GB by default; the actual size is max(disk_size_mb, origin
|
|
rootfs size).
|
|
timeout_sec:
|
|
type: integer
|
|
minimum: 0
|
|
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. Positive values below 60 are silently clamped
|
|
to 60 (the agent's startup envelope).
|
|
|
|
UsageResponse:
|
|
type: object
|
|
properties:
|
|
from:
|
|
type: string
|
|
format: date
|
|
to:
|
|
type: string
|
|
format: date
|
|
points:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
date:
|
|
type: string
|
|
format: date
|
|
cpu_minutes:
|
|
type: number
|
|
ram_mb_minutes:
|
|
type: number
|
|
|
|
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, pausing, paused, resuming, stopping, 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
|
|
metadata:
|
|
type: object
|
|
additionalProperties: {type: string}
|
|
nullable: true
|
|
description: |
|
|
Free-form key/value labels attached at create-time. Also carries
|
|
agent-side version info (kernel_version, vmm_version,
|
|
agent_version, envd_version) when running.
|
|
disk_size_mb:
|
|
type: integer
|
|
nullable: true
|
|
|
|
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
|
|
platform:
|
|
type: boolean
|
|
description: |
|
|
True when the template is platform-managed (visible to all teams,
|
|
e.g. the built-in `minimal` rootfs). False for team-owned
|
|
snapshot templates.
|
|
metadata:
|
|
type: object
|
|
additionalProperties: {type: string}
|
|
nullable: true
|
|
|
|
ExecRequest:
|
|
type: object
|
|
required: [cmd]
|
|
properties:
|
|
cmd:
|
|
type: string
|
|
args:
|
|
type: array
|
|
items:
|
|
type: string
|
|
timeout_sec:
|
|
type: integer
|
|
default: 30
|
|
description: Timeout in seconds (foreground exec only, default 30)
|
|
background:
|
|
type: boolean
|
|
default: false
|
|
description: If true, starts the process in the background and returns immediately with a PID and tag (HTTP 202)
|
|
tag:
|
|
type: string
|
|
description: Optional user-chosen tag for the background process. Auto-generated if omitted. Only used when background is true.
|
|
envs:
|
|
type: object
|
|
additionalProperties:
|
|
type: string
|
|
description: Environment variables for the process (background exec only)
|
|
cwd:
|
|
type: string
|
|
description: Working directory for the process (background exec only)
|
|
|
|
BackgroundExecResponse:
|
|
type: object
|
|
properties:
|
|
sandbox_id:
|
|
type: string
|
|
cmd:
|
|
type: string
|
|
pid:
|
|
type: integer
|
|
tag:
|
|
type: string
|
|
|
|
ProcessEntry:
|
|
type: object
|
|
properties:
|
|
pid:
|
|
type: integer
|
|
tag:
|
|
type: string
|
|
cmd:
|
|
type: string
|
|
args:
|
|
type: array
|
|
items:
|
|
type: string
|
|
|
|
ProcessListResponse:
|
|
type: object
|
|
properties:
|
|
processes:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ProcessEntry"
|
|
|
|
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 capsules 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 capsules 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 Cloud Hypervisor 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.create
|
|
- capsule.pause
|
|
- capsule.resume
|
|
- capsule.destroy
|
|
- template.snapshot.create
|
|
- template.snapshot.delete
|
|
- 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.create
|
|
- capsule.pause
|
|
- capsule.resume
|
|
- capsule.destroy
|
|
- template.snapshot.create
|
|
- template.snapshot.delete
|
|
- 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.
|
|
|
|
MeResponse:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
email:
|
|
type: string
|
|
format: email
|
|
has_password:
|
|
type: boolean
|
|
description: Whether the user has a password set (false for OAuth-only accounts)
|
|
providers:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: List of linked OAuth provider names (e.g. ["github"])
|
|
|
|
ChangePasswordRequest:
|
|
type: object
|
|
required: [new_password]
|
|
properties:
|
|
current_password:
|
|
type: string
|
|
description: Required when changing an existing password
|
|
new_password:
|
|
type: string
|
|
minLength: 8
|
|
confirm_password:
|
|
type: string
|
|
description: Required when adding a password to an OAuth-only account (must match new_password)
|
|
|
|
Error:
|
|
type: object
|
|
properties:
|
|
error:
|
|
type: object
|
|
properties:
|
|
code:
|
|
type: string
|
|
message:
|
|
type: string
|
|
|
|
AuditLogEntry:
|
|
type: object
|
|
properties:
|
|
id: {type: string}
|
|
actor_type: {type: string, enum: [user, api_key, host, system]}
|
|
actor_id: {type: string}
|
|
actor_name: {type: string}
|
|
resource_type: {type: string}
|
|
resource_id: {type: string}
|
|
action: {type: string}
|
|
scope: {type: string}
|
|
status: {type: string, enum: [success, failure]}
|
|
metadata:
|
|
type: object
|
|
additionalProperties: true
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
|
|
SSEEvent:
|
|
type: object
|
|
description: |
|
|
Wire format of one SSE message body. The event name (`event:` line) is
|
|
the `kind` and the JSON below is the `data:` line.
|
|
properties:
|
|
event:
|
|
type: string
|
|
enum:
|
|
- connected
|
|
- capsule.create
|
|
- capsule.pause
|
|
- capsule.resume
|
|
- capsule.destroy
|
|
- capsule.state.changed
|
|
- template.snapshot.create
|
|
- template.snapshot.delete
|
|
- host.up
|
|
- host.down
|
|
outcome:
|
|
type: string
|
|
enum: [success, error]
|
|
description: |
|
|
Present for action events (capsule.* except state.changed,
|
|
template.snapshot.*). Absent for host.up/down, capsule.state.changed,
|
|
and the connected sentinel.
|
|
resource:
|
|
type: object
|
|
properties:
|
|
id: {type: string}
|
|
type: {type: string}
|
|
actor:
|
|
type: object
|
|
properties:
|
|
type: {type: string, enum: [user, api_key, system]}
|
|
id: {type: string}
|
|
name: {type: string}
|
|
metadata:
|
|
type: object
|
|
additionalProperties: {type: string}
|
|
description: |
|
|
Event-specific context. Examples: `reason` (ttl_expired,
|
|
host_failure, cleanup_after_create_error, orphaned),
|
|
`host_ip`, `from`/`to` (for capsule.state.changed).
|
|
error:
|
|
type: string
|
|
description: Failure reason; only set when outcome=error.
|
|
sandbox:
|
|
allOf:
|
|
- $ref: "#/components/schemas/Capsule"
|
|
nullable: true
|
|
description: Populated for capsule.* events; null if DB lookup failed.
|
|
timestamp:
|
|
type: string
|
|
format: date-time
|