1742 lines
47 KiB
YAML
1742 lines
47 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
|