API reference
The same tenant-scoped operations behind the CLI and SSH command server.
Authenticate
The public API is https://api.runin.eu. Send your account's access token in the Authorization: Bearer header over HTTPS. Keep tokens in protected local configuration or environment files. Do not put them in URLs.
Machine lists and usage are scoped to your account. Requests for another account's Machine do not grant access. Administrative operations require operator credentials.
Use the OpenAPI document
Download the OpenAPI specification for request fields, responses, errors and authentication. This file is copied from the server's checked-in specification during the website build.
Lifecycle requests are asynchronous. Acceptance is not completion; poll the returned Machine, Template or Snapshot state. The customer CLI and SSH commands provide these waits.
Read the full specification
# Forgesmith API v1 (ticket 033). This file is the source of truth for the
# Control Plane's public surface: docs/m3-contract.md points at it, the CLI
# speaks it, and internal/controlplane/api serves it.
#
# The handlers are written by hand rather than generated. The surface is small
# and two of its shapes -- the NDJSON exec stream and the size field that is
# either a preset name or an object -- are ones a generator renders worse than
# a person does. What keeps the two in step instead of a generator is
# TestSpecMatchesRoutes in internal/controlplane/api: it fails if a route here
# has no handler, or a handler no route. Add a path here first, then a route.
#
# Indentation matters to that test, which reads this file with a small scanner
# rather than a YAML library (the Control Plane is standard library only):
# every path is a key at two spaces, every method a key at four.
openapi: 3.1.0
info:
title: Forgesmith API
version: "1.0.0"
summary: Machines, Templates, Snapshots and exec on EU bare metal.
description: |
The Control Plane's public API. A Machine is a Firecracker microVM owned by
one Tenant; a Template is the versioned disk image it starts from; a
Snapshot is the captured state of a paused Machine, and a Fork is a new
Machine made from one.
Every request carries `Authorization: Bearer <api key>`. A key belongs to
one Tenant, and everything a request can see or change belongs to that
Tenant: a Machine of another Tenant is not found rather than forbidden.
Machine lifecycle calls are asynchronous. Create answers 201 as soon as the
Machine's workflow has started, and pause, resume, fork and delete answer
202; poll `GET /v1/machines/{id}` for the phase.
Errors are RFC 7807 problem documents with the media type
`application/problem+json`.
servers:
- url: https://api.runin.eu
description: The public API (Caddy on runin-control; /v1 only)
- url: http://localhost:8080
description: fsm-control serve on a development machine
security:
- bearerAuth: []
tags:
- name: tenants
- name: members
- name: machines
- name: snapshots
- name: templates
- name: keys
- name: hosts
- name: meta
paths:
/v1/admin/abuse-reports:
post:
operationId: recordAbuseNotice
summary: Operator-only intake of an abuse notice for review
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [machine, reference, summary]
properties:
machine: {type: string}
reference: {type: string, maxLength: 256}
summary: {type: string, maxLength: 4096}
responses:
'201':
description: Durable notice recorded for review; does not automatically pause
'403':
description: Admin credential required
/v1/health:
get:
tags: [meta]
operationId: health
summary: Liveness of the API
description: The one route that needs no API key.
security: []
responses:
"200":
description: The API is serving.
content:
application/json:
schema:
type: object
required: [status]
properties:
status:
type: string
const: ok
version:
type: string
/v1/usage:
get:
tags: [tenants]
operationId: tenantUsage
summary: Current allocations and Pool quota status for the caller's Tenant
description: |
A single database observation, including live reservations. No Tenant
selector, even for admins. This is an allocation ledger, not utilization
or Host free capacity. CPU, memory and Machine count quotas are enforced
for create and Fork, and CPU and memory also for resume, including
pending reservations. Defaults are 8 allocated vCPU, 16384 MiB and 50
Machines; operator Pool overrides replace them. Remaining allowance
uses the same ledger as admission. Unknown allocations prevent admission
and remain null, never zero. Disk reports allocated guest capacity; Host
storage categories require fresh complete observations. Positive legacy
disk limits are not enforced.
responses:
"200":
description: Current usage; Cache-Control no-store.
content:
application/json:
schema:
$ref: '#/components/schemas/TenantUsage'
"401":
$ref: '#/components/responses/Unauthenticated'
"501":
description: This Control Plane has no allocation ledger.
/v1/usage/history:
get:
tags: [tenants]
operationId: tenantUsageHistory
summary: Recorded allocation totals by UTC day or month
description: |
Caller Tenant only, including for administrators and SSH forwarding.
The range is half-open, positive, at most 366 days and cannot end in the
future. Defaults to the current UTC month through the server clock.
This is prospective allocation accounting, not Host utilization or an
invoice. Coverage is incomplete before metering started or during
uncertain Machine state. No usage is invented for those periods.
parameters:
- in: query
name: from
schema: { type: string }
description: Inclusive UTC date YYYY-MM-DD or RFC3339 timestamp.
- in: query
name: to
schema: { type: string }
description: Exclusive UTC date YYYY-MM-DD or RFC3339 timestamp.
- in: query
name: group
schema: { type: string, enum: [day, month], default: day }
- in: query
name: format
schema: { type: string, enum: [json, csv], default: json }
responses:
"200":
description: Allocation history; Cache-Control no-store.
content:
application/json:
schema: { $ref: '#/components/schemas/UsageHistory' }
text/csv:
schema: { type: string }
"400": { $ref: '#/components/responses/BadRequest' }
"401": { $ref: '#/components/responses/Unauthenticated' }
"501":
description: History provider unavailable.
/v1/whoami:
get:
tags: [keys]
operationId: whoami
summary: Who the API key of this request belongs to
description: |
The Tenant, the key and whether it is an admin key. `runin login`
checks a token against this before storing it.
responses:
"200":
description: The caller.
content:
application/json:
schema:
$ref: "#/components/schemas/Whoami"
"401": { $ref: "#/components/responses/Unauthenticated" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/machines:
get:
tags: [machines]
operationId: listMachines
summary: List this Tenant's Machines
description: |
Default listings omit retained terminal records. Use all=true for history
including failed and destroyed Machines, or failed=true for cleanup
discovery. The two filters are mutually exclusive and never widen Tenant
authorization. Failed records with trusted proof of no Host runtime can
be removed through the normal DELETE operation.
Without scope=tenant the answer is the caller's own and shared Machines
only; ?scope=tenant is a Tenant admin's whole-Tenant list. An operator
key may name another Tenant with ?tenant=.
parameters:
- name: all
in: query
schema: { type: boolean, default: false }
description: Include retained failed and destroyed records.
- name: failed
in: query
schema: { type: boolean, default: false }
description: Show failed records only.
- name: scope
in: query
required: false
description: |
"tenant" lists every live Machine of the Tenant and needs a Tenant
admin; without it the answer is the caller's own and shared
Machines.
schema:
type: string
- name: tenant
in: query
required: false
description: Another Tenant's Machines. Admin keys only.
schema:
type: string
responses:
"200":
description: The Machines, newest first.
content:
application/json:
schema:
$ref: "#/components/schemas/MachineList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"429": { $ref: "#/components/responses/RateLimited" }
post:
tags: [machines]
operationId: createMachine
summary: Create a Machine from a Template
description: |
Mints a Machine ID, checks that the Tenant's Host Group has room, and
starts the Machine's workflow. The Machine is in phase `creating` when
this returns; the Host does the work afterwards.
A Host Group with no room is a 409 here rather than a Machine that
fails a few seconds later. Room is guaranteed CPU and memory: a
`reserved` Machine needs all of its vCPUs unguaranteed on one Host, a
`flex` one only its share, so a Host Group can be full for one class
and not the other. The check is not the authority, though: the
reservation inside the workflow is, so two creates that race can both
be accepted and one of them still fail.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMachineRequest"
responses:
"201":
description: The Machine was accepted and its workflow started.
headers:
Location:
description: The new Machine's URL.
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Machine"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"409":
description: |
The Tenant's Pool is full, or no Host in its Host Group has room.
The problem type says which.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/machines/{id}:
get:
tags: [machines]
operationId: getMachine
summary: One Machine and its current phase
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"200":
description: The Machine.
content:
application/json:
schema:
$ref: "#/components/schemas/Machine"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"429": { $ref: "#/components/responses/RateLimited" }
delete:
tags: [machines]
operationId: deleteMachine
summary: Destroy a Machine
description: Signals the Machine's workflow to destroy it and release its capacity.
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"202":
description: Destruction was signalled.
content:
application/json:
schema:
$ref: "#/components/schemas/Machine"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/machines/{id}/backup:
put:
tags: [machines]
operationId: updateMachineBackup
summary: Set a Machine's requested backup preference
description: |
Per-Machine backup opt-out (ticket 117). False asks the Host to start
no new backup captures for this Machine; true resumes them. This is the
requested preference, not an observation of enforcement: the Host acts
on it at its next policy refresh, and a capture it already admitted may
finish. Opting out deletes nothing -- existing checkpoints and their
retention are untouched -- and re-enabling resumes the ordinary
full/incremental workflow.
parameters:
- $ref: "#/components/parameters/MachineID"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [backups_enabled]
properties:
backups_enabled:
type: boolean
responses:
"200":
description: The Machine with its preference set.
content:
application/json:
schema:
$ref: "#/components/schemas/Machine"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/machines/{id}/abuse:
get:
summary: Read owned Machine abuse evidence summary and response incidents
operationId: machineAbuse
parameters:
- $ref: '#/components/parameters/MachineID'
responses:
'200':
description: Host evidence interval count, last observation, and incidents with response states
content:
application/json:
schema:
type: object
required: [machine, reports, incidents]
properties:
machine: {type: string}
reports: {type: integer}
last_observed_at: {type: string, format: date-time}
incidents:
type: array
items:
type: object
required: [id, machine, reason, state, created_at, updated_at]
properties:
id: {type: string}
machine: {type: string}
reason: {type: string}
state: {type: string, enum: [pending, scheduled, pause_requested, paused, failed, obsolete]}
created_at: {type: string, format: date-time}
updated_at: {type: string, format: date-time}
'404':
description: Unknown or unowned Machine
'503':
description: Durable abuse reporting unavailable
/v1/machines/{id}/pause:
post:
tags: [machines]
operationId: pauseMachine
summary: Pause a Machine, capturing a Snapshot
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"202":
description: The pause was signalled.
content:
application/json:
schema:
$ref: "#/components/schemas/Machine"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/machines/{id}/resume:
post:
tags: [machines]
operationId: resumeMachine
summary: Resume a paused Machine or recover an interrupted Machine
description: |
Requires available Tenant CPU and memory quota and Host capacity. API
preflight can return 409; admission repeats atomically in the workflow.
If a concurrent allocation wins, the Machine stays paused or interrupted
with the refusal in its status error. Interrupted recovery boots the
surviving disk on its current Host with the same Identity after runtime
cleanup and admission. Failed recovery preserves the disk and Snapshots.
Recovery is explicit; a returning Host does not restart Machines itself.
Resume of an already running Machine is idempotent.
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"202":
description: The resume was signalled.
content:
application/json:
schema:
$ref: "#/components/schemas/Machine"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/PoolExhausted" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/machines/{id}/fork:
post:
tags: [machines]
operationId: forkMachine
summary: Fork a Machine into one or more new Machines
description: |
The children's IDs are minted here, so the answer names them before any
of them exists. Each starts in phase `creating`.
parameters:
- $ref: "#/components/parameters/MachineID"
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/ForkRequest"
responses:
"202":
description: The Fork was signalled and the children named.
content:
application/json:
schema:
$ref: "#/components/schemas/MachineList"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/PoolExhausted" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/machines/{id}/commit:
post:
tags: [machines]
operationId: commitMachine
summary: Publish a Machine's disk as a new version of a Template
description: |
Promotion (ticket 061). The Machine's disk becomes the version that new
Machines of that Template start from, so what it accumulated -- a warm
Docker store, a filled cache, an installed toolchain -- is what they
begin with.
The Machine is paused first if it is running, and is left paused and
unchanged. Only the disk is published: the new version is booted once
on the Host to take the Machine's Identity, SSH host keys, machine-id
and any CI runner registration out of it, and a Golden Snapshot of the
result is captured at the Machine's own vCPU and memory shape.
`version` is required and the answer is `202`: the commit takes minutes
and the version appears in `GET /v1/templates/{name}/versions` when it
exists. Committing the same (template, version) twice is the same
commit, not a second one.
A Tenant key can commit a Machine it manages into its Tenant's named
Template. Another Tenant may use the same name independently. Platform
names are reserved and return `409` to customer publication.
parameters:
- $ref: "#/components/parameters/MachineID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CommitRequest"
responses:
"202":
description: The commit was started. The version named is the one being made.
content:
application/json:
schema:
$ref: "#/components/schemas/TemplateVersion"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/machines/{id}/exec:
post:
tags: [machines]
operationId: execMachine
summary: Run a command inside a Machine and stream its output
description: |
The response is a chunked `application/x-ndjson` stream, one JSON
object per line, in this order: one `start` event carrying the guest
pid, any number of `stdout`, `stderr` or `pty` events whose `data` is
base64, and one final `end` event carrying the exit status.
The stream's HTTP status is 200 as soon as the Host Agent accepted the
command, so a failure after that arrives as an `error` event rather
than a status code. A command that exits non-zero is not an error: its
status is in the `end` event.
parameters:
- $ref: "#/components/parameters/MachineID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ExecRequest"
responses:
"200":
description: The command ran; its output is the stream.
content:
application/x-ndjson:
schema:
$ref: "#/components/schemas/ExecEvent"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"409":
description: The Machine is not running.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
"502":
description: The Host Agent could not be reached.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
/v1/machines/{id}/snapshots:
get:
tags: [snapshots]
operationId: listSnapshots
summary: The Snapshots of one Machine
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"200":
description: The observed Snapshots, oldest first. Unavailable workflow status returns an error, not an empty list.
content:
application/json:
schema:
type: object
required: [snapshots]
properties:
snapshots:
type: array
items:
$ref: "#/components/schemas/Snapshot"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"429": { $ref: "#/components/responses/RateLimited" }
post:
tags: [snapshots]
operationId: createSnapshot
summary: Snapshot a running Machine without pausing it for good
description: |
Ticket 111. The workflow pauses the Machine, which writes the Snapshot
exactly as a pause does -- the Machine freezes for the seconds the
capture takes -- and resumes it at once, re-admitting through memory
admission like any resume. The answer carries the new Snapshot and, on
`state: running`, `frozen_ms`: the measured pause-to-running window.
The capture is marked with kind `manual`, which automatic retention
never prunes. A Machine that is not running is refused with 409 before
anything is signalled; the workflow refuses a race the same way. If
the resume fails the Machine stays paused with its Snapshot intact,
which `state: paused` reports, and a later resume starts it. When the
inline wait runs out the answer is `state: requested`, and the
Snapshot appears in the Snapshot list when it lands. Snapshots count
against the Tenant's storage exactly as a pause's do.
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"202":
description: The outcome of this request, or that it is still running.
content:
application/json:
schema:
type: object
required: [machine, state]
properties:
machine:
type: string
state:
type: string
enum: [requested, running, paused]
description: requested is accepted and still running; running came back up; paused captured the Snapshot but stayed paused, with the Snapshot intact.
snapshot:
$ref: "#/components/schemas/Snapshot"
frozen_ms:
type: integer
format: int64
description: The pause-to-running window in milliseconds, on state running.
error:
type: string
description: Why the Machine stayed paused, on state paused.
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"409":
description: The Machine was not running, or the capture did not happen.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
"429": { $ref: "#/components/responses/RateLimited" }
/v1/machines/{id}/snapshots/{snapshot}:
delete:
tags: [snapshots]
operationId: deleteSnapshot
summary: Delete one Snapshot of a Machine
parameters:
- $ref: "#/components/parameters/MachineID"
- name: snapshot
in: path
required: true
schema:
type: string
responses:
"202":
description: Accepted for serialized deletion. Poll the Machine for observed Snapshot absence; a fresh Machine error reports Host refusal, including active resume points or dependent Forks.
content:
application/json:
schema:
type: object
required: [machine, snapshot]
properties:
machine:
type: string
snapshot:
type: string
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/machines/{id}/connections:
get:
tags: [machines]
operationId: listMachineConnections
summary: List directed Machine TCP permissions
description: Requires Manage access to the source. Includes retained revocations, ordered by destination and port.
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"200":
description: Durable connection intent and acknowledged state.
content:
application/json:
schema:
type: array
items: {$ref: "#/components/schemas/MachineConnection"}
"401": {$ref: "#/components/responses/Unauthenticated"}
"403": {$ref: "#/components/responses/Forbidden"}
"404": {$ref: "#/components/responses/NotFound"}
"503":
description: Connection management is unavailable.
/v1/machines/{id}/connections/{destination}/tcp/{port}:
parameters:
- $ref: "#/components/parameters/MachineID"
- name: destination
in: path
required: true
schema: {type: string}
- $ref: "#/components/parameters/Port"
put:
tags: [machines]
operationId: putMachineConnection
summary: Allow one directed Machine TCP connection
description: |
Requires Manage access to both distinct Machines in the same Tenant.
Both must be running on the same Host. An unchanged intent keeps its
revision. No address, TAP, Host, or metadata is accepted as authority.
Pending means accepted; applied means this revision was acknowledged
by the Host. Pausing removes active access; resuming the same Machine
identity can restore allowed policy. A Fork never inherits permission.
responses:
"200":
description: Existing acknowledged or failed intent.
content:
application/json:
schema: {$ref: "#/components/schemas/MachineConnection"}
"202":
description: Durable pending intent, including a deferred workflow start.
content:
application/json:
schema: {$ref: "#/components/schemas/MachineConnection"}
"401": {$ref: "#/components/responses/Unauthenticated"}
"403": {$ref: "#/components/responses/Forbidden"}
"404": {$ref: "#/components/responses/NotFound"}
"409":
description: The endpoints are not running on the same Host.
"422": {$ref: "#/components/responses/Unprocessable"}
"503":
description: Connection management is unavailable.
delete:
tags: [machines]
operationId: deleteMachineConnection
summary: Revoke one directed Machine TCP connection
description: |
Retains a monotonic revocation revision. Requires Manage access to the
source and any surviving destination. A stopped or missing destination
does not prevent revocation. Delayed old acknowledgments cannot change
the newer intent. Revoking also stops existing matching streams.
responses:
"200":
description: Existing acknowledged revocation.
content:
application/json:
schema: {$ref: "#/components/schemas/MachineConnection"}
"202":
description: Durable pending revocation.
content:
application/json:
schema: {$ref: "#/components/schemas/MachineConnection"}
"401": {$ref: "#/components/responses/Unauthenticated"}
"403": {$ref: "#/components/responses/Forbidden"}
"404": {$ref: "#/components/responses/NotFound"}
"422": {$ref: "#/components/responses/Unprocessable"}
"503":
description: Connection management is unavailable.
/v1/machines/{id}/ports:
get:
tags: [machines]
operationId: listPorts
summary: List what the Tenant said about a Machine's ports
description: |
The HTTPS Front Door (ticket 041) serves every Machine at
`https://<id>.runin.eu`, which forwards to the default port or, with
none set, the first of 80, 3000 and 8080 that answers, and every port
at `https://<id>-<port>.runin.eu`. Both are private unless the port is
public: a request needs an API key whose Member may use the Machine as
a bearer token, or a share link.
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"200":
description: The ports, lowest first.
content:
application/json:
schema:
$ref: "#/components/schemas/PortList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/machines/{id}/ports/{port}:
put:
tags: [machines]
operationId: putPort
summary: Make a port public or private, or the default
description: |
Updates only supplied fields; omitted fields retain their current values.
A new port starts private and is not the default. Explicit false clears
the corresponding setting. Making a port the default clears that flag
on the other ports, without changing any port's visibility.
parameters:
- $ref: "#/components/parameters/MachineID"
- $ref: "#/components/parameters/Port"
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/PutPortRequest"
responses:
"200":
description: What the Front Door now does with the port.
content:
application/json:
schema:
$ref: "#/components/schemas/MachinePort"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"501": { $ref: "#/components/responses/NotImplemented" }
delete:
tags: [machines]
operationId: deletePort
summary: Forget a port, which makes it private and not the default
parameters:
- $ref: "#/components/parameters/MachineID"
- $ref: "#/components/parameters/Port"
responses:
"204":
description: Forgotten.
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/machines/{id}/shares:
get:
tags: [machines]
operationId: listShares
summary: List a Machine's share links
description: "Revoked and expired links are listed too. The URL is never shown again after the create."
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"200":
description: The share links, newest first.
content:
application/json:
schema:
$ref: "#/components/schemas/ShareList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"501": { $ref: "#/components/responses/NotImplemented" }
post:
tags: [machines]
operationId: createShare
summary: Make a share link to one port of a Machine
description: |
A signed, expiring URL that opens one port of this Machine to anyone
holding it, until it expires or is revoked. Opening it sets a cookie
for that hostname only and redirects to the same URL without the token.
parameters:
- $ref: "#/components/parameters/MachineID"
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/CreateShareRequest"
responses:
"201":
description: The link. `url` is shown here and never again.
content:
application/json:
schema:
$ref: "#/components/schemas/Share"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"422": { $ref: "#/components/responses/Unprocessable" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/machines/{id}/shares/{share}:
delete:
tags: [machines]
operationId: revokeShare
summary: Revoke a share link
description: "The Front Doors cache a check for at most 15 seconds, so a revoked link stops working within that."
parameters:
- $ref: "#/components/parameters/MachineID"
- name: share
in: path
required: true
schema:
type: string
responses:
"200":
description: Revoked. Revoking a revoked link is not an error.
content:
application/json:
schema:
$ref: "#/components/schemas/Share"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/machines/{id}/machine-shares:
get:
tags: [machines]
operationId: listMachineShares
summary: The Machine's Machine shares
description: |
Who the Machine is shared with (ticket 133): the Members that may see
and enter it without managing it. A share for `tenant` is one for
every Member of the Tenant. These are not the share links under
`/v1/machines/{id}/shares`, which open one port to whoever holds the
link.
parameters:
- $ref: "#/components/parameters/MachineID"
responses:
"200":
description: The Machine shares, oldest first.
content:
application/json:
schema:
$ref: "#/components/schemas/MachineShareList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"501": { $ref: "#/components/responses/NotImplemented" }
post:
tags: [machines]
operationId: createMachineShare
summary: Share the Machine with a Member, or with the whole Tenant
description: |
Manage on the Machine is required. Sharing twice keeps one share.
parameters:
- $ref: "#/components/parameters/MachineID"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [member]
properties:
member:
type: string
description: A live Member's name, or `tenant` for every Member of the Tenant.
responses:
"201":
description: The Machine share.
content:
application/json:
schema:
$ref: "#/components/schemas/MachineShare"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"422": { $ref: "#/components/responses/Unprocessable" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/machines/{id}/machine-shares/{share}:
delete:
tags: [machines]
operationId: deleteMachineShare
summary: Remove one Machine share
parameters:
- $ref: "#/components/parameters/MachineID"
- name: share
in: path
required: true
schema:
type: string
responses:
"204":
description: The share is gone.
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/machines/{id}/transfer:
post:
tags: [machines]
operationId: transferMachine
summary: Hand the Machine to another Member
description: |
A Tenant admin's operation (ticket 133). The Machine's Snapshots,
share links and Machine shares stay on it; only the owner changes.
parameters:
- $ref: "#/components/parameters/MachineID"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [member]
properties:
member:
type: string
description: The new owner's name, a live Member of the Tenant.
responses:
"200":
description: The Machine with its new owner.
content:
application/json:
schema:
$ref: "#/components/schemas/Machine"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"422": { $ref: "#/components/responses/Unprocessable" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/templates:
get:
tags: [templates]
operationId: listTemplates
summary: This Tenant's Templates, one row per version
responses:
"200":
description: The Template versions, newest first.
content:
application/json:
schema:
$ref: "#/components/schemas/TemplateList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"429": { $ref: "#/components/responses/RateLimited" }
post:
tags: [templates]
operationId: createTemplate
summary: Build a Template from a Dockerfile
description: |
The Dockerfile is the request body, as text. A Tenant's build runs in a
short-lived builder Machine of that Tenant's (ADR 0013); only an admin
key's build, for platform Templates, runs on the Host. It takes minutes, so this returns as soon as the work is accepted: the
version is named in the answer and its state is followed on
GET /v1/templates/{name}/versions.
`host` builds on one named Host instead of letting Placement choose,
and is admin-only. A Template version lives only on the Host that built
it until replication lands, so pre-warming a Host Group means building
on each Host in it.
Template names are unique within a Tenant. Another Tenant may use the
same name. Platform Template names are reserved and return `409` to
customer publication. Selectors always take names, never storage IDs. A Tenant has one Dockerfile build at a time; a second one
while the first is `building` is a `409` naming it.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateTemplateRequest"
responses:
"202":
description: The build was accepted and a workflow is running it.
headers:
Location:
description: The Template's versions URL, where the state is followed.
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/StartedTemplate"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/templates/{name}/versions:
get:
tags: [templates]
operationId: listTemplateVersions
summary: The versions of one Template
parameters:
- $ref: "#/components/parameters/TemplateName"
responses:
"200":
description: The versions, newest first.
content:
application/json:
schema:
$ref: "#/components/schemas/TemplateList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/templates/{name}/versions/{version}:
delete:
tags: [templates]
operationId: deleteTemplateVersion
summary: Remove one version of a Template
description: |
Retention. A Template that gains a version after every default-branch
job needs the old ones to go, and this is what removes one: the
dataset on every Host that holds it, and its Golden Snapshot with it.
It refuses with `409` while a Machine is still a clone of the version.
A Machine's disk exists only as a delta against the version it was
created from, so destroying the version would take that Machine's disk
with it. The problem document names what is in the way; destroy those
first.
parameters:
- $ref: "#/components/parameters/TemplateName"
- name: version
in: path
required: true
schema: { type: string }
description: The version to remove.
responses:
"204":
description: The version is gone.
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"429": { $ref: "#/components/responses/RateLimited" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/templates/{name}/golden:
post:
tags: [templates]
operationId: captureGolden
summary: Capture a Golden Snapshot of a Template version
description: |
Boots the version once on a Host and captures it, so that a Machine
created from it starts in well under a second instead of booting. The
capture runs on a Host and takes minutes, so this returns as soon as
the work is accepted.
A Golden Snapshot restores only in the shape (vCPUs and memory) it was
captured in, so a Host holds one per shape. Without `size` every size
preset is captured, one after the other; a Template build does the
same on its own once the version is ready. With `size` only that shape
is captured, which is how a custom size is made to restore.
parameters:
- $ref: "#/components/parameters/TemplateName"
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
version:
type: string
description: Which version to capture. Empty is the newest built.
size:
$ref: "#/components/schemas/SizeRequest"
responses:
"202":
description: The capture was accepted and a workflow is running it.
headers:
Location:
description: The Template's versions URL, where the state is followed.
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/StartedTemplate"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"429": { $ref: "#/components/responses/RateLimited" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/tenants:
get:
tags: [tenants]
operationId: listTenants
summary: Every Tenant (admin)
responses:
"200":
description: The Tenants, oldest first.
content:
application/json:
schema:
$ref: "#/components/schemas/TenantList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"429": { $ref: "#/components/responses/RateLimited" }
post:
tags: [tenants]
operationId: createTenant
summary: Create a Tenant in a named Host Group (admin)
description: |
A Tenant is placed in exactly one Host Group and the Host Group is
required here. The retired `internal_networking` field accepts only false. The
same request twice answers 200 with the existing Tenant; a request that
would change an existing Tenant is a 409 pointing at PATCH. Minting an API key for a name nobody has used also
creates a Tenant, but it cannot name a Host Group, and an empty Host
Group means any of them.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateTenantRequest"
responses:
"200":
description: The Tenant already existed in this Host Group.
content:
application/json:
schema:
$ref: "#/components/schemas/Tenant"
"201":
description: The Tenant was created.
headers:
Location:
description: The new Tenant's URL.
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Tenant"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/tenants/{id}:
get:
tags: [tenants]
operationId: getTenant
summary: One Tenant
description: |
A key reads its own Tenant; an admin key reads any. Another Tenant's is
a 404, as for every other resource, so a key cannot probe which Tenant
IDs exist.
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The Tenant.
content:
application/json:
schema:
$ref: "#/components/schemas/Tenant"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"429": { $ref: "#/components/responses/RateLimited" }
patch:
tags: [tenants]
operationId: updateTenant
summary: Change a Tenant's settings
description: |
Admin keys only, including on one's own Tenant. Only the fields present
in the body are changed.
The retired `internal_networking` field accepts false for migration.
A request for true is rejected with 422. Use directed Machine connections.
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateTenantRequest"
responses:
"200":
description: The Tenant as it now is.
content:
application/json:
schema:
$ref: "#/components/schemas/Tenant"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/keys:
get:
tags: [keys]
operationId: listKeys
summary: This Tenant's API keys
description: |
The secret itself is never returned again; only its prefix is.
Without scope=tenant the answer is the caller's own keys; ?scope=tenant
is a Tenant admin's whole-Tenant list, with a Member per key.
parameters:
- name: scope
in: query
required: false
description: |
"tenant" lists every key of the Tenant and needs a Tenant admin;
without it the answer is the caller's own keys.
schema:
type: string
responses:
"200":
description: The keys, newest first.
content:
application/json:
schema:
$ref: "#/components/schemas/KeyList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"429": { $ref: "#/components/responses/RateLimited" }
post:
tags: [keys]
operationId: createKey
summary: Create an API key
description: |
The secret is in the answer and nowhere else: it is stored only as a
SHA-256 hash, so a key that is lost is revoked and replaced.
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/CreateKeyRequest"
responses:
"201":
description: The key was created. This is the only sight of its secret.
content:
application/json:
schema:
$ref: "#/components/schemas/CreatedKey"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/keys/{id}:
delete:
tags: [keys]
operationId: revokeKey
summary: Revoke an API key
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The key is revoked. Revoking a revoked key is not an error.
content:
application/json:
schema:
$ref: "#/components/schemas/APIKey"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"429": { $ref: "#/components/responses/RateLimited" }
/v1/keys/ssh:
get:
tags: [keys]
operationId: listSSHKeys
summary: List the Tenant's SSH public keys
description: |
The keys the SSH Front Door accepts for this Tenant's Machines
(ticket 040). An admin key may name another Tenant with `?tenant=`.
Without scope=tenant the answer is the caller's own keys; ?scope=tenant
is a Tenant admin's whole-Tenant list, with a Member per key.
parameters:
- name: scope
in: query
required: false
description: |
"tenant" lists every key of the Tenant and needs a Tenant admin;
without it the answer is the caller's own keys.
schema:
type: string
- name: tenant
in: query
required: false
schema:
type: string
responses:
"200":
description: The keys, newest first.
content:
application/json:
schema:
$ref: "#/components/schemas/SSHKeyList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"501": { $ref: "#/components/responses/NotImplemented" }
post:
tags: [keys]
operationId: createSSHKey
summary: Register an SSH public key
description: |
One line of an OpenSSH `authorized_keys` file. The SSH Front Door finds
the Tenant from the key a client offers, so a key can be registered by
one Tenant only; a second registration is 409.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateSSHKeyRequest"
responses:
"201":
description: The key is registered.
content:
application/json:
schema:
$ref: "#/components/schemas/SSHKey"
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/Unprocessable" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/keys/ssh/{id}:
delete:
tags: [keys]
operationId: deleteSSHKey
summary: Remove an SSH public key
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The key is removed.
content:
application/json:
schema:
$ref: "#/components/schemas/SSHKey"
"401": { $ref: "#/components/responses/Unauthenticated" }
"404": { $ref: "#/components/responses/NotFound" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/members:
get:
tags: [members]
operationId: listMembers
summary: The Tenant's Members, with their roles
description: Every Member of the caller's Tenant, oldest first (ticket 133).
responses:
"200":
description: The live Members.
content:
application/json:
schema:
$ref: "#/components/schemas/MemberList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/members/{name}/role:
put:
tags: [members]
operationId: setMemberRole
summary: Change a Member's role
description: |
A Tenant admin's operation. The role is `admin` or `member`; demoting
the Tenant's last admin is refused.
parameters:
- name: name
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [role]
properties:
role:
type: string
enum: [admin, member]
responses:
"200":
description: The Member with their new role.
content:
application/json:
schema:
$ref: "#/components/schemas/Member"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/Unprocessable" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/members/{name}:
delete:
tags: [members]
operationId: removeMember
summary: Remove a Member
description: |
A Tenant admin's operation, run as a workflow (ticket 133): the
Member's SSH keys are deleted, their tokens revoked, their pending
invites cancelled, their Machines pass to the removing admin, the
Machine shares naming them are deleted and their name is freed. The
last Tenant admin cannot be removed. If one of their tokens was used
in the last 24 hours the removal answers 409 unless `force` is set. A
second caller whose request joins a removal already running is
answered with the first caller's result: the Machines went to whoever
ran the first.
parameters:
- name: name
in: path
required: true
schema:
type: string
- name: force
in: query
schema:
type: boolean
default: false
responses:
"200":
description: The Member is removed.
content:
application/json:
schema:
$ref: "#/components/schemas/Member"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/invites:
get:
tags: [members]
operationId: listInvites
summary: The Tenant's pending invites
description: Every invite of the caller's Tenant that is not used and
not cancelled, oldest first (ticket 135); an expired one stays listed
and keeps its name until an admin cancels it. A Tenant admin's
operation.
responses:
"200":
description: The pending invites.
content:
application/json:
schema:
$ref: "#/components/schemas/InviteList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"501": { $ref: "#/components/responses/NotImplemented" }
post:
tags: [members]
operationId: createInvite
summary: Invite a Member
description: |
A Tenant admin's operation (ticket 135). Mints a one-time code, stores
only its hash and answers the code once. The pending invite reserves
its name; whether the name is also free among live Members is checked
here and again when the code is redeemed. Default expiry 24 hours,
longest seven days.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name, role]
properties:
name:
type: string
description: 1 to 32 lowercase letters, digits or dashes;
not `tenant` and not a Member's name.
role:
type: string
enum: [admin, member]
ttl:
type: string
description: A duration such as `24h`. Default 24 hours,
longest 168h.
responses:
"201":
description: The invite, with its code. The code is shown once.
content:
application/json:
schema:
$ref: "#/components/schemas/InviteCreated"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/Unprocessable" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/invites/{id}:
delete:
tags: [members]
operationId: cancelInvite
summary: Cancel a pending invite
description: A Tenant admin's operation (ticket 135). The invite's row
goes and its name is free. Cancelling one that was redeemed already is
refused.
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"204":
description: The invite is cancelled.
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"501": { $ref: "#/components/responses/NotImplemented" }
/v1/hosts:
get:
tags: [hosts]
operationId: listHosts
summary: Every Host and what it has left
description: Admin keys only. This is the fleet view, not a Tenant's.
responses:
"200":
description: The Hosts, by Host Group then name.
content:
application/json:
schema:
$ref: "#/components/schemas/HostList"
"401": { $ref: "#/components/responses/Unauthenticated" }
"403": { $ref: "#/components/responses/Forbidden" }
"429": { $ref: "#/components/responses/RateLimited" }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: |
An API key from `POST /v1/keys`, or the bootstrap admin key that
`fsm-control init` prints once. Keys are 32 random bytes in base64url
behind an `fsm_v1_` marker, and are stored only as a SHA-256 hash.
parameters:
Port:
name: port
in: path
required: true
description: A guest TCP port.
schema:
type: integer
minimum: 1
maximum: 65535
MachineID:
name: id
in: path
required: true
description: A Machine ID, `m-` and twelve base32 characters.
schema:
type: string
pattern: "^m-[a-z2-7]{12}$"
TemplateName:
name: name
in: path
required: true
schema:
type: string
responses:
BadRequest:
description: The request body or its parameters could not be read.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
Unauthenticated:
description: No API key, or one that is unknown or revoked.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
Forbidden:
description: The key is valid but may not do this.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
NotFound:
description: No such resource for this Tenant.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
Conflict:
description: The request is at odds with what already exists.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
Unprocessable:
description: The request was read but asks for something that is not allowed.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
PoolExhausted:
description: The Tenant's Pool has no room for this.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
RateLimited:
description: This key has spent its burst.
headers:
Retry-After:
description: Seconds until the next token.
schema:
type: integer
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
NotImplemented:
description: The Control Plane cannot do this yet.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
schemas:
MachineConnection:
type: object
required: [id, tenant, source_machine, destination_machine, host, port, revision, allowed, state]
properties:
id: {type: string}
tenant: {type: string}
source_machine: {type: string}
destination_machine: {type: string}
host: {type: string}
port: {type: integer, minimum: 1, maximum: 65535}
revision: {type: integer, minimum: 1, format: int64}
allowed: {type: boolean}
state: {type: string, enum: [pending, applied, revoked, failed]}
UsageHistory:
type: object
required: [tenant, from, to, group, metering_started_at, complete, buckets]
properties:
tenant: { type: string }
from: { type: string, format: date-time }
to: { type: string, format: date-time }
group: { type: string, enum: [day, month] }
metering_started_at: { type: string, format: date-time }
complete: { type: boolean }
buckets:
type: array
items: { $ref: '#/components/schemas/UsageBucket' }
UsageBucket:
type: object
required: [from, to, vcpu_seconds, memory_gib_seconds, disk_gib_hours, unknown_machine_seconds]
properties:
from: { type: string, format: date-time }
to: { type: string, format: date-time }
vcpu_seconds: { type: number, minimum: 0 }
memory_gib_seconds: { type: number, minimum: 0 }
disk_gib_hours: { type: number, minimum: 0 }
unknown_machine_seconds: { type: number, minimum: 0 }
ResourceUsage:
type: object
required: [used, unit, limit, remaining, limit_status]
properties:
used:
type: [integer, "null"]
format: int64
unit:
type: string
enum: [vCPU, mCPU, MiB, GiB, Machines]
limit:
type: [integer, "null"]
format: int64
remaining:
type: [integer, "null"]
format: int64
description: Only numeric for enforced quotas; negative means exceeded.
limit_status:
type: string
enum: [enforced, unlimited, unconfigured, not_enforced, unavailable]
StorageUsage:
type: object
description: Host-observed ZFS categories. Null means missing or stale; referenced bytes include shared blocks and are not an additional physical charge.
required: [observed_at, complete, disk_allocated_bytes, disk_dataset_bytes, disk_snapshot_bytes, disk_referenced_bytes, memory_snapshot_bytes, guest_filesystem_used_bytes]
properties:
observed_at:
type: string
format: date-time
complete:
type: boolean
disk_allocated_bytes:
type: [integer, "null"]
format: int64
disk_dataset_bytes:
type: [integer, "null"]
format: int64
disk_snapshot_bytes:
type: [integer, "null"]
format: int64
disk_referenced_bytes:
type: [integer, "null"]
format: int64
backup_metadata_bytes:
type: [integer, "null"]
format: int64
minimum: 0
memory_snapshot_bytes:
type: [integer, "null"]
format: int64
guest_filesystem_used_bytes:
type: [integer, "null"]
format: int64
TenantUsage:
type: object
required: [tenant, observed_at, accounting, resources, machines, notes]
properties:
storage:
$ref: '#/components/schemas/StorageUsage'
tenant:
type: string
observed_at:
type: string
format: date-time
accounting:
type: string
const: allocation_ledger
resources:
type: object
required: [cpu, cpu_guarantee, memory, disk, machine_count]
additionalProperties:
$ref: '#/components/schemas/ResourceUsage'
machines:
type: object
description: Counts by lifecycle phase, plus reserved for claims before a Machine row exists. The Machine count limit is the machine_count resource.
required: [running, paused]
additionalProperties:
type: integer
format: int64
notes:
type: array
items:
type: string
Tenant:
type: object
description: A customer account. Owns Machines, Templates and Secrets.
required: [id]
properties:
id:
type: string
name:
type: string
host_group:
type: string
description: Where this Tenant's Machines are placed. Empty is any Host Group.
suspended:
type: boolean
additional_disk_sizes_gib:
$ref: "#/components/schemas/AdditionalDiskSizes"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
CreateTenantRequest:
type: object
required: [id, host_group]
properties:
id:
type: string
description: The TenantID. No spaces, and none of / ? # %.
name:
type: string
host_group:
type: string
description: The Host Group this Tenant's Machines are placed in.
additional_disk_sizes_gib:
$ref: "#/components/schemas/AdditionalDiskSizes"
internal_networking:
type: boolean
deprecated: true
enum: [false]
default: false
description: Retired compatibility field. True is rejected with 422.
UpdateTenantRequest:
type: object
description: |
Only the fields present are changed. A field that is present and false
turns that setting off.
properties:
name:
type: string
host_group:
type: string
additional_disk_sizes_gib:
$ref: "#/components/schemas/AdditionalDiskSizes"
internal_networking:
type: boolean
deprecated: true
enum: [false]
description: False clears historical metadata for migration; true is rejected with 422.
AdditionalDiskSizes:
type: array
uniqueItems: true
description: |
Exact operator-managed service disk capacities, in GiB. Each is 20 or
above 30, and at most 200. Ordinary customer choices remain 10 and 30.
Updating this list replaces it; an empty list clears the allowances.
Only operator credentials may set these allowances.
items:
oneOf:
- type: integer
enum: [20]
- type: integer
minimum: 31
maximum: 200
Problem:
type: object
description: An RFC 7807 problem document.
required: [type, title, status]
properties:
type:
type: string
format: uri
description: A URI naming the kind of failure.
title:
type: string
status:
type: integer
detail:
type: string
instance:
type: string
errors:
type: array
description: One entry per field that was wrong.
items:
type: object
required: [field, detail]
properties:
field:
type: string
detail:
type: string
Size:
type: object
description: What a Machine is allotted.
required: [vcpus, mem_mib]
properties:
vcpus:
type: integer
minimum: 1
maximum: 16
mem_mib:
type: integer
format: int64
minimum: 1
maximum: 32768
disk_gib:
type: integer
format: int64
minimum: 0
maximum: 200
description: |
0 inherits the Template's verified capacity. Customer choices are
10 or 30 GiB; exact additional service capacities require an
operator-managed Tenant allowance. Cannot shrink the Template.
SizeRequest:
description: |
Either a preset name or explicit values. The presets are `small`
(2 vCPU, 4096 MiB), `medium` (4 vCPU, 8192 MiB) and `large` (8 vCPU,
16384 MiB); omitting it is `small`. Every Host holds a Golden Snapshot
of each Template version in each preset, so a preset restores in well
under a second and any other size cold-boots, saying so in
`cold_boot`.
oneOf:
- type: string
enum: [small, medium, large]
- $ref: "#/components/schemas/Size"
Machine:
type: object
description: |
A Machine. The size and address are flattened rather than nested so
that the fields the CLI prints are the fields the JSON has.
required: [id, tenant, phase, template]
properties:
id:
type: string
tenant:
type: string
host:
type: string
phase:
type: string
enum: [creating, running, pausing, paused, interrupted, resuming, recovering, forking, destroying, destroyed, failed]
description: |
Interrupted preserves the disk, Snapshots and Identity after runtime
loss. Compute remains allocated or uncertain until the Host confirms
runtime cleanup. Recovering boots the preserved disk after explicit
resume and admission on the current Host. Failed recovery returns to
interrupted with an error; it does not delete persistent data.
template:
type: string
template_id:
type: string
description: Immutable storage identifier of the source Template. The template field remains its logical name.
version:
type: string
source:
type: string
description: "\"How the Machine came into being: golden, cold or fork.\""
cold_boot:
type: string
description: Why it booted cold, when it did.
parent:
type: string
description: The Machine this one was Forked from.
class:
$ref: "#/components/schemas/MachineClass"
metadata:
type: object
additionalProperties:
type: string
description: |
What the create passed in, handed straight back. This API mints a
Machine's ID and refuses a body that carries one, so a client that
needs its own name for a Machine puts it here and reads it back
from a list. It is echoed, never interpreted.
vcpus:
type: integer
mem_mib:
type: integer
format: int64
disk_gib:
type: integer
format: int64
address:
type: string
gateway:
type: string
tap:
type: string
backup:
$ref: "#/components/schemas/BackupStatus"
backups_enabled:
type: [boolean, "null"]
description: |
The Tenant's requested backup preference (ticket 117). False means
this Machine is opted out of automatic off-Host backups; the Host
starts no new captures once its policy refresh observes it. This is
a request, not an observation: `backup` reports what actually
happened, and its history stays visible after an opt-out. Null is a
Control Plane from before the preference existed.
snapshots:
type: array
items:
$ref: "#/components/schemas/Snapshot"
error:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
BackupStatus:
type: object
required: [recovery_point_secs, observed_at, captured_age_secs, overdue, last_success, last_attempt]
description: "Optional backup observation. Its absence means status is unavailable, not successful. Capture time measures the age of protected data; replication time is upload completion. A Machine opted out of backups never reads as overdue: no new captures are expected, so compliance is not applicable and `overdue` is null while the history stays visible."
properties:
recovery_point_secs:
type: integer
minimum: 1
observed_at:
type: string
format: date-time
captured_age_secs:
type: [integer, "null"]
format: int64
overdue:
type: [boolean, "null"]
last_success:
oneOf:
- $ref: "#/components/schemas/BackupSuccess"
- type: "null"
last_attempt:
oneOf:
- $ref: "#/components/schemas/BackupAttempt"
- type: "null"
BackupSuccess:
type: object
required: [checkpoint_id, captured_at, replicated_at, kind]
properties:
checkpoint_id:
type: string
captured_at:
type: string
format: date-time
replicated_at:
type: string
format: date-time
kind:
type: string
enum: [disk, paused]
snapshot:
type: string
manifest_version:
type: string
BackupAttempt:
type: object
required: [checkpoint_id, started_at, finished_at, state]
properties:
checkpoint_id:
type: string
started_at:
type: string
format: date-time
finished_at:
type: [string, "null"]
format: date-time
state:
type: string
enum: [running, succeeded, failed]
error:
type: string
MachineList:
type: object
required: [machines]
properties:
machines:
type: array
items:
$ref: "#/components/schemas/Machine"
CreateMachineRequest:
type: object
required: [template]
properties:
template:
type: string
version:
type: string
description: Empty is the newest version the Host holds.
size:
$ref: "#/components/schemas/SizeRequest"
class:
$ref: "#/components/schemas/MachineClass"
backups_enabled:
type: boolean
default: true
description: |
The requested backup preference for the new Machine (ticket 117).
Omitted is true, which is what every Machine has until its Tenant
says otherwise. A disposable Runner Machine passes false; a Service
Machine keeps the default.
metadata:
type: object
additionalProperties:
type: string
MachineClass:
type: string
enum: [flex, reserved]
default: flex
description: |
The Machine's CPU class, chosen at create time (ticket 055). `flex`,
the default, is guaranteed a share of each vCPU (a quarter unless the
Host Group says otherwise) and bursts to all of its vCPUs when the
Host has idle cores. `reserved` is guaranteed every vCPU it has and is
never oversubscribed. Memory is the same for both: never
oversubscribed. A Fork has its parent's class, and the class of a
running Machine cannot be changed.
ForkRequest:
type: object
properties:
count:
type: integer
minimum: 1
maximum: 32
default: 1
snapshot:
type: string
description: Which Snapshot to Fork from. Empty is the newest.
backups_enabled:
type: boolean
description: |
The children's requested backup preference (ticket 117). Omitted
inherits the parent's current preference, read at the Fork; an
explicit value overrides it either way.
Snapshot:
type: object
required: [machine, id]
properties:
machine:
type: string
id:
type: string
disk:
type: string
mem_mib:
type: integer
format: int64
kind:
type: string
enum: [pause, manual]
description: pause is an automatic Snapshot a pause wrote (only the newest is kept); manual is one kept until deleted. Absent is a Snapshot from before the field and reads as pause.
created_at:
type: string
format: date-time
ExecRequest:
type: object
required: [cmd]
properties:
cmd:
type: string
description: The program. Nothing splits a string, so a pipeline is /bin/sh -c.
args:
type: array
items:
type: string
env:
type: object
additionalProperties:
type: string
cwd:
type: string
user:
type: string
description: Who to be inside the Machine. Empty runs as the Machine's recorded login user.
tty:
type: boolean
description: Give the command a terminal; its output then arrives as pty events.
cols:
type: integer
rows:
type: integer
timeout_ms:
type: integer
format: int64
description: Give up on the command after this long. 0 waits for it.
ExecEvent:
type: object
description: One line of the NDJSON exec stream.
required: [event]
properties:
event:
type: string
enum: [start, stdout, stderr, pty, end, error]
pid:
type: integer
description: On a start event.
data:
type: string
format: byte
description: On a stdout, stderr or pty event.
exit_code:
type: integer
description: On an end event.
exited:
type: boolean
description: False on an end event when a signal ended the command.
status:
type: string
description: The guest's own description of how the command ended.
message:
type: string
description: On an error event.
CommitRequest:
type: object
required: [template, version]
properties:
template:
type: string
description: The Template to publish into. Usually the Machine's own.
version:
type: string
description: |
The version to publish as. Required: a commit is idempotent on
(template, version), so a caller retrying one has to be able to ask
for the same version again. The Runner Controller names them
`<yyyymmdd>-<sha>`.
TemplateVersion:
type: object
required: [id, name, version]
properties:
id:
type: string
description: Immutable storage identifier. Existing Templates retain their old name as ID; new Templates use tpl- followed by a UUID. This is not a customer selector.
name:
type: string
description: Name within the owning Tenant, or a reserved platform name.
version:
type: string
tenant:
type: string
state:
type: string
enum: [building, ready, failed]
description: Where this version's build has got to.
golden_state:
type: string
enum: [none, capturing, ready, failed]
description: Where the Golden Snapshot capture of this version has got to.
error:
type: string
description: What the Host said about a build or a capture that failed.
golden:
type: boolean
description: |
Whether a Host really holds a Golden Snapshot of this version,
which is what decides whether a Machine starts in a second. It
comes from the Hosts' capacity reports, so it lags golden_state by
up to one report after a capture.
host:
type: string
golden_shapes:
type: array
items:
type: string
description: |
The shapes some Host holds a Golden Snapshot of this version in,
as `<vcpus>x<mem_mib>`, smallest first. A Machine of one of these
shapes restores; any other size cold-boots.
built_at:
type: string
format: date-time
StartedTemplate:
description: |
A Template version whose build or capture has just been accepted, with
the workflow doing it and where to watch.
allOf:
- $ref: "#/components/schemas/TemplateVersion"
- type: object
required: [workflow, status_url]
properties:
workflow:
type: string
description: The Temporal workflow ID doing the work.
status_url:
type: string
description: Where to read the version's state.
TenantList:
type: object
required: [tenants]
properties:
tenants:
type: array
items:
$ref: "#/components/schemas/Tenant"
TemplateList:
type: object
required: [templates]
properties:
templates:
type: array
items:
$ref: "#/components/schemas/TemplateVersion"
CreateTemplateRequest:
type: object
required: [name, dockerfile]
properties:
disk_gib:
type: integer
format: int64
minimum: 0
maximum: 200
default: 10
description: |
Template root filesystem size in GiB. Omitted or 0 defaults to 10.
Use 30 for a larger Template. An existing Template volume can grow
but cannot shrink; earlier versions and Machines keep their size.
name:
type: string
version:
type: string
description: Empty is a UTC timestamp.
host:
type: string
description: |
Build on this Host instead of letting Placement choose. Admin keys
only.
dockerfile:
type: string
description: The Dockerfile, as text.
APIKey:
type: object
required: [id, tenant, prefix, created_at]
properties:
id:
type: string
tenant:
type: string
name:
type: string
prefix:
type: string
description: The first characters of the secret, which is how it is looked up.
admin:
type: boolean
created_at:
type: string
format: date-time
revoked_at:
type: string
format: date-time
Whoami:
type: object
required: [tenant, key_id, key_prefix, admin]
properties:
tenant:
type: string
tenant_name:
type: string
description: Empty for a Tenant that has keys and no Tenant record, such as the bootstrap admin Tenant.
host_group:
type: string
key_id:
type: string
key_name:
type: string
key_prefix:
type: string
admin:
type: boolean
member:
type: object
description: Who the key acts as (ticket 133). Absent on a Control
Plane whose Store keeps no Members.
properties:
id:
type: string
name:
type: string
role:
type: string
enum: [admin, member]
KeyList:
type: object
required: [keys]
properties:
keys:
type: array
items:
$ref: "#/components/schemas/APIKey"
CreateKeyRequest:
type: object
properties:
name:
type: string
tenant:
type: string
description: Another Tenant's key. Admin keys only.
admin:
type: boolean
description: Make an admin key. Admin keys only.
CreatedKey:
allOf:
- $ref: "#/components/schemas/APIKey"
- type: object
required: [key]
properties:
key:
type: string
description: The secret, shown once.
Host:
type: object
required: [host, group]
properties:
host:
type: string
group:
type: string
vcpus:
type: integer
mem_mib:
type: integer
format: int64
running_vcpus:
type: integer
running_mem_mib:
type: integer
format: int64
paused_count:
type: integer
templates:
type: array
items:
type: object
properties:
name:
type: string
version:
type: string
golden:
type: boolean
shapes:
type: array
description: The shapes the Host holds a usable Golden Snapshot in.
items:
type: object
properties:
vcpus:
type: integer
mem_mib:
type: integer
format: int64
cpu_template:
type: string
description: |
The Firecracker CPU template the Host applies to every Machine, as
`<name>@sha256:<16 hex>`; empty for none. A Snapshot restores only
on a Host applying the one it was taken with.
reported_at:
type: string
format: date-time
HostList:
type: object
required: [hosts]
properties:
hosts:
type: array
items:
$ref: "#/components/schemas/Host"
SSHKey:
type: object
required: [id, tenant, public_key, fingerprint, created_at]
properties:
id:
type: string
tenant:
type: string
name:
type: string
public_key:
type: string
description: The key without its comment, `<type> <base64>`.
fingerprint:
type: string
description: OpenSSH's SHA256 fingerprint, `SHA256:...`.
created_at:
type: string
format: date-time
SSHKeyList:
type: object
required: [keys]
properties:
keys:
type: array
items:
$ref: "#/components/schemas/SSHKey"
Member:
type: object
required: [id, name, role]
properties:
id:
type: string
name:
type: string
description: Unique among the Tenant's live Members; the name a
removed Member frees.
role:
type: string
enum: [admin, member]
created_at:
type: string
format: date-time
MemberList:
type: object
required: [members]
properties:
members:
type: array
items:
$ref: "#/components/schemas/Member"
Invite:
type: object
required: [id, name, role, created_at, expires_at]
properties:
id:
type: string
name:
type: string
description: The name the redeemer's Member gets.
role:
type: string
enum: [admin, member]
created_by:
type: string
description: The Member id of the admin who created the invite.
created_by_name:
type: string
created_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
description: How long the code still works.
InviteCreated:
type: object
required: [id, name, role, created_at, expires_at, code]
properties:
id:
type: string
name:
type: string
role:
type: string
enum: [admin, member]
created_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
code:
type: string
description: The one-time code. It is shown here and never again;
only its hash is stored.
InviteList:
type: object
required: [invites]
properties:
invites:
type: array
items:
$ref: "#/components/schemas/Invite"
MachineShare:
type: object
required: [id, machine, member, created_at]
properties:
id:
type: string
machine:
type: string
member:
type: string
description: The Member's name, or `tenant` for every Member of the Tenant.
created_by:
type: string
description: The Member that created the share.
created_at:
type: string
format: date-time
MachineShareList:
type: object
required: [shares]
properties:
shares:
type: array
items:
$ref: "#/components/schemas/MachineShare"
CreateSSHKeyRequest:
type: object
required: [public_key]
properties:
public_key:
type: string
description: One line of an authorized_keys file, without options.
name:
type: string
description: Defaults to the key's comment.
tenant:
type: string
description: Register for another Tenant. Admin keys only.
MachinePort:
type: object
required: [machine, port, public, default]
properties:
machine:
type: string
port:
type: integer
public:
type: boolean
description: Served on the HTTPS Front Door with no credential.
default:
type: boolean
description: The port behind `https://<machine>.runin.eu`.
updated_at:
type: string
format: date-time
PortList:
type: object
required: [ports]
properties:
ports:
type: array
items:
$ref: "#/components/schemas/MachinePort"
PutPortRequest:
type: object
properties:
public:
type: boolean
description: Omit to preserve visibility; false makes this port private without revoking share links.
default:
type: boolean
description: Omit to preserve the default route; true selects this port without changing visibility.
Share:
type: object
required: [id, tenant, machine, port, created_at, expires_at]
properties:
id:
type: string
tenant:
type: string
machine:
type: string
port:
type: integer
description: The guest port the link opens; 0 is the default route.
name:
type: string
created_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
revoked_at:
type: string
format: date-time
url:
type: string
description: Only in the answer to a create.
ShareList:
type: object
required: [shares]
properties:
shares:
type: array
items:
$ref: "#/components/schemas/Share"
CreateShareRequest:
type: object
properties:
port:
type: integer
minimum: 0
maximum: 65535
name:
type: string
ttl_seconds:
type: integer
format: int64
description: A day when absent; between 60 seconds and 30 days.