All checks were successful
ci/woodpecker/push/unit Pipeline was successful
PTY input and output now travel as raw WebSocket binary frames to match the updated server protocol. Control messages stay as JSON.
1722 lines
46 KiB
YAML
1722 lines
46 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: Wrenn API
|
|
description: API for Wrenn — the runtime where AI coding agents live, work, and ship.
|
|
version: 0.2.2
|
|
servers:
|
|
- url: http://localhost:8080
|
|
description: Local development
|
|
security: []
|
|
paths:
|
|
/v1/capsules:
|
|
post:
|
|
summary: Create a capsule
|
|
operationId: createCapsule
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateCapsuleRequest'
|
|
responses:
|
|
'202':
|
|
description: Capsule creation initiated (status will be "starting")
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Capsule'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'409':
|
|
$ref: '#/components/responses/FailedPrecondition'
|
|
'502':
|
|
description: Host agent error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
get:
|
|
summary: List capsules for your team
|
|
operationId: listCapsules
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
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: []
|
|
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: []
|
|
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: []
|
|
responses:
|
|
'200':
|
|
description: Capsule details
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Capsule'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'404':
|
|
description: Capsule not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
delete:
|
|
summary: Destroy a capsule
|
|
operationId: destroyCapsule
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
responses:
|
|
'202':
|
|
description: Capsule destruction initiated
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/FailedPrecondition'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/capsules/{id}/exec:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: Execute a command
|
|
operationId: execCommand
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ExecRequest'
|
|
responses:
|
|
'200':
|
|
description: Command output (foreground exec)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ExecResponse'
|
|
'202':
|
|
description: Background process started
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/BackgroundExecResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/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: []
|
|
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'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/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: []
|
|
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
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/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: []
|
|
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: []
|
|
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
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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: []
|
|
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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/capsules/{id}/pause:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: Pause a running capsule
|
|
operationId: pauseCapsule
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
description: 'Takes a snapshot of the capsule (VM state + memory + rootfs), then
|
|
|
|
destroys all running resources. The capsule exists only as files on
|
|
|
|
disk and can be resumed later.
|
|
|
|
'
|
|
responses:
|
|
'202':
|
|
description: Capsule pause initiated (status will be "pausing")
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Capsule'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
description: Capsule not running
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/capsules/{id}/resume:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: Resume a paused capsule
|
|
operationId: resumeCapsule
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
description: 'Restores a paused capsule from its snapshot. 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'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
description: Capsule not paused
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/snapshots:
|
|
post:
|
|
summary: Create a snapshot template
|
|
operationId: createSnapshot
|
|
tags:
|
|
- snapshots
|
|
security:
|
|
- apiKeyAuth: []
|
|
description: 'Snapshot a capsule, processed asynchronously. The call returns
|
|
|
|
immediately with the capsule in the `snapshotting` state, then it
|
|
|
|
returns to its original state on completion. The capsule must be
|
|
|
|
`running` or `paused`.
|
|
|
|
|
|
A `running` capsule is snapshotted live: it briefly pauses while its VM
|
|
|
|
state + memory + flattened rootfs are written to a new template, then
|
|
|
|
resumes to `running`. A `paused` capsule is snapshotted directly from
|
|
|
|
its on-disk state without reviving the VM, and stays `paused`.
|
|
|
|
|
|
Because it is async, the response does NOT contain the template. Watch
|
|
|
|
for the `template.snapshot.create` SSE event (its `outcome` reports
|
|
|
|
success or failure) or poll `GET /v1/snapshots` to observe completion.
|
|
|
|
|
|
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:
|
|
'202':
|
|
description: Snapshot accepted; capsule is now snapshotting
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Capsule'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
description: Name already exists, or capsule is not running or paused
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
get:
|
|
summary: List templates for your team
|
|
operationId: listSnapshots
|
|
tags:
|
|
- snapshots
|
|
security:
|
|
- apiKeyAuth: []
|
|
parameters:
|
|
- name: type
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- base
|
|
- snapshot
|
|
description: Filter by template type.
|
|
responses:
|
|
'200':
|
|
description: List of templates
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Template'
|
|
/v1/snapshots/{name}:
|
|
parameters:
|
|
- name: name
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
delete:
|
|
summary: Delete a snapshot template
|
|
operationId: deleteSnapshot
|
|
tags:
|
|
- snapshots
|
|
security:
|
|
- apiKeyAuth: []
|
|
description: Removes the snapshot files from disk and deletes the database record.
|
|
responses:
|
|
'204':
|
|
description: Snapshot deleted
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'403':
|
|
description: Cannot delete a platform-owned / system base template
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
'404':
|
|
description: Template not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
'409':
|
|
description: Delete failed (e.g. owning host offline)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
/v1/capsules/{id}/files/write:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: Upload a file
|
|
operationId: uploadFile
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- path
|
|
- file
|
|
properties:
|
|
path:
|
|
type: string
|
|
description: Absolute destination path inside the capsule
|
|
file:
|
|
type: string
|
|
format: binary
|
|
description: File content
|
|
responses:
|
|
'204':
|
|
description: File uploaded
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/capsules/{id}/files/read:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: Download a file
|
|
operationId: downloadFile
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ReadFileRequest'
|
|
responses:
|
|
'200':
|
|
description: File content
|
|
content:
|
|
application/octet-stream:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/capsules/{id}/files/list:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: List directory contents
|
|
operationId: listDir
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ListDirRequest'
|
|
responses:
|
|
'200':
|
|
description: Directory listing
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ListDirResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/capsules/{id}/files/mkdir:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: Create a directory
|
|
operationId: makeDir
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/MakeDirRequest'
|
|
responses:
|
|
'200':
|
|
description: Directory created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/MakeDirResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/capsules/{id}/files/remove:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: Remove a file or directory
|
|
operationId: removePath
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RemoveRequest'
|
|
responses:
|
|
'204':
|
|
description: File or directory removed
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/capsules/{id}/exec/stream:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Stream command execution via WebSocket
|
|
operationId: execStream
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
description: 'Opens a WebSocket connection for streaming command execution.
|
|
|
|
|
|
**Client sends** (first message to start the process):
|
|
|
|
```json
|
|
|
|
{"type": "start", "cmd": "tail", "args": ["-f", "/var/log/syslog"]}
|
|
|
|
```
|
|
|
|
|
|
**Client sends** (to stop the process):
|
|
|
|
```json
|
|
|
|
{"type": "stop"}
|
|
|
|
```
|
|
|
|
|
|
**Server sends** (process events as they arrive):
|
|
|
|
```json
|
|
|
|
{"type": "start", "pid": 1234}
|
|
|
|
{"type": "stdout", "data": "line of output\n"}
|
|
|
|
{"type": "stderr", "data": "warning message\n"}
|
|
|
|
{"type": "exit", "exit_code": 0}
|
|
|
|
{"type": "error", "data": "description of error"}
|
|
|
|
```
|
|
|
|
|
|
The connection closes automatically after the process exits.
|
|
|
|
|
|
Note: capsule-not-found and not-running conditions encountered after the
|
|
|
|
WebSocket upgrade are delivered as in-band `{"type": "error"}` frames
|
|
|
|
rather than HTTP status codes.
|
|
|
|
'
|
|
responses:
|
|
'101':
|
|
description: WebSocket upgrade
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'404':
|
|
description: Capsule not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
'409':
|
|
description: Capsule not running
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
/v1/capsules/{id}/pty:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Interactive PTY session via WebSocket
|
|
operationId: ptySession
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
description: "Opens a WebSocket connection for an interactive PTY (terminal) session.\nSupports creating new sessions,\
|
|
\ sending input, resizing, killing, and\nreconnecting to existing sessions.\n\n**Client sends** (first message — start\
|
|
\ a new PTY):\n```json\n{\n \"type\": \"start\",\n \"cmd\": \"/bin/bash\",\n \"args\": [],\n \"cols\": 80,\n \
|
|
\ \"rows\": 24,\n \"envs\": {\"TERM\": \"xterm-256color\"},\n \"cwd\": \"/home/user\",\n \"user\": \"user\"\n}\n\
|
|
```\nAll fields except `type` are optional. Defaults: cmd=\"/bin/bash\", cols=80, rows=24.\n\n**Client sends** (first\
|
|
\ message — reconnect to existing PTY):\n```json\n{\"type\": \"connect\", \"tag\": \"pty-abc123de\"}\n```\n\n**Client\
|
|
\ sends** (control messages, after session is established — JSON text frames):\n```json\n{\"type\": \"resize\", \"\
|
|
cols\": 120, \"rows\": 40}\n{\"type\": \"kill\"}\n```\n\nKeystroke input is sent as **raw binary WebSocket frames**\
|
|
\ containing the\nterminal bytes verbatim — not JSON, not base64. Every binary frame from\nthe client is treated as\
|
|
\ PTY input.\n\n**Server sends** (control messages — JSON text frames):\n```json\n{\"type\": \"started\", \"tag\"\
|
|
: \"pty-abc123de\", \"pid\": 42}\n{\"type\": \"exit\", \"exit_code\": 0}\n{\"type\": \"error\", \"data\": \"description\"\
|
|
, \"fatal\": true}\n{\"type\": \"ping\"}\n```\n\nPTY output is sent as **raw binary WebSocket frames** containing\
|
|
\ the\nterminal bytes verbatim — not JSON, not base64. Sending input and output\nas binary avoids the ~33% inflation\
|
|
\ of base64 over the JSON path.\n\nThe connection negotiates permessage-deflate (RFC 7692) compression\nautomatically;\
|
|
\ rapid output (e.g. full-screen TUI repaints) is coalesced\ninto fewer, larger frames before compression.\n\nSessions\
|
|
\ persist across WebSocket disconnections — the process keeps\nrunning in the capsule. Use the `tag` from the \"started\"\
|
|
\ response to\nreconnect later.\n"
|
|
responses:
|
|
'101':
|
|
description: WebSocket upgrade
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'404':
|
|
description: Capsule not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
'409':
|
|
description: Capsule not running
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
/v1/capsules/{id}/files/stream/write:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: Upload a file (streaming)
|
|
operationId: streamUploadFile
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
description: 'Streams file content to the capsule without buffering in memory.
|
|
|
|
Suitable for large files. Uses the same multipart/form-data format
|
|
|
|
as the non-streaming upload endpoint.
|
|
|
|
'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- path
|
|
- file
|
|
properties:
|
|
path:
|
|
type: string
|
|
description: Absolute destination path inside the capsule
|
|
file:
|
|
type: string
|
|
format: binary
|
|
description: File content
|
|
responses:
|
|
'204':
|
|
description: File uploaded
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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'
|
|
'502':
|
|
description: Host agent error while streaming the upload
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/v1/capsules/{id}/files/stream/read:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
post:
|
|
summary: Download a file (streaming)
|
|
operationId: streamDownloadFile
|
|
tags:
|
|
- capsules
|
|
security:
|
|
- apiKeyAuth: []
|
|
description: 'Streams file content from the capsule without buffering in memory.
|
|
|
|
Suitable for large files. Returns raw bytes with chunked transfer encoding.
|
|
|
|
'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ReadFileRequest'
|
|
responses:
|
|
'200':
|
|
description: File content streamed in chunks
|
|
content:
|
|
application/octet-stream:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'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'
|
|
'503':
|
|
$ref: '#/components/responses/ServiceUnavailable'
|
|
/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: []
|
|
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'
|
|
components:
|
|
responses:
|
|
BadRequest:
|
|
description: Invalid request parameters
|
|
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'
|
|
ServiceUnavailable:
|
|
description: The host serving this capsule is unreachable (host_unavailable)
|
|
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.
|
|
schemas:
|
|
CreateCapsuleRequest:
|
|
type: object
|
|
properties:
|
|
template:
|
|
type: string
|
|
default: minimal-ubuntu
|
|
vcpus:
|
|
type: integer
|
|
default: 1
|
|
memory_mb:
|
|
type: integer
|
|
default: 512
|
|
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).
|
|
|
|
'
|
|
metadata:
|
|
type: object
|
|
additionalProperties:
|
|
type: string
|
|
nullable: true
|
|
description: 'Optional user-supplied key/value labels attached to the capsule. Max 20 keys; keys match [a-zA-Z0-9][a-zA-Z0-9._-]{0,63};
|
|
values are at most 64 characters. Reserved system keys (kernel_version, vmm_version, agent_version, envd_version)
|
|
are rejected. Metadata is set at create-time only.
|
|
|
|
'
|
|
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
|
|
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
|
|
- snapshotting
|
|
- resuming
|
|
- stopping
|
|
- hibernated
|
|
- stopped
|
|
- missing
|
|
- error
|
|
template:
|
|
type: string
|
|
vcpus:
|
|
type: integer
|
|
memory_mb:
|
|
type: integer
|
|
timeout_sec:
|
|
type: integer
|
|
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: 'User-supplied key/value labels attached at create-time. Once the
|
|
|
|
capsule is running this also carries agent-side system version info
|
|
|
|
(kernel_version, vmm_version, agent_version, envd_version); those
|
|
|
|
reserved keys are owned by the system and cannot be set by users.
|
|
|
|
'
|
|
disk_size_mb:
|
|
type: integer
|
|
description: Maximum disk capacity in MiB.
|
|
disk_used_mb:
|
|
type: integer
|
|
format: int64
|
|
description: Current disk usage in MiB. Only populated on individual capsule GET; omitted in list responses.
|
|
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-ubuntu` rootfs). False for team-owned
|
|
|
|
snapshot templates.
|
|
|
|
'
|
|
protected:
|
|
type: boolean
|
|
description: 'True for built-in system base templates (minimal-ubuntu,
|
|
|
|
minimal-alpine, minimal-arch, minimal-fedora). Protected templates
|
|
|
|
cannot be deleted.
|
|
|
|
'
|
|
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 (applies to both foreground and background exec)
|
|
cwd:
|
|
type: string
|
|
description: Working directory for the process (applies to both foreground and background exec)
|
|
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
|
|
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
|
|
Error:
|
|
type: object
|
|
properties:
|
|
error:
|
|
type: object
|
|
properties:
|
|
code:
|
|
type: string
|
|
message:
|
|
type: string
|
|
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
|