All checks were successful
ci/woodpecker/push/unit Pipeline was successful
Strip session-only routes (auth, me, teams, hosts, channels, admin, audit-logs) from api/openapi.yaml via new scripts/prune_openapi.py, which is wired into `make generate` and `make prune-spec`. Spec drops from 4787 to 1717 lines; regenerated _generated.py drops from 906 to 415 lines with all obsolete models removed. Add SDK methods on CapsulesResource / AsyncCapsulesResource for the apiKey-usable routes that were previously unwrapped: - capsules.stats(range=...) -> CapsuleStats - capsules.usage(from_=..., to=...) -> UsageResponse (accepts date) - capsules.metrics(id, range=...) -> CapsuleMetrics Add EventsResource / AsyncEventsResource exposing WrennClient.events.stream() over /v1/events/stream (SSE). Parser handles `:keepalive` comments and multi-line `data:` frames. Re-export new models (CapsuleStats, CapsuleMetrics, MetricPoint, UsageResponse, SSEEvent, Resource, Actor) from wrenn.models. Drop email-validator runtime dep (no EmailStr remains). Bump version to 0.3.0 (pyproject + __version__). Add pyyaml to dev deps and types-PyYAML to the pre-commit mypy hook for the prune script. Normalize docstrings on new methods/classes to Google style with Example:: blocks where useful. Regenerate docs/reference.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1718 lines
46 KiB
YAML
1718 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** (after session is established):\n```json\n{\"type\": \"input\", \"data\": \"<base64-encoded bytes>\"}\n\
|
|
{\"type\": \"resize\", \"cols\": 120, \"rows\": 40}\n{\"type\": \"kill\"}\n```\n\n**Server sends**:\n```json\n{\"\
|
|
type\": \"started\", \"tag\": \"pty-abc123de\", \"pid\": 42}\n{\"type\": \"output\", \"data\": \"<base64-encoded PTY\
|
|
\ bytes>\"}\n{\"type\": \"exit\", \"exit_code\": 0}\n{\"type\": \"error\", \"data\": \"description\", \"fatal\": true}\n\
|
|
{\"type\": \"ping\"}\n```\n\nPTY data (input and output) is base64-encoded because it contains raw\nterminal bytes\
|
|
\ (escape sequences, control codes) that are not valid UTF-8.\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
|