Files
sencho/docs/openapi.yaml
T
Anso 0daddfde00 fix: reconcile sticky update indicators with Anatomy preview (#1698)
* fix: reconcile sticky update indicators with Anatomy preview

Sidebar, Updates filter, and Fleet treated retained partial/failed
scanner has_update as confirmed. Keep raw state for retention/notifications,
project confirmed-only to APIs, show distinct incomplete indicators, and
clear sticky rows only after an authoritative-negative preview.

Closes #1685

* test: align sidebar truncate E2E with failed-over-retained precedence

Purple update indicators are confirmed-only; hasUpdate with a failed
check correctly shows the failed trailing icon.

* fix: clear confirmed update rows on authoritative-negative preview

Address audit SF-1/SF-2/SF-3: observation-watermark clears for older
ok+has_update rows (DB + memory gens), Fleet checkability parity with
backend not_checkable, and Updates chip confirmed-only regressions.

* fix: tombstone equal-generation writers on preview clear

Advance the per-stack write generation when clearing at the observation
watermark so a scanner reserved before preview cannot recreate the row
after an authoritative-negative reconcile.

* fix: clear sticky updates with digest and tag preview parity

Share detection across scanner and preview, keep GET read-only with POST reconcile, gate Apply to digest and rebuild updates, and invalidate the hub fleet cache on clear.

* test: set digestUpdate on auto-update checkImage mocks

Scheduler and execute routes now gate Compose on digest drift; fixtures that expect an apply need digestUpdate so they exercise the update path.

* fix: clear unused lint errors on sticky update branch

Drop unused partial helper and fleet invalidate import; keep the CacheService inflight self-ref as let with an eslint exception so tsc stays green.

* fix: use inflight holder for CacheService prefer-const

Keep generation-aware ownership without a let self-reference that fights ESLint and tsc.
2026-07-25 15:42:19 -04:00

4200 lines
141 KiB
YAML

openapi: 3.1.0
info:
title: Sencho API
version: 0.25.3
description: |
REST API for Sencho, a self-hosted Docker Compose management dashboard.
Sencho exposes a public API for automating stack deployments, managing webhooks,
monitoring fleet health, and integrating with CI/CD pipelines.
## Authentication
All authenticated endpoints accept a Bearer token in the `Authorization` header.
Generate API tokens from **Settings > API Tokens** in the Sencho dashboard (admin role required).
```
Authorization: Bearer YOUR_API_TOKEN
```
## Node Routing
Sencho manages multiple nodes (local and remote). To target a specific node,
include the `x-node-id` header or `?nodeId=` query parameter. If omitted,
the request targets the default node.
## License Tiers
Some endpoints require an Admiral license. Requests to gated endpoints
on Community return `403` with `code: "PAID_REQUIRED"`.
contact:
name: Sencho
url: https://sencho.io
license:
name: AGPL-3.0-only
url: https://github.com/studio-saelix/sencho/blob/main/LICENSE
servers:
- url: "{protocol}://{host}:{port}"
description: Your Sencho instance
variables:
protocol:
default: https
enum: [http, https]
host:
default: localhost
port:
default: "1852"
security:
- bearerAuth: []
tags:
- name: Health
description: Instance health check
- name: Stacks
description: Create, read, update, delete, and operate Docker Compose stacks
- name: Containers
description: List and manage running containers
- name: API Tokens
description: Manage scoped API tokens (admin role required)
- name: Webhooks
description: Configure and trigger deployment webhooks
- name: Nodes
description: Manage local and remote Sencho nodes
- name: Fleet
description: Multi-node fleet overview and snapshots
- name: Scheduled Tasks
description: Configure recurring automated operations (admin role required)
- name: Registries
description: Manage private container registry credentials. Docker Hub, GHCR, and custom registries are available on every tier; AWS ECR requires Admiral. These endpoints are only accessible via browser sessions. API tokens receive `SCOPE_DENIED`.
- name: Image Updates
description: Check for available container image updates across stacks
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: API token generated from Settings > API Tokens. Scopes — `read-only`, `deploy-only`, or `full-admin`.
webhookSignature:
type: apiKey
in: header
name: X-Webhook-Signature
description: HMAC-SHA256 signature for webhook trigger requests. Compute as `sha256=HMAC(request_body, webhook_secret)`.
parameters:
stackName:
name: stackName
in: path
required: true
description: >
Stack directory name. Must match `^[a-zA-Z0-9_-]+$` (alphanumeric characters,
hyphens, and underscores only). Returns `400 Invalid stack name` if the name
contains path separators, dots, spaces, or other special characters.
schema:
type: string
pattern: '^[a-zA-Z0-9_-]+$'
example: my-stack
nodeId:
name: x-node-id
in: header
required: false
description: Target a specific node by ID. If omitted, the default node is used. Can also be passed as `?nodeId=` query parameter.
schema:
type: integer
example: 1
nodeIdQuery:
name: nodeId
in: query
required: false
description: Target a specific node by ID (alternative to `x-node-id` header).
schema:
type: integer
example: 1
idPath:
name: id
in: path
required: true
description: Numeric resource ID.
schema:
type: integer
schemas:
Error:
type: object
required: [error]
properties:
error:
type: string
description: Human-readable error message.
code:
type: string
description: Machine-readable error code (e.g., `PAID_REQUIRED`, `SCOPE_DENIED`).
enum: [PAID_REQUIRED, SCOPE_DENIED]
SuccessMessage:
type: object
required: [message]
properties:
message:
type: string
SuccessBoolean:
type: object
required: [success]
properties:
success:
type: boolean
example: true
UpdatePreviewImage:
type: object
required:
[service, image, current_tag, next_tag, has_update, digest_update, tag_update, semver_bump, check_status]
properties:
service: { type: string }
image: { type: string }
current_tag: { type: string }
next_tag: { type: ["string", "null"] }
has_update: { type: boolean }
digest_update:
type: boolean
description: Same-tag registry content drift; Compose pull can apply without changing the pin.
tag_update:
type: boolean
description: A higher pinned semver exists; advisory until Compose is edited.
semver_bump:
type: string
enum: [none, unknown, patch, minor, major]
check_status:
type: string
enum: [ok, partial, failed, not_checkable]
UpdatePreviewSummary:
type: object
required:
[has_update, primary_image, current_tag, next_tag, semver_bump, update_kind, blocked, blocked_reason, has_build_services, rebuild_available, check_status]
properties:
has_update: { type: boolean }
primary_image: { type: ["string", "null"] }
current_tag: { type: ["string", "null"] }
next_tag: { type: ["string", "null"] }
semver_bump:
type: string
enum: [none, unknown, patch, minor, major]
update_kind:
type: string
enum: [tag, digest, none]
blocked: { type: boolean }
blocked_reason: { type: ["string", "null"] }
has_build_services: { type: boolean }
rebuild_available: { type: boolean }
check_status:
type: string
enum: [ok, partial, failed]
UpdatePreview:
type: object
required: [stack_name, images, build_services, summary, rollback_target, changelog]
properties:
stack_name: { type: string }
images:
type: array
items:
$ref: "#/components/schemas/UpdatePreviewImage"
build_services:
type: array
items: { type: string }
summary:
$ref: "#/components/schemas/UpdatePreviewSummary"
rollback_target: { type: ["string", "null"] }
changelog: { type: ["string", "null"] }
LabelSource:
type: string
description: Provenance of a label. `unknown` when a container or image could not be inspected.
enum: [compose, runtime, image, compose-system, unknown]
LabelValue:
type: object
required: [key, value, source]
properties:
key: { type: string }
value:
type: string
description: Redacted to `[redacted]` for secret-like keys unless the caller is an admin and passes `reveal=1`.
source: { $ref: "#/components/schemas/LabelSource" }
redacted: { type: boolean }
LabelIndexContainerRef:
type: object
required: [id, name, stack, service]
properties:
id: { type: string }
name: { type: string }
stack: { type: string, nullable: true }
service: { type: string, nullable: true }
nodeId: { type: integer }
nodeName: { type: string }
LabelIndexRow:
type: object
description: One unique key/value/source and every container carrying it.
required: [key, value, source, containers]
properties:
key: { type: string }
value: { type: string }
source: { $ref: "#/components/schemas/LabelSource" }
redacted: { type: boolean }
containers:
type: array
items: { $ref: "#/components/schemas/LabelIndexContainerRef" }
ContainerLabelRow:
type: object
required: [id, name, stack, service, state, labels]
properties:
id: { type: string }
name: { type: string }
stack: { type: string, nullable: true }
service: { type: string, nullable: true }
state: { type: string }
labels:
type: array
items: { $ref: "#/components/schemas/LabelValue" }
StackLabelReplica:
type: object
required: [id, name, state, runtimeLabels, onlyInCompose, onlyOnContainer, inBoth, changed]
properties:
id: { type: string }
name: { type: string }
state: { type: string }
runtimeLabels:
type: array
items: { $ref: "#/components/schemas/LabelValue" }
onlyInCompose: { type: array, items: { type: string } }
onlyOnContainer: { type: array, items: { type: string } }
inBoth: { type: array, items: { type: string } }
changed:
type: array
items: { type: string }
description: Keys declared in Compose and present at runtime but with a different value.
inspectFailed:
type: boolean
description: Runtime labels could not be read for this replica; reconciliation was skipped.
StackServiceLabelRow:
type: object
required: [service, declaredLabels, replicas]
properties:
service: { type: string }
declaredLabels:
type: array
items: { $ref: "#/components/schemas/LabelValue" }
replicas:
type: array
items: { $ref: "#/components/schemas/StackLabelReplica" }
StackLabelInventory:
type: object
required: [stackName, renderable, services, partial, generatedAt]
properties:
stackName: { type: string }
renderable:
type: boolean
description: False when the Compose model could not be rendered; declared provenance is then unknown.
partial:
type: boolean
description: A replica or its image could not be fully inspected.
generatedAt: { type: integer }
services:
type: array
items: { $ref: "#/components/schemas/StackServiceLabelRow" }
NodeLabelInventory:
type: object
required: [nodeId, containers, byLabel, partial, generatedAt]
properties:
nodeId: { type: integer }
partial: { type: boolean }
generatedAt: { type: integer }
containers:
type: array
items: { $ref: "#/components/schemas/ContainerLabelRow" }
byLabel:
type: array
items: { $ref: "#/components/schemas/LabelIndexRow" }
FleetLabelInventory:
type: object
required: [nodes, aggregatedByLabel, nodeErrors, generatedAt]
properties:
generatedAt: { type: integer }
nodes:
type: array
items:
type: object
required: [nodeId, nodeName, status, inventory, error]
properties:
nodeId: { type: integer }
nodeName: { type: string }
status: { type: string, enum: [ok, error] }
inventory:
nullable: true
allOf: [{ $ref: "#/components/schemas/NodeLabelInventory" }]
error: { type: string, nullable: true }
aggregatedByLabel:
type: array
items: { $ref: "#/components/schemas/LabelIndexRow" }
nodeErrors:
type: object
additionalProperties: { type: string }
description: Map of node id to error for nodes that were unreachable or returned a malformed payload.
FailureClassification:
type: object
description: Classified cause of a failed deploy or update, with a suggested next step.
required: [reason, label, suggestion]
properties:
reason:
type: string
enum:
- image_pull_failed
- compose_render_failed
- env_missing
- port_conflict
- bind_path_missing
- permission_denied
- container_exited
- healthcheck_failed
- dependency_unavailable
- node_unreachable
- unknown
label:
type: string
description: Short display headline for the cause.
suggestion:
type: string
description: Suggested next action, one sentence.
Container:
type: object
properties:
Id:
type: string
description: Docker container ID.
Names:
type: array
items:
type: string
description: Container names (prefixed with `/`).
Service:
type: string
description: Compose service name (if part of a stack).
State:
type: string
description: Container state.
enum: [running, exited, paused, restarting, dead, created]
Status:
type: string
description: Human-readable status string (e.g., "Up 2 hours").
Ports:
type: array
items:
type: object
properties:
PrivatePort:
type: integer
PublicPort:
type: integer
Node:
type: object
required: [id, name, type, compose_dir, is_default, created_at]
properties:
id:
type: integer
name:
type: string
type:
type: string
enum: [local, remote]
compose_dir:
type: string
description: Base directory for Docker Compose stacks on this node.
is_default:
type: integer
enum: [0, 1]
description: Whether this is the default node (1 = true).
api_url:
type: string
description: Remote node API URL (only for remote nodes).
api_token:
type: string
description: Redacted API token for remote node authentication.
created_at:
type: integer
description: Unix timestamp (seconds).
Webhook:
type: object
required: [id, name, stack_name, action, secret, enabled, created_at]
properties:
id:
type: integer
node_id:
type: integer
description: Node the webhook executes against.
name:
type: string
stack_name:
type: string
action:
type: string
enum: [deploy, restart, stop, start, pull, git-pull]
secret:
type: string
description: Masked secret (e.g., `****...****`). Full secret is only returned on creation.
enabled:
type: integer
enum: [0, 1]
created_at:
type: integer
description: Unix timestamp (seconds).
WebhookExecution:
type: object
properties:
id:
type: integer
webhook_id:
type: integer
triggered_at:
type: integer
description: Unix timestamp (seconds).
status:
type: string
response:
type: string
error:
type: ["string", "null"]
ApiToken:
type: object
required: [id, name, scope, user_id, created_at]
properties:
id:
type: integer
name:
type: string
scope:
type: string
enum: [read-only, deploy-only, full-admin]
user_id:
type: integer
created_at:
type: integer
description: Unix timestamp (seconds).
expires_at:
type: ["integer", "null"]
description: Unix timestamp (seconds) or null for no expiry.
revoked_at:
type: ["integer", "null"]
last_used_at:
type: ["integer", "null"]
FleetNodeOverview:
type: object
properties:
id:
type: integer
name:
type: string
type:
type: string
enum: [local, remote]
status:
type: string
enum: [online, offline]
stats:
type: ["object", "null"]
description: Container statistics for the node.
systemStats:
type: ["object", "null"]
description: Host system resource statistics.
stacks:
type: ["integer", "null"]
description: Number of stacks on this node.
FleetSnapshot:
type: object
required: [id, description, created_by, created_at, node_count, stack_count]
properties:
id:
type: integer
description:
type: string
created_by:
type: string
created_at:
type: integer
description: Unix timestamp (seconds).
node_count:
type: integer
stack_count:
type: integer
skipped_nodes:
type: string
description: JSON-stringified array of skipped node names.
FleetSnapshotDetail:
allOf:
- $ref: "#/components/schemas/FleetSnapshot"
- type: object
properties:
fileDecryptWarnings:
type: array
description: Non-sensitive identifiers for snapshot files that could not be decrypted.
items:
type: object
required: [nodeId, nodeName, stackName, filename]
properties:
nodeId:
type: integer
nodeName:
type: string
stackName:
type: string
filename:
type: string
nodes:
type: array
items:
type: object
properties:
nodeId:
type: integer
nodeName:
type: string
stacks:
type: array
items:
type: object
properties:
stackName:
type: string
files:
type: array
items:
oneOf:
- type: object
required: [filename, content]
properties:
filename:
type: string
content:
type: string
description: Decrypted file body; may be an empty string.
- type: object
required: [filename, unavailable]
properties:
filename:
type: string
unavailable:
type: boolean
enum: [true]
ScheduledTask:
type: object
required: [id, name, target_type, action, cron_expression, enabled, created_by, created_at, updated_at]
properties:
id:
type: integer
name:
type: string
target_type:
type: string
enum: [stack, fleet, system, container]
target_id:
type: ["string", "null"]
description: Stack or container name (when target_type is `stack` or `container`).
node_id:
type: ["integer", "null"]
description: Target node ID (when target_type is `stack` or `container`).
action:
type: string
enum: [restart, snapshot, prune]
cron_expression:
type: string
description: Standard cron expression (5 fields).
example: "0 3 * * *"
enabled:
type: integer
enum: [0, 1]
created_by:
type: string
created_at:
type: integer
description: Unix timestamp (seconds).
updated_at:
type: integer
description: Unix timestamp (seconds).
last_run_at:
type: ["integer", "null"]
next_run_at:
type: ["integer", "null"]
last_status:
type: ["string", "null"]
last_error:
type: ["string", "null"]
prune_targets:
type: ["string", "null"]
description: JSON-stringified array of prune targets (containers, images, networks, volumes).
target_services:
type: ["string", "null"]
description: JSON-stringified array of service names to restart.
prune_label_filter:
type: ["string", "null"]
description: Docker label filter for prune operations.
ScheduledTaskRun:
type: object
required: [id, task_id, started_at, status, triggered_by]
properties:
id:
type: integer
task_id:
type: integer
started_at:
type: integer
description: Unix timestamp (seconds).
completed_at:
type: ["integer", "null"]
triggered_by:
type: string
enum: [scheduled, manual]
status:
type: string
enum: [pending, running, success, failed]
output:
type: ["string", "null"]
error:
type: ["string", "null"]
Registry:
type: object
properties:
id:
type: integer
name:
type: string
example: GitHub Container Registry
url:
type: string
example: ghcr.io
type:
type: string
enum: [dockerhub, ghcr, ecr, custom]
username:
type: string
aws_region:
type: ["string", "null"]
description: AWS region (only present for ECR registries).
created_at:
type: integer
description: Unix timestamp (seconds).
RegistryCreate:
type: object
required: [name, url, type, username, secret]
properties:
name:
type: string
maxLength: 100
example: GitHub Container Registry
url:
type: string
maxLength: 500
example: ghcr.io
type:
type: string
enum: [dockerhub, ghcr, ecr, custom]
username:
type: string
example: my-user
secret:
type: string
description: Password, token, or access key. Write-only — never returned in GET responses.
aws_region:
type: string
description: Required when `type` is `ecr`.
example: us-east-1
RegistryTagList:
type: object
required: [tags, nextCursor, registryId, registryName, repository]
properties:
tags:
type: array
items: { type: string }
example: [latest, "1.0"]
nextCursor:
type: ["string", "null"]
description: Opaque cursor for the next page, or null when there are no more tags.
registryId:
type: integer
registryName:
type: string
repository:
type: string
description: Path-safe repository name after Docker Hub library/ normalization when applicable.
example: library/nginx
ImageUpdateStatus:
type: object
properties:
stack_name:
type: string
description: Name of the stack.
has_updates:
type: integer
enum: [0, 1]
description: Whether any images in this stack have newer versions available.
last_checked:
type: ["integer", "null"]
description: Unix timestamp of the last check.
ImageUpdateCheckStatus:
type: object
properties:
checking:
type: boolean
description: "`true` if a scan is currently running."
intervalMinutes:
type: integer
description: Configured fallback interval in minutes (15-1440).
lastCheckedAt:
type: ["integer", "null"]
description: Unix epoch-ms of the last check start, or null if never checked.
nextCheckAt:
type: ["integer", "null"]
description: Unix epoch-ms of the next scheduled check, or null when not scheduled.
manualCooldownMinutes:
type: integer
description: Fixed cooldown ceiling in minutes for manual refresh.
manualCooldownRemainingMs:
type: integer
description: Live remaining cooldown in milliseconds (0 when refresh is allowed).
mode:
type: string
enum: [interval, cron]
description: Active scheduling mode.
cronExpression:
type: ["string", "null"]
description: 5-field cron expression when mode is 'cron', null otherwise.
sidebarIndicators:
type: boolean
description: Whether sidebar update-status indicators are enabled. Controlled by the `image_update_sidebar_indicators` global setting. Default is `false`.
responses:
Unauthorized:
description: Authentication required. Provide a valid Bearer token.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: "Authentication required"
Forbidden:
description: Insufficient permissions or license tier.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: "This feature requires a Sencho Admiral license."
code: "PAID_REQUIRED"
NotFound:
description: Resource not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
InternalError:
description: Internal server error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
paths:
# ── Health ──────────────────────────────────────────────
/api/health:
get:
operationId: getHealth
tags: [Health]
summary: Health check
description: Returns instance health status and uptime. No authentication required. Useful for uptime monitors and Docker HEALTHCHECK.
security: []
responses:
"200":
description: Instance is healthy.
content:
application/json:
schema:
type: object
required: [status, uptime]
properties:
status:
type: string
example: ok
uptime:
type: number
description: Process uptime in seconds.
/api/meta:
get:
operationId: getMeta
tags: [Health]
summary: Instance metadata
description: Returns this Sencho instance's version and list of supported capabilities. No authentication required. Used by nodes to negotiate feature compatibility.
security: []
responses:
"200":
description: Instance metadata.
content:
application/json:
schema:
type: object
required: [version, capabilities]
properties:
version:
type: string
description: Sencho version (semver).
example: "0.32.0"
capabilities:
type: array
description: List of feature capabilities this instance supports.
items:
type: string
example: ["stacks", "containers", "fleet", "auto-updates", "host-console"]
imagePinKind:
type: string
nullable: true
enum: [floating, semver, digest, unknown]
description: How the Sencho service image is pinned in the operator's compose file. Omitted when self-update is unavailable or the compose file could not be read.
updateBlocked:
type: boolean
description: True when the image is pinned in a way Fleet cannot repin automatically (digest or unresolved value).
updateError:
type: string
description: Error from the last failed self-update attempt, when present.
/api/system/update:
post:
operationId: triggerSelfUpdate
tags: [Health]
summary: Trigger self-update
description: |
Instructs this Sencho instance to pull the update image and recreate its own container.
An optional release version in the request body drives semver repinning; floating tags are pulled without rewriting the compose file.
Digest and unresolved pins are rejected with 409 before any pull runs.
Returns 202 immediately; the actual update happens asynchronously after the response is sent.
Requires the instance to be deployed via Docker Compose with the Docker socket mounted.
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
targetVersion:
type: string
description: Release version to update to (valid semver).
responses:
"202":
description: Update initiated.
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: "Update initiated. The server will restart shortly."
"401":
$ref: "#/components/responses/Unauthorized"
"400":
description: Invalid release version in request body.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Update blocked because the compose image cannot be repinned automatically.
content:
application/json:
schema:
type: object
properties:
error:
type: string
code:
type: string
"503":
description: Self-update not available (not running in Docker Compose).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
# ── Stacks ──────────────────────────────────────────────
/api/stacks:
get:
operationId: listStacks
tags: [Stacks]
summary: List all stacks
description: Returns an array of stack directory names on the target node.
parameters:
- $ref: "#/components/parameters/nodeId"
- $ref: "#/components/parameters/nodeIdQuery"
responses:
"200":
description: Array of stack names.
content:
application/json:
schema:
type: array
items:
type: string
example: ["traefik", "portainer", "monitoring"]
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalError"
post:
operationId: createStack
tags: [Stacks]
summary: Create a new stack
description: Creates a new stack directory with a default `compose.yaml` file.
parameters:
- $ref: "#/components/parameters/nodeId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [stackName]
properties:
stackName:
type: string
description: Stack name. Alphanumeric characters, hyphens, and underscores only.
pattern: "^[a-zA-Z0-9_-]+$"
example: my-new-stack
responses:
"201":
description: Stack created.
content:
application/json:
schema:
type: object
required: [message, name]
properties:
message:
type: string
example: Stack created successfully
name:
type: string
example: my-new-stack
"400":
description: Invalid stack name.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Stack already exists.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: Stack already exists
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}:
get:
operationId: getStack
tags: [Stacks]
summary: Get stack compose file
description: Returns the raw `compose.yaml` content for the specified stack.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Raw compose.yaml content.
content:
text/plain:
schema:
type: string
"400":
description: Invalid stack name.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
put:
operationId: updateStack
tags: [Stacks]
summary: Update stack compose file
description: Overwrites the `compose.yaml` content for the specified stack. Requires `stack:edit` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [content]
properties:
content:
type: string
description: Full compose.yaml content.
responses:
"200":
description: Stack saved.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessMessage"
example:
message: Stack saved successfully
"400":
description: Invalid stack name or content.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
delete:
operationId: deleteStack
tags: [Stacks]
summary: Delete stack
description: Permanently deletes the stack directory and all its files. Requires `stack:delete` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Stack deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessMessage"
example:
message: Stack deleted successfully
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/envs:
get:
operationId: getStackEnvFiles
tags: [Stacks]
summary: List env file paths
description: Returns the list of environment file paths referenced in the stack's compose file (`env_file` directives).
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: List of env file paths.
content:
application/json:
schema:
type: object
required: [envFiles]
properties:
envFiles:
type: array
items:
type: string
example: [".env", "config/database.env"]
"400":
description: Invalid stack name.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/env-inventory:
get:
operationId: getStackEnvInventory
tags: [Stacks]
summary: Get environment inventory
description: >-
Returns a per-stack inventory of environment variables: each variable's
source, whether Compose interpolates it or injects it into a container,
and a status (present, missing, unused, duplicate, or unpersisted), plus
likely-secret classification. Names only: a variable value is never read
or returned. When the effective model cannot be rendered, `renderable` is
false and the inventory is derived from the authored source alone.
Requires `stack:read` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: The environment inventory.
content:
application/json:
schema:
type: object
required: [stackName, renderable, items, envFiles, summary]
properties:
stackName:
type: string
renderable:
type: boolean
description: False when the effective model could not be rendered; the inventory is then partial.
items:
type: array
items:
type: object
required: [key, sources, usedForInterpolation, injectedIntoService, required, hasDefault, likelySecret, status]
properties:
key:
type: string
sources:
type: array
items:
type: string
enum: [compose-inline, env-file, dotenv, process-env, compose-ref]
usedForInterpolation:
type: boolean
injectedIntoService:
type: boolean
required:
type: boolean
hasDefault:
type: boolean
likelySecret:
type: boolean
status:
type: string
enum: [present, missing, unused, duplicate, unpersisted]
envFiles:
type: array
items:
type: object
properties:
rawPaths:
type: array
items:
type: string
existence:
type: string
enum: [present, missing, unverifiable]
required:
type: boolean
isInterpolationSource:
type: boolean
isInjectionSource:
type: boolean
declaringServices:
type: array
items:
type: string
summary:
type: object
properties:
total: { type: integer }
missing: { type: integer }
unused: { type: integer }
duplicate: { type: integer }
unpersisted: { type: integer }
likelySecret: { type: integer }
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: Stack not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/label-inventory:
get:
operationId: getStackLabelInventory
tags: [Stacks]
summary: Get Docker label inventory for a stack
description: >-
Returns declared Compose labels and runtime container labels per service,
with reconciliation hints (only in Compose, only on running container,
present in both, or value changed when the values differ) and provenance
for each runtime label (compose, image, runtime, compose-system, or unknown
when a container or image cannot be inspected). Secret-like label values are
redacted unless the caller is an admin and passes `reveal=1`. Requires
`stack:read` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
- name: reveal
in: query
required: false
schema:
type: string
enum: ['1', 'true']
responses:
"200":
description: Stack label inventory.
content:
application/json:
schema:
$ref: "#/components/schemas/StackLabelInventory"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: Stack not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/system/container-labels:
get:
operationId: getNodeContainerLabels
tags: [Fleet]
summary: Get the Docker label inventory for the active node
description: >-
Returns every container on the node with its labels and provenance
(compose, image, runtime, compose-system, or unknown), plus an inverted
`byLabel` index. Marks `partial` when a container or image inspect fails.
Secret-like values are redacted unless the caller is an admin and passes
`reveal=1`. Requires `node:read` permission.
parameters:
- $ref: "#/components/parameters/nodeId"
- name: reveal
in: query
required: false
schema:
type: string
enum: ['1', 'true']
responses:
"200":
description: Node container label inventory.
content:
application/json:
schema:
$ref: "#/components/schemas/NodeLabelInventory"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/fleet/container-labels:
get:
operationId: getFleetContainerLabels
tags: [Fleet]
summary: Get the Docker label inventory aggregated across the fleet
description: >-
Fans out to every node's container label inventory and aggregates the
inverted index by key, value, and source. Unreachable or malformed nodes
degrade into `nodeErrors` rather than failing the whole request. Secret-like
values are redacted unless the caller is an admin and passes `reveal=1`.
Requires `node:read` permission.
parameters:
- name: reveal
in: query
required: false
schema:
type: string
enum: ['1', 'true']
responses:
"200":
description: Fleet-wide container label inventory.
content:
application/json:
schema:
$ref: "#/components/schemas/FleetLabelInventory"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/env:
get:
operationId: getStackEnv
tags: [Stacks]
summary: Get env file content
description: Returns the content of an environment file. Defaults to `.env` if no `file` query parameter is specified.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
- name: file
in: query
required: false
description: Path to a specific env file (must be in the resolved env files list).
schema:
type: string
responses:
"200":
description: Raw env file content (KEY=VALUE format).
content:
text/plain:
schema:
type: string
"400":
description: Invalid stack name or disallowed file path.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Env file not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: Env file not found
"500":
$ref: "#/components/responses/InternalError"
put:
operationId: updateStackEnv
tags: [Stacks]
summary: Update env file
description: Overwrites an environment file's content. Requires `stack:edit` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
- name: file
in: query
required: false
description: Path to a specific env file to update.
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [content]
properties:
content:
type: string
description: Full env file content (KEY=VALUE format).
responses:
"200":
description: Env file saved.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessMessage"
example:
message: Env file saved successfully
"400":
description: Invalid input.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/containers:
get:
operationId: getStackContainers
tags: [Stacks]
summary: List stack containers
description: Returns all containers belonging to the specified stack.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Array of container objects.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Container"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/services:
get:
operationId: getStackServices
tags: [Stacks]
summary: List stack services
description: Returns the service names defined in the stack's compose file.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Array of service names.
content:
application/json:
schema:
type: array
items:
type: string
example: ["web", "db", "redis"]
"400":
description: Invalid stack name.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/deploy:
post:
operationId: deployStack
tags: [Stacks]
summary: Deploy stack
description: |
Runs `docker compose up -d` for the stack using atomic deployment
with automatic rollback on failure. Requires `stack:deploy` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Stack deployed successfully.
content:
application/json:
schema:
type: object
required: [message]
properties:
message:
type: string
example: Deployed successfully
healthGateId:
type: string
nullable: true
description: |
Id of the post-deploy health gate observation started for
this deploy, for use with the health-gate endpoint. Null
when the health gate is disabled on the node.
"403":
$ref: "#/components/responses/Forbidden"
"409":
description: |
Deploy blocked before Compose ran. Common codes include a stack
operation already in progress, a scan-policy block, or missing
external networks (`code: missing_external_networks`).
content:
application/json:
schema:
type: object
properties:
error:
type: string
code:
type: string
example: missing_external_networks
kind:
type: string
enum: [prompt, unsupported, unavailable, create_failed]
networks:
type: array
items:
type: object
createdNames:
type: array
items:
type: string
remainingNames:
type: array
items:
type: string
"500":
description: Deployment failed.
content:
application/json:
schema:
type: object
required: [error]
properties:
error:
type: string
rolledBack:
type: boolean
description: Whether the stack was automatically rolled back.
failure:
$ref: "#/components/schemas/FailureClassification"
"503":
description: |
Deploy blocked because Sencho could not render the Compose model or
read Docker networking state to verify required external networks.
/api/stacks/{stackName}/missing-external-networks:
get:
operationId: getMissingExternalNetworks
tags: [Stacks]
summary: List missing external networks for a stack
description: |
Returns whether declared `external: true` networks are missing on the
target node, whether automatic creation is enabled, and per-network
safety metadata. Requires `stack:read`. Used by interactive deploy
preflight when the node advertises `guided-external-network-preflight`.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Missing-external-network envelope.
content:
application/json:
schema:
type: object
required: [status, autoCreateEnabled, stackName, networks, declaredExternalCount]
properties:
status:
type: string
enum: [ok, render_unavailable, runtime_unavailable]
autoCreateEnabled:
type: boolean
stackName:
type: string
declaredExternalCount:
type: integer
networks:
type: array
items:
type: object
required: [name, keys, declarations, safe, unsupportedFeatures, creationSpec]
properties:
name:
type: string
keys:
type: array
items:
type: string
declarations:
type: array
items:
type: object
safe:
type: boolean
blockReason:
type: string
unsupportedFeatures:
type: array
items:
type: string
creationSpec:
type: object
nullable: true
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: Stack not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/stacks/{stackName}/down:
post:
operationId: downStack
tags: [Stacks]
summary: Tear down stack
description: Runs `docker compose down` for the stack, removing containers and networks. The stack definition remains on disk. Requires `stack:deploy` permission. Pass `removeVolumes=true` to also remove compose volumes when the node advertises the `stack-down-remove-volumes` capability.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
- name: removeVolumes
in: query
required: false
schema:
type: boolean
description: When `true`, runs `docker compose down --volumes`. Only honored when the target node advertises `stack-down-remove-volumes`.
responses:
"200":
description: Command started.
content:
application/json:
schema:
type: object
required: [status]
properties:
status:
type: string
example: Command started
"400":
description: Volume removal requested on a node that does not support it.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/start:
post:
operationId: startStack
tags: [Stacks]
summary: Start stack containers
description: Starts all stopped containers in the stack via the Docker Engine API. Requires `stack:deploy` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Containers started.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: No containers found for this stack.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/stop:
post:
operationId: stopStack
tags: [Stacks]
summary: Stop stack containers
description: Stops all running containers in the stack via the Docker Engine API. Requires `stack:deploy` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Containers stopped.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: No containers found for this stack.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/restart:
post:
operationId: restartStack
tags: [Stacks]
summary: Restart stack containers
description: Restarts all containers in the stack via the Docker Engine API. Requires `stack:deploy` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Containers restarted.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: No containers found for this stack.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/update:
post:
operationId: updateStackImages
tags: [Stacks]
summary: Pull and recreate stack
description: |
Pulls latest images and recreates containers (`docker compose pull && up -d`)
using atomic update with automatic rollback on failure.
Requires `stack:deploy` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Update completed.
content:
application/json:
schema:
type: object
required: [status]
properties:
status:
type: string
example: Update completed
healthGateId:
type: string
nullable: true
description: |
Id of the post-update health gate observation started for
this update, for use with the health-gate endpoint. Null
when the health gate is disabled on the node.
"403":
$ref: "#/components/responses/Forbidden"
"500":
description: Update failed.
content:
application/json:
schema:
type: object
required: [error]
properties:
error:
type: string
rolledBack:
type: boolean
failure:
$ref: "#/components/schemas/FailureClassification"
/api/stacks/{stackName}/rollback:
post:
operationId: rollbackStack
tags: [Stacks]
summary: Rollback stack
description: Restores the stack to its previous deployment state. Requires `stack:deploy` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Stack rolled back.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessMessage"
example:
message: 'Stack rolled back: compose and env files restored.'
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: No backup available.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: No backup available for this stack.
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/backup:
get:
operationId: getStackBackupStatus
tags: [Stacks]
summary: Check backup status
description: Returns whether a deployment backup exists for this stack and when it was created.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Backup status.
content:
application/json:
schema:
type: object
required: [exists]
properties:
exists:
type: boolean
timestamp:
type: ["integer", "null"]
description: Unix timestamp of the backup, or null if no backup exists.
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/update-preview:
get:
operationId: getStackUpdatePreview
tags: [Stacks]
summary: Compute image update preview (read-only)
description: |
Computes the current registry update preview for the stack without
mutating persisted scanner state. Use POST when the client should
reconcile sticky update indicators after an authoritative-negative
result. Requires `stack:read` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Update preview.
content:
application/json:
schema:
$ref: "#/components/schemas/UpdatePreview"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
post:
operationId: reconcileStackUpdatePreview
tags: [Stacks]
summary: Compute update preview and reconcile sticky state
description: |
Computes the same preview as GET. When every image reports
`check_status: ok` and `has_update` is false (mixed `ok` +
`not_checkable` does not clear), clears sticky confirmed update rows
for the stack and sets `reconciled: true`. Requires `stack:read`
permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Update preview, with reconcile outcome.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/UpdatePreview"
- type: object
required: [reconciled]
properties:
reconciled:
type: boolean
description: |
True when sticky update rows were cleared after an
authoritative-negative preview.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/update-readiness:
get:
operationId: getStackUpdateReadiness
tags: [Stacks]
summary: Check update readiness
description: |
Computes an advisory readiness verdict for updating the stack right
now, from the stored preflight result, open drift findings, live
container health, the pending image change, the rollback backup slot,
and node disk headroom. Advisory only: no verdict blocks an update.
Requires `stack:read` permission. Pass `service` to scope the verdict
to one Compose service on a multi-service stack.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
- name: service
in: query
required: false
schema:
type: string
description: |
When set, readiness is scoped to this Compose service (multi-service
stacks only). Stack-wide guardrails still apply; sibling and
dependency signals become advisory. Single-service stacks reject
this parameter with 400.
responses:
"200":
description: Readiness report.
content:
application/json:
schema:
type: object
required: [stack, computedAt, verdict, signals]
properties:
stack:
type: string
computedAt:
type: integer
description: Unix timestamp (ms) the report was computed.
verdict:
type: string
enum: [ready, ready_with_warnings, review_required, blocked, unknown]
signals:
type: array
items:
type: object
required: [id, status, title, detail, affectsVerdict]
properties:
id:
type: string
status:
type: string
enum: [ok, warning, attention, blocked, unknown]
title:
type: string
detail:
type: string
affectsVerdict:
type: boolean
"400":
description: Invalid service scope.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/effective-services:
get:
operationId: getStackEffectiveServices
tags: [Stacks]
summary: List effective Compose services
description: |
Returns the fail-closed effective service model for the stack (declared
image, build presence, expected replicas, depends_on, healthcheck).
Requires `stack:read` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Effective model result.
content:
application/json:
schema:
type: object
required: [renderable]
properties:
renderable:
type: boolean
services:
type: array
items:
type: object
required: [name, declaredImage, hasBuild, expectedReplicas, dependsOn, hasHealthcheck]
properties:
name:
type: string
declaredImage:
type: string
nullable: true
hasBuild:
type: boolean
expectedReplicas:
type: integer
dependsOn:
type: array
items:
type: string
hasHealthcheck:
type: boolean
code:
type: string
error:
type: string
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/services/{serviceName}/update:
post:
operationId: updateStackService
tags: [Stacks]
summary: Update or rebuild one Compose service
description: |
Manually pulls or builds one declared service, then recreates only that
service with `--no-deps --force-recreate`. Multi-service stacks only.
Requires `stack:deploy`. Automation paths continue to update the full stack.
parameters:
- $ref: "#/components/parameters/stackName"
- name: serviceName
in: path
required: true
schema:
type: string
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Service update completed.
content:
application/json:
schema:
type: object
required: [serviceName, healthGateId, observing, recoveryAvailable]
properties:
serviceName:
type: string
healthGateId:
type: string
nullable: true
observing:
type: boolean
recoveryId:
type: string
nullable: true
recoveryAvailable:
type: boolean
recheckWarning:
type: string
"400":
description: Service update rejected (single-service stack, missing service, or unavailable capability).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
description: Service update failed.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/stacks/{stackName}/services/{serviceName}/recovery:
get:
operationId: getStackServiceRecovery
tags: [Stacks]
summary: Latest active service recovery snapshot
description: |
Returns the newest active, unexpired recovery row for one service, or
`recovery: null` when none is available. Used so Restore stays
discoverable when Deploy Progress is disabled or dismissed.
Requires `stack:deploy`.
parameters:
- $ref: "#/components/parameters/stackName"
- name: serviceName
in: path
required: true
schema:
type: string
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Recovery lookup completed.
content:
application/json:
schema:
type: object
required: [recovery]
properties:
recovery:
oneOf:
- type: "null"
- type: object
required: [id, status, expiresAt, createdAt]
properties:
id:
type: string
status:
type: string
healthGateId:
type: string
nullable: true
expiresAt:
type: integer
createdAt:
type: integer
majorityImageId:
type: string
declaredImageRef:
type: string
"400":
description: Capability unavailable.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/api/stacks/{stackName}/services/{serviceName}/restore:
post:
operationId: restoreStackService
tags: [Stacks]
summary: Restore one Compose service from a recovery snapshot
description: |
Retags the captured majority image onto the declared tag and recreates
only that service. Body must include `recoveryId` from a prior service
update. Requires `stack:deploy`.
parameters:
- $ref: "#/components/parameters/stackName"
- name: serviceName
in: path
required: true
schema:
type: string
- $ref: "#/components/parameters/nodeId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [recoveryId]
properties:
recoveryId:
type: string
responses:
"200":
description: Service restore completed.
content:
application/json:
schema:
type: object
required: [serviceName, healthGateId, observing, recoveryAvailable]
properties:
serviceName:
type: string
healthGateId:
type: string
nullable: true
observing:
type: boolean
recoveryId:
type: string
nullable: true
recoveryAvailable:
type: boolean
"400":
description: Restore rejected (missing recoveryId or invalid binding).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
description: Service restore failed.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/stacks/{stackName}/rollback-readiness:
get:
operationId: getStackRollbackReadiness
tags: [Stacks]
summary: Check rollback readiness
description: |
Reports what a rollback of this stack can actually restore: the backed
up compose file, env coverage (variable names only, never values), the
known previous image tag, and an explicit disclosure that volume and
bind-mounted data are not covered by file backups.
Requires `stack:read` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Rollback readiness report.
content:
application/json:
schema:
type: object
required: [stack, computedAt, overall, items]
properties:
stack:
type: string
computedAt:
type: integer
overall:
type: string
enum: [ready, partial, not_ready]
items:
type: array
items:
type: object
required: [id, state, label, detail]
properties:
id:
type: string
state:
type: string
enum: [ready, missing, unknown, not_covered]
label:
type: string
detail:
type: string
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/stacks/{stackName}/health-gate:
get:
operationId: getStackHealthGate
tags: [Stacks]
summary: Get post-update health gate result
description: |
Returns a post-deploy health gate observation for the stack: the run
named by `gateId` (as returned in a deploy or update response), or the
most recent run when omitted. Status `never-run` means no observation
exists. Requires `stack:read` permission.
parameters:
- $ref: "#/components/parameters/stackName"
- $ref: "#/components/parameters/nodeId"
- name: gateId
in: query
required: false
schema:
type: string
description: A specific gate run id to read instead of the latest.
responses:
"200":
description: Health gate report.
content:
application/json:
schema:
type: object
required: [stack, status]
properties:
stack:
type: string
id:
type: string
nullable: true
status:
type: string
enum: [observing, passed, failed, unknown, never-run]
trigger:
type: string
nullable: true
enum: [update, deploy, service_update, service_restore, null]
reason:
type: string
nullable: true
windowSeconds:
type: integer
nullable: true
startedAt:
type: integer
nullable: true
endedAt:
type: integer
nullable: true
containers:
type: array
items:
type: object
required: [name, state, restarts]
properties:
name:
type: string
state:
type: string
description: Docker container state, or `missing` when it vanished mid-observation.
health:
type: string
nullable: true
restarts:
type: integer
service:
type: string
nullable: true
role:
type: string
nullable: true
enum: [primary, collateral, null]
targetScope:
type: string
enum: [stack, service]
description: Defaults to stack for legacy runs.
serviceName:
type: string
nullable: true
failureSource:
type: string
nullable: true
enum: [primary, collateral, null]
description: Set on failed service-scoped runs; null for stack and successful runs.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
# ── Containers ──────────────────────────────────────────
/api/containers:
get:
operationId: listContainers
tags: [Containers]
summary: List all containers
description: Returns all running containers on the target node.
parameters:
- $ref: "#/components/parameters/nodeId"
- $ref: "#/components/parameters/nodeIdQuery"
responses:
"200":
description: Array of container objects.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Container"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalError"
/api/containers/{id}/logs:
get:
operationId: streamContainerLogs
tags: [Containers]
summary: Stream container logs
description: |
Streams container logs as Server-Sent Events (SSE). The connection remains
open and pushes new log lines as `data:` events in real time.
parameters:
- $ref: "#/components/parameters/idPath"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: SSE log stream.
content:
text/event-stream:
schema:
type: string
description: Each event is a `data:` line containing a JSON-encoded log string.
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalError"
/api/containers/{id}/start:
post:
operationId: startContainer
tags: [Containers]
summary: Start container
description: Starts a stopped container. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Container started.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessMessage"
example:
message: Container started
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/containers/{id}/stop:
post:
operationId: stopContainer
tags: [Containers]
summary: Stop container
description: Stops a running container. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Container stopped.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessMessage"
example:
message: Container stopped
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/containers/{id}/restart:
post:
operationId: restartContainer
tags: [Containers]
summary: Restart container
description: Restarts a container. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
- $ref: "#/components/parameters/nodeId"
responses:
"200":
description: Container restarted.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessMessage"
example:
message: Container restarted
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
# ── API Tokens ──────────────────────────────────────────
/api/api-tokens:
post:
operationId: createApiToken
tags: [API Tokens]
summary: Create API token
description: |
Generates a new scoped API token. The full token is only returned in the creation response
and cannot be retrieved again. Requires admin role.
**Note:** API tokens cannot create other API tokens.
responses:
"201":
description: Token created. The `token` field contains the full JWT. Save it now; it will not be shown again.
content:
application/json:
schema:
type: object
required: [id, token]
properties:
id:
type: integer
token:
type: string
description: Full JWT token. Store securely — this is the only time it's returned.
"400":
description: Validation error (missing/invalid fields, or maximum of 25 active tokens reached).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"409":
description: An active token with this name already exists.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name, scope]
properties:
name:
type: string
description: Human-readable token name. Must be unique among the user's active tokens.
maxLength: 100
example: CI/CD Deploy Token
scope:
type: string
description: |
Permission scope:
- `read-only` — GET requests only
- `deploy-only` — Read + stack deploy/stop/restart operations
- `full-admin` — All operations (except license, user, and token management)
enum: [read-only, deploy-only, full-admin]
expires_in:
type: ["integer", "null"]
description: Token lifetime in days. Use `null` for no expiry.
enum: [30, 60, 90, 365, null]
example: 90
get:
operationId: listApiTokens
tags: [API Tokens]
summary: List API tokens
description: |
Returns all API tokens for the current user. Token hashes are never exposed.
Requires admin role.
**Note:** API tokens cannot list other API tokens.
responses:
"200":
description: Array of token metadata.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ApiToken"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/api-tokens/{id}:
delete:
operationId: revokeApiToken
tags: [API Tokens]
summary: Revoke API token
description: |
Permanently revokes an API token. Users can only revoke their own tokens.
Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Token revoked.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"400":
description: Invalid token ID.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
# ── Webhooks ────────────────────────────────────────────
/api/webhooks:
get:
operationId: listWebhooks
tags: [Webhooks]
summary: List webhooks
description: Returns all configured webhooks with masked secrets.
responses:
"200":
description: Array of webhook objects.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Webhook"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
post:
operationId: createWebhook
tags: [Webhooks]
summary: Create webhook
description: |
Creates a new webhook for a stack. The webhook secret is auto-generated and only
returned in the creation response. Requires admin role.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name, stack_name, action]
properties:
name:
type: string
maxLength: 100
example: GitHub Deploy Hook
stack_name:
type: string
description: Target stack name.
example: my-app
action:
type: string
enum: [deploy, restart, stop, start, pull, git-pull]
description: Action to execute when the webhook is triggered. `git-pull` requires a Git source attached to the stack.
enabled:
type: boolean
default: true
node_id:
type: integer
description: Node the webhook executes against. Defaults to the request's resolved node or the default node.
responses:
"201":
description: Webhook created. The `secret` field is the HMAC signing key; save it now.
content:
application/json:
schema:
type: object
required: [id, secret]
properties:
id:
type: integer
secret:
type: string
description: HMAC-SHA256 signing secret. Store securely; this is the only time it's returned in full.
"400":
description: Validation error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/webhooks/{id}:
put:
operationId: updateWebhook
tags: [Webhooks]
summary: Update webhook
description: Updates webhook configuration. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
maxLength: 100
stack_name:
type: string
action:
type: string
enum: [deploy, restart, stop, start, pull, git-pull]
enabled:
type: boolean
node_id:
type: integer
description: Retarget the webhook to a different node.
responses:
"200":
description: Webhook updated.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"400":
description: Validation error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
operationId: deleteWebhook
tags: [Webhooks]
summary: Delete webhook
description: Permanently deletes a webhook. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Webhook deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/webhooks/{id}/history:
get:
operationId: getWebhookHistory
tags: [Webhooks]
summary: Get webhook execution history
description: Returns the execution log for a webhook.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Array of execution records.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WebhookExecution"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/webhooks/{id}/trigger:
post:
operationId: triggerWebhook
tags: [Webhooks]
summary: Trigger webhook
description: |
Externally triggers a webhook action. This endpoint is public but requires a valid
HMAC-SHA256 signature in the `X-Webhook-Signature` header. Sign the exact raw bytes
of the request body; an empty body is rejected.
Compute the signature as: `sha256=` + HMAC-SHA256(raw_request_body, webhook_secret).
Every unauthenticated rejection (unknown id, disabled webhook, non-paid licence,
missing or invalid signature, empty body) returns the same `404` response so callers
cannot enumerate webhook ids or fingerprint the instance's licence tier.
security:
- webhookSignature: []
parameters:
- $ref: "#/components/parameters/idPath"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
action:
type: string
enum: [deploy, restart, stop, start, pull, git-pull]
description: Override the default webhook action. Must be one of the allowed actions.
responses:
"202":
description: Webhook accepted and action queued.
content:
application/json:
schema:
type: object
required: [message, action]
properties:
message:
type: string
example: Webhook accepted
action:
type: string
example: deploy
"400":
description: Authentication succeeded but the body's `action` override was not in the allowlist.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Authentication failed. The webhook is unknown, disabled, the licence is not paid, the signature header is missing, the body was empty, or the signature did not match. Sencho returns the same response for every unauthenticated case.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"429":
description: Rate limit exceeded. The trigger endpoint allows 500 requests per minute per source IP; CI/CD callers behind shared NAT may share that budget.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
# ── Nodes ───────────────────────────────────────────────
/api/nodes:
get:
operationId: listNodes
tags: [Nodes]
summary: List all nodes
description: Returns all registered nodes (local and remote).
responses:
"200":
description: Array of node objects.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Node"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalError"
post:
operationId: createNode
tags: [Nodes]
summary: Register a new node
description: |
Adds a new local or remote node. Only one local node is allowed per instance;
attempting to create a second local node returns 409. Remote nodes require
an API URL pointing to another Sencho instance and an API token for
authentication.
Requires `node:manage` permission. Node type is immutable after creation.
**Note:** API tokens cannot manage nodes.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name, type]
properties:
name:
type: string
example: production-server
type:
type: string
enum: [local, remote]
compose_dir:
type: string
description: Base directory for Compose stacks (defaults to `/app/compose`).
is_default:
type: boolean
description: Set as the default node.
api_url:
type: string
description: Remote Sencho instance URL (required for remote nodes).
example: https://sencho.example.com:1852
api_token:
type: string
description: API token for authenticating with the remote Sencho instance.
responses:
"200":
description: Node created.
content:
application/json:
schema:
type: object
required: [success, id]
properties:
success:
type: boolean
id:
type: integer
description: New node ID.
warning:
type: string
description: Warning if the remote URL uses plain HTTP.
"400":
description: Validation error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"409":
description: Node name already exists, or a local node already exists.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/nodes/{id}:
get:
operationId: getNode
tags: [Nodes]
summary: Get node details
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Node object.
content:
application/json:
schema:
$ref: "#/components/schemas/Node"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
operationId: updateNode
tags: [Nodes]
summary: Update node
description: Updates node configuration. Node type is immutable after creation. Requires `node:manage` permission.
parameters:
- $ref: "#/components/parameters/idPath"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
compose_dir:
type: string
is_default:
type: boolean
api_url:
type: string
api_token:
type: string
responses:
"200":
description: Node updated.
content:
application/json:
schema:
type: object
required: [success]
properties:
success:
type: boolean
warning:
type: string
"400":
description: Validation error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
delete:
operationId: deleteNode
tags: [Nodes]
summary: Delete node
description: Removes a node from the fleet. The last local node cannot be deleted. Requires `node:manage` permission.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Node deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"400":
description: Cannot delete the only local node (or the default node).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/nodes/{id}/test:
post:
operationId: testNodeConnection
tags: [Nodes]
summary: Test node connection
description: Tests connectivity to a node. For remote nodes, pings the remote Sencho instance's health endpoint.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Connection test result.
content:
application/json:
schema:
type: object
required: [success]
properties:
success:
type: boolean
message:
type: string
error:
type: string
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalError"
/api/nodes/{id}/meta:
get:
operationId: getNodeMeta
tags: [Nodes]
summary: Get node capabilities
description: "Returns the version and supported capabilities for a specific node. For local nodes, returns this instance's own metadata. For remote nodes, relays the response from the remote instance's `/api/meta` endpoint. Returns `{ version: null, capabilities: [] }` if the remote node is offline or doesn't support the meta endpoint."
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Node capability metadata.
content:
application/json:
schema:
type: object
required: [version, capabilities]
properties:
version:
type: string
nullable: true
description: Sencho version of the node, or null if unavailable.
example: "0.31.0"
capabilities:
type: array
description: List of feature capabilities the node supports. Empty array if unavailable.
items:
type: string
example: ["stacks", "containers", "fleet"]
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
# ── Fleet ───────────────────────────────────────────────
/api/fleet/overview:
get:
operationId: getFleetOverview
tags: [Fleet]
summary: Fleet overview
description: Returns an aggregated overview of all nodes including status, container stats, and stack counts.
responses:
"200":
description: Array of fleet node overviews.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FleetNodeOverview"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalError"
/api/fleet/node/{nodeId}/stacks:
get:
operationId: getFleetNodeStacks
tags: [Fleet]
summary: List stacks on a fleet node
description: Returns stack names from a specific fleet node.
parameters:
- name: nodeId
in: path
required: true
schema:
type: integer
responses:
"200":
description: Array of stack names.
content:
application/json:
schema:
type: array
items:
type: string
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
"502":
description: Failed to reach remote node.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"503":
description: Remote node not configured.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/fleet/node/{nodeId}/stacks/{stackName}/containers:
get:
operationId: getFleetNodeStackContainers
tags: [Fleet]
summary: List containers in a fleet node stack
description: Returns containers for a specific stack on a specific fleet node.
parameters:
- name: nodeId
in: path
required: true
schema:
type: integer
- $ref: "#/components/parameters/stackName"
responses:
"200":
description: Array of container objects.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Container"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
"503":
description: Remote node not configured.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/fleet/update-status:
get:
operationId: getFleetUpdateStatus
tags: [Fleet]
summary: Fleet update status
description: Returns version comparison and active update status for all nodes. Compares each node's version against the gateway's version. Local rows include compose pin details; remote rows expose only the safe pin kind and blocked flag from each node's public meta.
responses:
"200":
description: Update status for all nodes.
content:
application/json:
schema:
type: object
properties:
nodes:
type: array
items:
type: object
properties:
nodeId:
type: integer
name:
type: string
type:
type: string
enum: [local, remote]
version:
type: string
nullable: true
latestVersion:
type: string
updateAvailable:
type: boolean
updateStatus:
type: string
nullable: true
enum: [updating, completed, timeout, failed, null]
skipActive:
type: boolean
skippedVersion:
type: string
nullable: true
imagePinKind:
type: string
nullable: true
composeImageRef:
type: string
nullable: true
targetImageRef:
type: string
nullable: true
updateBlocked:
type: boolean
updateBlockedReason:
type: string
nullable: true
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/api/fleet/nodes/{nodeId}/skip-version:
post:
operationId: skipNodeVersion
tags: [Fleet]
summary: Skip a node version
description: Marks a version as skipped for a node, hiding the update prompt and excluding it from update-all. Admin only.
parameters:
- name: nodeId
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [version]
properties:
version:
type: string
description: Semver version string to skip.
responses:
"204":
description: Skip recorded.
"400":
description: Invalid parameters.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: Node not found.
delete:
operationId: unskipNodeVersion
tags: [Fleet]
summary: Clear a node version skip
description: Removes the skipped-version record for a node. Admin only.
parameters:
- name: nodeId
in: path
required: true
schema:
type: integer
responses:
"204":
description: Skip cleared.
"400":
description: Invalid parameters.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: Node not found.
/api/fleet/update-status/release-notes:
get:
operationId: getFleetReleaseNotes
tags: [Fleet]
summary: Release notes for the latest Sencho version
description: Returns the GitHub release body and URL for the latest Sencho release, used by the Changelog tab.
responses:
"200":
description: Release notes.
content:
application/json:
schema:
type: object
properties:
releaseNotes:
type: string
nullable: true
htmlUrl:
type: string
nullable: true
"401":
$ref: "#/components/responses/Unauthorized"
/api/fleet/nodes/{nodeId}/update:
post:
operationId: triggerNodeUpdate
tags: [Fleet]
summary: Trigger node update
description: Initiates a self-update on the specified node. For remote nodes, sends the update command via the proxy. For local nodes, triggers the update directly.
parameters:
- name: nodeId
in: path
required: true
schema:
type: integer
responses:
"202":
description: Update initiated.
content:
application/json:
schema:
type: object
properties:
message:
type: string
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: Node not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Update already in progress.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"503":
description: Self-update not available on the target node.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/fleet/update-all:
post:
operationId: triggerFleetUpdateAll
tags: [Fleet]
summary: Update all outdated nodes
description: Triggers self-update on all remote nodes running an older version than the gateway. Forwards the hub compare target as `targetVersion` so semver-pinned remotes can repin before recreate. Local nodes are excluded from bulk updates.
responses:
"202":
description: Bulk update initiated.
content:
application/json:
schema:
type: object
properties:
updating:
type: array
items:
type: string
description: Names of nodes where update was triggered.
skipped:
type: array
items:
type: string
description: Names of nodes that were skipped.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/api/fleet/snapshots:
post:
operationId: createFleetSnapshot
tags: [Fleet]
summary: Create fleet snapshot
description: Creates a point-in-time backup of all compose files across all nodes. Requires admin role.
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
description:
type: string
description: Optional description for the snapshot.
example: Pre-migration backup
responses:
"201":
description: Snapshot created.
content:
application/json:
schema:
$ref: "#/components/schemas/FleetSnapshot"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
get:
operationId: listFleetSnapshots
tags: [Fleet]
summary: List fleet snapshots
description: Returns paginated fleet snapshots.
parameters:
- name: limit
in: query
schema:
type: integer
default: 50
maximum: 100
- name: offset
in: query
schema:
type: integer
default: 0
responses:
"200":
description: Paginated snapshot list.
content:
application/json:
schema:
type: object
required: [snapshots, total]
properties:
snapshots:
type: array
items:
$ref: "#/components/schemas/FleetSnapshot"
total:
type: integer
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/fleet/snapshots/{id}:
get:
operationId: getFleetSnapshot
tags: [Fleet]
summary: Get snapshot details
description: Returns full snapshot details including all captured files grouped by node and stack.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Snapshot with file contents.
content:
application/json:
schema:
$ref: "#/components/schemas/FleetSnapshotDetail"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
operationId: deleteFleetSnapshot
tags: [Fleet]
summary: Delete snapshot
description: Permanently deletes a fleet snapshot. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Snapshot deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/fleet/snapshots/{id}/restore:
post:
operationId: restoreFleetSnapshot
tags: [Fleet]
summary: Restore from snapshot
description: Restores a specific stack on a specific node from the snapshot. Optionally redeploys after restore. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [nodeId, stackName]
properties:
nodeId:
type: integer
description: Target node ID to restore to.
stackName:
type: string
description: Stack to restore.
redeploy:
type: boolean
default: false
description: Whether to redeploy the stack after restoring files.
responses:
"200":
description: Stack restored.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessMessage"
example:
message: Stack restored successfully.
"400":
description: Missing required fields.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"409":
description: One or more snapshot files for the stack could not be decrypted.
content:
application/json:
schema:
type: object
required: [error, code]
properties:
error:
type: string
code:
type: string
enum: [SNAPSHOT_FILE_UNAVAILABLE]
"500":
$ref: "#/components/responses/InternalError"
/api/scheduled-tasks:
get:
operationId: listScheduledTasks
tags: [Scheduled Tasks]
summary: List scheduled tasks
description: Returns all scheduled tasks. Requires admin role.
responses:
"200":
description: Array of scheduled task objects.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ScheduledTask"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
post:
operationId: createScheduledTask
tags: [Scheduled Tasks]
summary: Create scheduled task
description: |
Creates a new recurring task. Action-target rules:
- `restart` requires `target_type: stack` or `target_type: container` (with `target_id` and `node_id`)
- `auto_stop` and `auto_start` accept `target_type: stack` or `target_type: container`
- `snapshot` requires `target_type: fleet`
- `prune` requires `target_type: system`
Requires admin role.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name, target_type, action, cron_expression]
properties:
name:
type: string
example: Nightly restart
target_type:
type: string
enum: [stack, fleet, system, container]
target_id:
type: string
description: Stack or container name (required when target_type is `stack` or `container`).
node_id:
type: integer
description: Target node ID (required when target_type is `stack` or `container`).
action:
type: string
enum: [restart, snapshot, prune]
cron_expression:
type: string
description: Standard 5-field cron expression.
example: "0 3 * * *"
enabled:
type: boolean
default: true
prune_targets:
type: array
items:
type: string
enum: [containers, images, networks, volumes]
description: Resources to prune (only for `prune` action).
target_services:
type: array
items:
type: string
description: Specific services to restart (only for `restart` action).
prune_label_filter:
type: string
description: Docker label filter for prune operations.
responses:
"201":
description: Task created.
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledTask"
"400":
description: Validation error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/scheduled-tasks/{id}:
get:
operationId: getScheduledTask
tags: [Scheduled Tasks]
summary: Get scheduled task
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Scheduled task object.
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledTask"
"400":
description: Invalid task ID.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
operationId: updateScheduledTask
tags: [Scheduled Tasks]
summary: Update scheduled task
description: Updates task configuration. Same validation rules as creation apply. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
target_type:
type: string
enum: [stack, fleet, system, container]
target_id:
type: string
description: Stack or container name when target_type is `stack` or `container`.
node_id:
type: integer
action:
type: string
enum: [restart, snapshot, prune]
cron_expression:
type: string
enabled:
type: boolean
prune_targets:
type: array
items:
type: string
target_services:
type: array
items:
type: string
prune_label_filter:
type: string
responses:
"200":
description: Task updated.
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledTask"
"400":
description: Validation error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
operationId: deleteScheduledTask
tags: [Scheduled Tasks]
summary: Delete scheduled task
description: Permanently deletes a scheduled task. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Task deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"400":
description: Invalid task ID.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/scheduled-tasks/{id}/toggle:
patch:
operationId: toggleScheduledTask
tags: [Scheduled Tasks]
summary: Toggle task enabled/disabled
description: Flips the enabled state of a scheduled task. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Task toggled. Returns updated task.
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledTask"
"400":
description: Invalid task ID.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/scheduled-tasks/{id}/run:
post:
operationId: runScheduledTask
tags: [Scheduled Tasks]
summary: Run task immediately
description: Executes the scheduled task immediately, regardless of its cron schedule. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Task executed. Returns updated task with `last_run_at`.
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledTask"
"400":
description: Invalid task ID.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/scheduled-tasks/{id}/runs:
get:
operationId: listScheduledTaskRuns
tags: [Scheduled Tasks]
summary: List task execution history
description: Returns paginated execution history for a scheduled task. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
- name: limit
in: query
schema:
type: integer
default: 50
- name: offset
in: query
schema:
type: integer
default: 0
responses:
"200":
description: Array of task run records.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ScheduledTaskRun"
"400":
description: Invalid task ID.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/scheduled-tasks/{id}/runs/export:
get:
operationId: exportScheduledTaskRuns
tags: [Scheduled Tasks]
summary: Export task history as CSV
description: Downloads the execution history for a scheduled task as a CSV file. Requires admin role.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: CSV file download.
content:
text/csv:
schema:
type: string
description: "CSV with columns: Timestamp, Source, Status, Duration (s), Details"
"400":
description: Invalid task ID.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
# ── Registries ──────────────────────────────────────────
/api/registries:
get:
operationId: listRegistries
tags: [Registries]
summary: List registries
description: |
Returns all configured private container registries. Secrets are never included in the response.
Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`.
responses:
"200":
description: Array of registry objects.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Registry"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
post:
operationId: createRegistry
tags: [Registries]
summary: Create registry
description: |
Adds a new private container registry. The secret is encrypted at rest using AES-256-GCM.
Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegistryCreate"
responses:
"201":
description: Registry created.
content:
application/json:
schema:
type: object
required: [id]
properties:
id:
type: integer
"400":
description: Validation error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
/api/registries/{id}:
put:
operationId: updateRegistry
tags: [Registries]
summary: Update registry
description: |
Updates an existing registry. All fields are optional — only provided fields are changed.
Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`.
parameters:
- $ref: "#/components/parameters/idPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegistryCreate"
responses:
"200":
description: Registry updated.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"400":
description: Validation error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
operationId: deleteRegistry
tags: [Registries]
summary: Delete registry
description: |
Removes a registry and its encrypted credentials.
Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Registry deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessBoolean"
"400":
description: Invalid registry ID.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/registries/{id}/test:
post:
operationId: testRegistry
tags: [Registries]
summary: Test registry connection
description: |
Tests connectivity and authentication against a configured registry.
Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`.
parameters:
- $ref: "#/components/parameters/idPath"
responses:
"200":
description: Test result.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
message:
type: string
"400":
description: Invalid registry ID.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalError"
# ── Image Updates ───────────────────────────────────────
/api/image-updates:
get:
operationId: getImageUpdates
tags: [Image Updates]
summary: Get image update status
description: Returns the update availability status for all stacks.
responses:
"200":
description: Array of update status objects per stack.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ImageUpdateStatus"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalError"
/api/image-updates/refresh:
post:
operationId: refreshImageUpdates
tags: [Image Updates]
summary: Trigger image update check
description: |
Triggers a background check for newer image versions across all stacks.
Rate limited to one refresh per 10 minutes. Requires admin role.
responses:
"200":
description: Refresh started.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
message:
type: string
example: Image update check started in background.
"401":
$ref: "#/components/responses/Unauthorized"
"429":
description: Rate limited — wait at least 10 minutes between refreshes.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
/api/image-updates/status:
get:
operationId: getImageUpdateCheckStatus
tags: [Image Updates]
summary: Get scanner cadence status
description: Returns the scanner's current status including scheduling mode and next check time.
responses:
"200":
description: Scanner status.
content:
application/json:
schema:
$ref: "#/components/schemas/ImageUpdateCheckStatus"
"401":
$ref: "#/components/responses/Unauthorized"
/api/image-updates/interval:
put:
operationId: setImageUpdateCheckSchedule
tags: [Image Updates]
summary: Set the detection schedule
description: Configure the image-update check cadence. In interval mode only `minutes` is required. In cron mode also send `mode` and `cron`.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [minutes]
properties:
minutes:
type: integer
minimum: 15
maximum: 1440
description: Fallback interval in minutes (used in interval mode, or as fallback when cron is unset).
mode:
type: string
enum: [interval, cron]
description: Scheduling mode. Omit to leave unchanged.
cron:
type: string
description: 5-field cron expression (required when mode is 'cron'). Nicknames like @daily are accepted.
responses:
"200":
description: Schedule updated.
content:
application/json:
schema:
$ref: "#/components/schemas/ImageUpdateCheckStatus"
"400":
$ref: "#/components/responses/Forbidden"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: Non-admin users cannot change the schedule.
/api/registries/{id}/tags:
get:
tags: [Registries]
summary: List tags for a repository on a configured registry
description: |
Hub-only. Admin only. Credentials are resolved by exact registry ID.
Upstream registry auth failures return 424 with a REGISTRY_* code, never HTTP 401.
parameters:
- name: id
in: path
required: true
schema: { type: integer }
- name: repository
in: query
required: true
schema: { type: string }
description: Path-safe repository name (no host or scheme)
- name: cursor
in: query
required: false
schema: { type: string }
- name: limit
in: query
required: false
schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
responses:
'200':
description: Tag page
content:
application/json:
schema:
$ref: "#/components/schemas/RegistryTagList"
'400':
description: Invalid repository or response
'404':
description: Registry or repository not found
'424':
description: Upstream registry auth or policy failure (not a Sencho session 401)
'502':
description: Upstream registry transport failure