The platform-release view — installed, available, eligibility, faults (admin).
const url = 'http://localhost:8080/v1/admin/platform/releases';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use reqwest;
#[tokio::main]pub async fn main() { let url = "http://localhost:8080/v1/admin/platform/releases";
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Authorization", "Bearer <token>".parse().unwrap());
let client = reqwest::Client::new(); let response = client.get(url) .headers(headers) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request GET \ --url http://localhost:8080/v1/admin/platform/releases \ --header 'Authorization: Bearer <token>'One read that answers the whole admin Releases page: the installed identities (this control plane plus every host), the releases available on the configured channel newest first with their notes, the per-target eligibility FOR THE NEWEST LISTED RELEASE with a stable reason identifier, and the faults. READ-ONLY AND NEVER A TRIGGER: it applies nothing, writes nothing, and does not run detection - detection is a jobs-framework job and “Check now” is POST /v1/admin/jobs/{job_id}/run. A release below the installed control plane’s schema_version is never listed on either channel (ADR 0002), and a stable release whose manifest is missing or invalid is not listed either (ADR 0001 - there is nothing to pin it by); the latter is not a fault in this view - the platform.release_detect job counts it in its run summary (amendment 26).
Authorizations
Section titled “Authorizations”Responses
Section titled “Responses”OK.
The whole admin Releases page in one read (GET /v1/admin/platform/releases).
object
The instance’s channel. Everything in available is what it selects; another channel’s releases are never mixed in. On beta the entries carry channel stable, because beta reads those rows rather than storing its own.
ADDITIVE (#104). The configured release repository as owner/name (QUASAR_PLATFORM_RELEASE_REPO, default accreleus/quasar), so a client can compose the GitHub links a release view needs - the release page, the commit, and the issues the notes reference - instead of hard-coding a repository the operator may have re-pointed. “” when detection is switched off, which a client reads as “render no links”, never as the default. Optional in the schema so a pre-#104 server stays conformant; a client must read absent as “”.
The branch the edge channel follows. Reported on BOTH channels so a UI can render the control without a second read; it selects nothing while channel is stable.
When detection LAST SUCCEEDED; null before the first success. It is NOT when this response was computed - a view served from a week-old list must say so.
The last detection failure as operator prose, cleared on the next success. Prose, never parsed - the machine-readable record is the job’s run history. A stale checked_at WITH a non-null last_error is the normal shape of “failing since then”.
object
What one component IS. Served for the control plane by GET /v1/admin/platform/identity and carried inside the release view.
object
Stamped semver without a leading “v”. “dev” when the binary was built unstamped (a developer build, or a source deploy that skipped the linker flags). NEVER NULL - a build always has some answer, and “dev” is the honest one.
Full 40-character lowercase hex; null on an unstamped build.
RFC3339 UTC; null on an unstamped build.
The highest migration version the binary embeds (the 0NNN file number as an integer). ALWAYS KNOWN, because it is derived from the embedded migration set rather than a build flag - which is why it, and not semver or built_at, is the ordering key everywhere in this surface (ADR 0002).
Owned when this machine’s recovery actor answered; otherwise null. The enum is the host’s; this amendment defines only when owned is reported. A transient failure to reach the actor also reads null, so a developer apply to the control-plane target is refused 409 target_not_owned until it answers; the updater_socket preflight surfaces that condition.
Semver of this machine’s recovery actor; null when absent or unparseable.
7-40 lowercase hex, the commit that recovery actor was built from.
Opaque version of the seed the recovery actor last saw on this machine.
Owned = Quasar created this database and takes its pre-update dump; external = the operator’s own database, only used, so a migrating update needs external_backup_confirmed. What tells a client when to show that confirmation and backup_space. A client meeting an unrecognized value shows it as unknown.
Non-null exactly when a recovery actor created this control plane, whether or not the actor is answering (install_mode alone reads null then); null on a Compose or source control plane, and from an older server. A client meeting an unknown value shows the shape as unknown and treats no host as this machine’s.
Non-null exactly when machine_role is. On combined, the node name the machine’s own agent registers under. A client identifies the control plane’s own host only when machine_role is combined and the host’s node_name equals this; never on control_only, where a GPU host could share the name.
One host’s installed identity, as last reported on the agent register message.
object
7-40 lowercase hex, stored exactly as the agent sent it; null until reported.
Owned is appended by amendment 14 (#353) and is eligible exactly like registry.
Whether an updater sits on this host’s stack. NULL IS NOT false: null = no amendment-aware agent has registered, false = an agent looked and found none. The first is an old agent, the second a real gap an operator must close. On an owned host (amendment 14): whether its recovery actor answered on the agent socket.
True only when all four of source_commit, built_at, install_mode and updater_present are non-null. A CLIENT MUST READ THIS RATHER THAN RE-DERIVING IT, so “what counts as known” cannot disagree between server and client. A host with identity_known false is never eligible for an apply.
ADDITIVE (amendment 14, owner addition on #353). SERVER-DERIVED, in the same posture as identity_known: true when the host’s reported agent_version OR recovery_actor_version orders below the installed control plane’s floor by SemVer precedence (an absent or unparseable version is never below). The console’s “must update before it can be managed”: such a host is offered only an update. It changes no eligibility, and eligible true still always carries reason null. A server implementing the amendment always serializes it; optional here, and a client reads absent as false.
Releases the configured channel selects that are still offerable, NEWEST FIRST (schema_version DESC, then built_at DESC - the tiebreak matters because an edge channel produces many builds at one schema_version). On BETA, SemVer 2.0.0 precedence is inserted between those two keys, because that channel is the only one whose rows can arrive out of version order: 0.2.0-rc.2 orders below 0.2.0, which orders below 0.2.1-rc.1, whatever order they were built in. A release below the installed control plane’s schema_version is NEVER here (ADR 0002); neither is one whose VERSION orders below an installed prerelease at the same schema_version, which is what stops a switch back to stable offering a downgrade (amendment 3); a prerelease is never here on stable; a stable or beta release with a missing or invalid manifest is never here either (and is not a fault - amendment 26).
One detected platform release (schema.md platform_releases). Ordering wherever a list of these appears is schema_version DESC, then built_at DESC.
object
Stable across detections; the handle amendment 2’s apply will name.
The channel this row is STORED on, which is not always the instance’s channel: beta has no rows of its own and reads stable’s, so a release listed while the instance is on beta reports stable here. Unchanged by amendment 3; preview added by amendment 48 (#840), whose builds are rows of their own.
Stable semver without a leading “v” (“0.2.0”, “0.2.0-rc.1”). NULL ON EDGE - an edge build is a commit, not a version, and a synthesized one would be rendered to an operator as if it were real.
Full 40-character lowercase hex. The identity on both channels.
The second ordering key.
The highest migration the release’s control-plane image embeds. The first ordering key and the ADR 0002 gate.
True for a prerelease tag. A stable-channel read never lists one, a BETA read lists it (that is what the channel is for); on edge it is reported as found.
Release notes, MARKDOWN, verbatim from the GitHub Release’s body field (which the publish workflow takes from the changelog section). “” ON EDGE - no notes exist and compare_url stands in for them. Never null, so a client renders one type. UNTRUSTED UPSTREAM TEXT: sanitize at render.
On edge, the GitHub compare link from the installed control plane’s source_commit to this release’s. Null on stable (the notes are that), and null on edge when the installed commit is unknown - there is nothing to compare from.
The platform-release-manifest.json asset attached to a stable GitHub Release, produced by the publish workflow from the same tag as the notes so digests and prose cannot disagree. Served back VERBATIM as PlatformRelease.manifest. NOTE THE TWO VERSION FIELDS ARE DIFFERENT THINGS: format_version versions THIS DOCUMENT’S FORMAT, schema_version is the DATABASE MIGRATION VERSION the release’s control-plane image embeds (ADR 0002’s ordering key). Not to be confused with scripts/release/release-manifest.json in the quasar superproject, which is the release-preflight input set and a different file - the asset is named platform-release-manifest.json precisely so the two never collide.
object
The manifest format’s own version; 1 today. A consumer that meets a format_version it does not know treats the manifest as INVALID (the release is not listed, and no fault is raised) rather than guessing.
Semver without a leading “v”.
Mirrors the tag’s prerelease status - what keeps a release candidate off the stable channel.
40 lowercase hex.
The highest migration the release’s control-plane image embeds.
EXACTLY TWO ENTRIES, IN THIS ORDER: control-plane, then node-agent. THE ORDER IS NORMATIVE, NOT PRESENTATIONAL - #108’s producer validator emits and checks exactly this sequence, so a consumer validates positionally rather than searching by name and a reordered manifest is invalid rather than quietly accepted. The platform release is the control plane (which carries the web client) and the node agent; app catalog images have their own version and push machinery and never appear here.
object
A registry reference with NO TAG AND NO DIGEST - the repository name alone (e.g. ghcr.io/accreleus/quasar/quasar-control-plane). Consumers compose image@digest with the digest field beside it. A TAG IS NEVER AN IDENTITY (ADR 0001), and the digest is named in exactly one place, so an image that already carried a tag or a digest could only disagree with the field that is authoritative.
Sha256:<64 lowercase hex>. The only form that cannot be moved under a running fleet.
AMENDMENT 14 (#353). The platform-release-manifest.v2.json asset of an RH06-era release: format 1’s fields with their grammar and meaning unchanged, THREE components and a FLOOR. Published under its own asset name so a consumer that reads only the format-1 asset never sees it. Served back VERBATIM as PlatformRelease.manifest. A signed release carries platform-release-manifest.v2.json.sig, ADR 0003’s document format over this asset’s exact bytes. Full semantics: control-api.md §“RH06 — Quasar-owned installation”, “Release manifest format 2”.
object
2 for this shape. A consumer that meets a format_version it does not know treats the manifest as INVALID (the release is not listed) rather than guessing.
Semver without a leading “v”.
Mirrors the tag’s prerelease status.
40 lowercase hex. One commit for every component.
The highest migration the release’s control-plane image embeds (ADR 0002’s ordering key).
EXACTLY THREE ENTRIES, IN THIS NORMATIVE ORDER: control-plane, node-agent, recovery-actor - validated positionally, as in format 1. The seed and Postgres are never components.
object
A registry reference with NO TAG AND NO DIGEST, exactly as in format 1 (ADR 0001).
Sha256:<64 lowercase hex>.
EXACTLY TWO ENTRIES, IN THIS ORDER: node-agent, recovery-actor. Each names the OLDEST release of that component this release’s control plane still manages (CONTEXT.md “Floor”); a host below it reads below_floor and is offered only an update. No floor version may order above the manifest’s own version.
object
Semver without a leading “v” - the same grammar as ReleaseManifestV2.version. Compared by SemVer precedence.
AMENDMENT 48 (#840). The preview manifest: one document at the tag preview of quasar-release-manifest in the platform registry, the single layer of an OCI image manifest. Format 2 with three differences: version is null, prerelease is false, and it may carry commits. Every component was built from source_commit. Served back VERBATIM as PlatformRelease.manifest. Full semantics: control-api.md §“Platform-release preview channel”.
object
A preview build is a commit, not a version.
40 lowercase hex. Every component was built from it.
As in format 2.
Exactly as in format 2: control-plane, node-agent, recovery-actor, in this order.
object
A registry reference with NO TAG AND NO DIGEST, exactly as in format 1 (ADR 0001).
Sha256:<64 lowercase hex>.
Exactly as in format 2, except that no floor version is compared with version, which is null.
object
Semver without a leading “v” - the same grammar as ReleaseManifestV2.version. Compared by SemVer precedence.
OPTIONAL. The build’s first-parent history, newest first, from source_commit back to, and not including, the newest stable release tag reachable from it; cut to the newest 500. Absent is valid (new_commit_count is then null). Only a preview manifest may carry it.
object
40 lowercase hex.
One line: #<number> <title> for a pull-request merge, the commit’s subject line otherwise. UNTRUSTED UPSTREAM TEXT: render as plain text, never as markdown or HTML.
ADDITIVE, amendment 6 (#153). True when applying this release runs at least one migration on this instance - its schema_version is above the installed control plane’s. THE ONE THING that decides whether a fleet apply’s control-plane step drains the instance (see the apply section), so it is DERIVED AND SERVED rather than left to a client to re-derive, exactly as identity_known is: a client twin of the rule would keep naming the old policy after the rule moved. Always present. With no release to apply, read the cautious answer, true.
ADDITIVE, amendment 48 (#840). On a preview release whose manifest carries commits, how many of its leading entries the installed control plane does not run: the entries before the one whose commit equals the installed control plane’s source_commit, or all of them when none does. Null on every other release. A client shows the first new_commit_count entries of manifest.commits. DERIVED AND SERVED, like migrates. Always serialized by a server implementing amendment 48; a client reads absent as null.
When THIS instance first saw the release. Provenance, not ordering - two instances can legitimately disagree.
One entry per target - the control plane, then every registered host - each evaluated against available[0]. When available is empty every target is eligible:false with reason “no_release”.
One target’s eligibility, EVALUATED AGAINST available[0] - the newest listed release - and against nothing else. This surface carries no per-release eligibility matrix and a client must not present one.
object
Null for the control-plane target.
Null for the control-plane target.
Why a target is not eligible for the newest listed release. A CLOSED vocabulary of STABLE IDENTIFIERS the UI maps to text - the server never sends the sentence, so wording can improve in the client with no contract change. Precedence is fixed and is the order listed here, so two implementations cannot disagree about which of several true reasons is reported. A client meeting an unrecognized value renders it verbatim rather than dropping the row. Full per-value semantics: control-api.md §“Platform releases”. AMENDMENT 2 (#104/#114) APPENDS attempt_in_flight and run_active AT THE END, so no existing evaluation changes: they are the most transient facts on the list, and amendment 1’s rule that durable reasons outrank transient ones fixes their position. attempt_in_flight precedes run_active because it is about THIS target. AMENDMENT 9 (#185) INSERTS preflight_blocked after control_plane_not_first and before the two transient ones: a stack shape (an unmounted socket volume, a squatted health port) is a durable fact, and the rule that durable reasons outrank transient ones is what fixes its position. The only target whose answer changes is one that is BOTH blocked and mid-apply, which now reads preflight_blocked. The failing check and its fix are on the same target’s preflight. A preflight of unknown never produces this reason. AMENDMENT 14 (#353) APPENDS below_floor: the host’s agent or recovery actor would be, or already is, older than the floor the installed control plane declares (the oldest agent and recovery-actor release it still manages). It is produced by the per-host revert refusal (409 host_not_eligible) and NEVER on a targets entry - a target is evaluated for an update to available[0], which is what a below-floor host is offered - so it takes no position in the precedence above. Amendment 14 also reads “install mode is registry” as “registry or owned”, and up_to_date on an owned host as “agent AND recovery actor on the release’s commit”.
ADDITIVE (amendment 9, #185). Whether this target’s stack is SHAPED so that an apply can be carried out - a different question from eligible (may it take the release) and from a host’s readiness (can it run sessions). Always serialized by a server implementing the amendment; absent on an older one, which a client reads as unknown. Evaluated whether or not a release is listed: the stack-shape checks are useful on their own, and only image_resolvable needs a release. state blocked is what produces the preflight_blocked eligibility reason; unknown never blocks anything.
object
“ok” when every check passed; “blocked” when at least one failed; “unknown” when none failed but at least one could not be evaluated. Not an enum, as above.
When the facts behind this evaluation were gathered - for a host, when its agent last reported readiness; for the control plane, when its updater was last asked. Null when nothing was gathered at all.
In the vocabulary’s order; every check the target has, evaluated, so the card can name every fix at once.
object
The CLOSED vocabulary of pre-update checks (amendment 9, #185). The ones a host’s agent can answer about itself - updater_socket, health_addr_bindable - are ALSO that agent’s readiness check ids (agent-api.md readiness), so preflight and the host’s readiness card say the same words about the same fact. Full per-value semantics: control-api.md §“Self-update hardening”. AMENDMENT 14 (#353) APPENDS owner_conflict (both targets, owned only: a Quasar-looking container without this installation’s labels is on the machine; also the host agent’s readiness check id, never carrying blocks) and backup_space (the control-plane target, owned with a Quasar-owned database only: room for the pre-update dump when available[0] migrates). updater_stack_dir and updater_overlays RETIRED with RH06-15 (#367, the RH06 contract step): no target emits them, they are removed from this enum, and both ids stay reserved (never reused). A client meeting an unrecognized id renders it verbatim.
Known values: “pass”, “fail”, “unknown” - unknown means the collector could not look (an agent predating the check, an updater that did not answer), which is itself a finding but not a blocker. DELIBERATELY NOT AN ENUM, for the same reason ReadinessCheck.status is not: a consumer MUST pass an unrecognized value through rather than reject it.
Operator prose. On a fail it NAMES THE FIX (the command, the variable, the port). NEVER PARSED and never branched on - the id and status are what a client keys off.
object
Everything wrong that is not an ineligibility. A CLOSED vocabulary. A fault gates nothing anywhere - it is reported so a wrong state is visible instead of silent. Prefixed (rather than a bare FaultKind) because “fault” already names an unrelated thing in this system - an NVIDIA Xid is a GPU fault. Full semantics: control-api.md §“Platform releases”. Amendment 26 (#157) removed a third value, manifest_invalid, that no control plane ever emitted.
Null on an instance-scoped fault. Neither kind in the vocabulary is one today (amendment 26), so both name a host.
Null on an instance-scoped fault.
Operator prose. NEVER PARSED and never branched on - the kind is what a client keys off.
What is in flight right now, carried on the release view so the Releases page needs one read. ONE FIELD RATHER THAN A PER-TARGET ONE: a per-target “waiting on N sessions” field would cover only a fleet run and would put the same attempt in two places in one response, where the two can disagree.
object
One fleet apply (schema.md platform_apply_runs): one release applied across the instance, control plane first then eligible hosts in sequence (ADR 0002). AT MOST ONE IS ACTIVE PER INSTANCE, enforced by a partial unique index rather than by a code check, so two admins pressing Apply at once produce a unique violation and not two fleets moving.
object
A fleet run’s state. A run succeeds only when EVERY target it reached succeeded, and it STOPS AT ITS FIRST FAILED TARGET - past a failed control plane, continuing would move agents onto a release the control plane is not on (ADR 0002); past a failed host, it would march a known-bad digest set across the fleet. A FAILED run has no partial variant: it may have succeeded targets behind it, and the per-target attempts are where that is read. AMENDMENT 9 (#185) APPENDS succeeded_partial: the run reached the end of its host list with nothing failed, but it PASSED OVER at least one host that was behind the release for a reason other than up_to_date (its agent was offline, its preflight was blocked, it is source-built, it has no updater) - so the fleet is on mixed versions. Terminal, and NOT a failure: it does not suppress an unattended release, and the next unattended pass or a “retry” (a plain fleet apply of the same release carrying retry_of) picks the host up once it can take the release. A client renders it distinctly from succeeded - the skipped list and its reasons are the outcome, not a footnote.
ADDITIVE (amendment 9, #185). The run this one was started to finish - an admin pressed “Retry skipped hosts” on a succeeded_partial run. Provenance only: the retry is an ordinary fleet apply of the same release, and the hosts it moves are decided at their turn exactly as always (the already-updated ones read up_to_date and are skipped). Null on every other run, and on a server predating the amendment. ON DELETE SET NULL, so deleting the original leaves the retry standing.
Applied to EVERY host target in this run. It exists on the fleet body, and not only on the per-host one, because a run whose every host target waits for a natural drain can otherwise stall indefinitely.
ADDITIVE, amendment 8 (#122). True when the detection schedule started this run rather than an admin pressing Update. A CLIENT SHOULD SAY SO: an admin finding a fleet run they did not start is owed the explanation. requested_by cannot answer this - it is null for an unattended run AND for one whose requesting admin has since been deleted (ON DELETE SET NULL). Always false on a run an admin created, and false on a server predating this amendment. An unattended run NEVER carries force: force is an operator agreeing to end N live sessions and there is no operator, so an unattended run is only ever started for a release whose migrates is false (see the apply section).
The cancel FLAG, not the cancel state. Read BETWEEN TARGETS and never mid-attempt. It is persisted rather than an in-memory signal so a cancel survives a control-plane restart - which matters here more than anywhere, because a fleet run’s first target IS a control-plane restart.
The target the run is on now; null when pending or terminal.
Operator prose for a run-level failure belonging to no attempt. Never parsed.
Hosts the run passed over as ineligible at their turn.
A host a fleet run passed over. An ineligibility is NOT a failure - a run must not go failed because a host happened to be offline - so a skip produces no attempt row and is reported here instead, which is what answers “why did my host not move” from the run itself.
object
Why a target is not eligible for the newest listed release. A CLOSED vocabulary of STABLE IDENTIFIERS the UI maps to text - the server never sends the sentence, so wording can improve in the client with no contract change. Precedence is fixed and is the order listed here, so two implementations cannot disagree about which of several true reasons is reported. A client meeting an unrecognized value renders it verbatim rather than dropping the row. Full per-value semantics: control-api.md §“Platform releases”. AMENDMENT 2 (#104/#114) APPENDS attempt_in_flight and run_active AT THE END, so no existing evaluation changes: they are the most transient facts on the list, and amendment 1’s rule that durable reasons outrank transient ones fixes their position. attempt_in_flight precedes run_active because it is about THIS target. AMENDMENT 9 (#185) INSERTS preflight_blocked after control_plane_not_first and before the two transient ones: a stack shape (an unmounted socket volume, a squatted health port) is a durable fact, and the rule that durable reasons outrank transient ones is what fixes its position. The only target whose answer changes is one that is BOTH blocked and mid-apply, which now reads preflight_blocked. The failing check and its fix are on the same target’s preflight. A preflight of unknown never produces this reason. AMENDMENT 14 (#353) APPENDS below_floor: the host’s agent or recovery actor would be, or already is, older than the floor the installed control plane declares (the oldest agent and recovery-actor release it still manages). It is produced by the per-host revert refusal (409 host_not_eligible) and NEVER on a targets entry - a target is evaluated for an update to available[0], which is what a below-floor host is offered - so it takes no position in the precedence above. Amendment 14 also reads “install mode is registry” as “registry or owned”, and up_to_date on an owned host as “agent AND recovery actor on the release’s commit”.
The per-target attempts, in the order the run reached them (control plane first).
One attempt to move ONE target - the control plane, or one host - to one digest set (schema.md platform_apply_attempts). Every apply produces one: fleet or standalone, apply or revert, success or failure.
object
The fleet run this belongs to. NULL for a standalone per-host apply or revert - those are attempts with no run, because a run carries fleet ordering and a single-host action has none.
A REVERT IS AN APPLY WITH AN OLDER DIGEST SET - same wire message, same states, same reasons. This field exists so history can say which button was pressed, and for nothing else. AMENDMENT 9 (#185) APPENDS auto_revert: no button was pressed - the host’s UPDATER put the previous digests back itself after the new agent container failed its health wait (agent-api.md release_state restored), and the control plane wrote this row beside the failed apply so the history shows both steps. It is recorded succeeded on insert (the updater reports restored only for a restore that came up; a restore that itself failed leaves no row and both failures in the failed apply’s output), was never driven over the wire, and its requested_digests are the failed apply’s previous_digests. Amendment 14 (#353): on an owned host the recovery actor did the restore, and the row names ONLY the restored component (the one whose replacement failed), which may be recovery-actor. Amendment 14 (owner addition on #353) APPENDS developer_apply: an admin applied an arbitrary digest set to an owned target (POST /v1/admin/platform/developer-apply); release_id is null, and it is otherwise an ordinary apply and revert source. A client meeting an unrecognized kind renders it verbatim.
Null exactly when target is control_plane.
The release the digests came from, when one is still known. NULL is legitimate on a revert, whose digest set may correspond to no release row this instance still has - requested_digests, not this field, is the authority for what an attempt did.
What was asked for, in manifest order - the exact components array sent on agent-api.md release_apply. THE AUTHORITY for what this attempt did.
One component of a platform release, pinned. Same shape as ReleaseManifestComponent and as agent-api.md release_apply.components.
object
The component. Only “node-agent” is ever sent to a registry host; an owned host (amendment 14, #353) may also be sent “recovery-actor”, and the ORDER of the list is the order of replacement. “control-plane” is applied by the updater or recovery actor beside the control plane and never over an agent connection.
Registry repository reference with NO TAG AND NO DIGEST. Consumers compose image@digest (ADR 0001).
Sha256: followed by 64 lowercase hex.
What the target was on before. Empty until first reported. What a revert reads back, and what the manual restore recipe in a failure is copied from.
What a component was on BEFORE an attempt. Recorded from every release_state, in every state and not only failing ones, because the previous digests are what restores a half-failed stack and something has to have written them down before the restore is needed (#113 prototype finding 5). This is what a revert reads.
object
Null when the updater could not determine it. NEVER OMITTED - a client must be able to tell “nothing was there” from “nobody looked”.
One target’s attempt state. The six middle values are EXACTLY agent-api.md release_state.state, relayed unchanged. queued and waiting_sessions are control-plane-only and precede the wire (the command has not been sent); cancelled applies ONLY to an attempt a cancel caught in one of those two states - CANCEL NEVER INTERRUPTS AN ATTEMPT THAT HAS BEEN SENT. Amendment 14 (#353) rewords, without changing any value: recreating means the target’s actor is replacing a component’s container (on an owned machine the old one is stopped and kept until the new one is verified), not a Compose command.
Why an attempt failed. A CLOSED vocabulary of STABLE IDENTIFIERS the UI maps to text, shared verbatim with agent-api.md release_state.reason and with the release_apply ack’s error, so ONE client-side mapping serves the wire, this API and the history. Non-null exactly when the state is failed. “unsupported” is written by the control plane and never sent on the wire: no ack arrived within the 10s ack timeout, so the agent build predates the amendment. A client meeting an unrecognized value renders it verbatim. “signature_missing” and “signature_invalid” are amendment 5’s two APPENDED values (#120): a host that requires a signed release met an unsigned one, and a manifest signature that did not verify (bad signature, untrusted key, digests not named by the signed manifest, a signature that could not be fetched, or verification on with no trusted keys - all fail closed). Signature verification is OFF BY DEFAULT (ADR 0003), so neither occurs unless an operator turns it on. AMENDMENT 14 (#353) APPENDS five values, produced only by an owned machine and each meaning “nothing changed”: recipe_unsupported (the recovery actor lacks the recipe revision an image declares, ADR 0008), owner_conflict (a Quasar-looking container without this installation’s labels is in the way), backup_failed (control plane, migrating release, Quasar-owned database: the pre-update dump could not be taken), backup_unconfirmed (control plane, migrating release, external database, no external_backup_confirmed) and interrupted (the actor, engine or machine restarted before the old container was taken out of service; settled, never retried). Amendment 14 also confirms signature_missing and signature_invalid as members. Full per-value semantics: agent-api.md §release_state.
The N in “waiting on N sessions”: the last observed non-terminal session count while state is waiting_sessions - ON THIS HOST for a host target, and FLEET-WIDE for the control-plane target, whose recreate is instance-wide. On a control-plane target it is set ONLY when the release’s migrates is true (amendment 6, #153): a recreate alone no longer ends a running session, so a non-migrating control-plane attempt never enters waiting_sessions and leaves this null throughout. A CLIENT MUST SHOW IT IN A FORCE CONFIRMATION - force is the operator agreeing to end N live sessions, and a confirmation that does not name N is not informed consent. Advisory and last-observed, never a gate. Null once the attempt has been sent.
Skip the zero-sessions wait and stop what is running. The agent does no session logic on it: it records which decision the control plane made.
The bounded tail (last 8192 bytes, truncated from the front at a line boundary) of the failing step’s output. “” when there is nothing to report. NEVER a credential or an environment value. Amendment 14 (#353): a failed MIGRATING control-plane attempt on an owned machine is never restored automatically, and its output ends with the one-line restore command naming pre_update_dump and the version it returns to.
ADDITIVE (amendment 14, #353). The DUMP REFERENCE: the name of the pre-update dump the recovery actor took before replacing the control plane with a migrating release, exactly as the operator passes it to the restore command. Opaque - display it, never parse it. Null on every host attempt, on a non-migrating or external-database control-plane attempt, and on a registry control plane. A server implementing the amendment always serializes it; optional here so a client still accepts an older server (absent reads as null).
EVERY open attempt on the instance, whether it belongs to that run or is a standalone per-host apply or revert. A client renders a target’s progress by finding the attempt whose host_id matches the targets entry (null = the control plane) and reading state and sessions_remaining.
One attempt to move ONE target - the control plane, or one host - to one digest set (schema.md platform_apply_attempts). Every apply produces one: fleet or standalone, apply or revert, success or failure.
object
The fleet run this belongs to. NULL for a standalone per-host apply or revert - those are attempts with no run, because a run carries fleet ordering and a single-host action has none.
A REVERT IS AN APPLY WITH AN OLDER DIGEST SET - same wire message, same states, same reasons. This field exists so history can say which button was pressed, and for nothing else. AMENDMENT 9 (#185) APPENDS auto_revert: no button was pressed - the host’s UPDATER put the previous digests back itself after the new agent container failed its health wait (agent-api.md release_state restored), and the control plane wrote this row beside the failed apply so the history shows both steps. It is recorded succeeded on insert (the updater reports restored only for a restore that came up; a restore that itself failed leaves no row and both failures in the failed apply’s output), was never driven over the wire, and its requested_digests are the failed apply’s previous_digests. Amendment 14 (#353): on an owned host the recovery actor did the restore, and the row names ONLY the restored component (the one whose replacement failed), which may be recovery-actor. Amendment 14 (owner addition on #353) APPENDS developer_apply: an admin applied an arbitrary digest set to an owned target (POST /v1/admin/platform/developer-apply); release_id is null, and it is otherwise an ordinary apply and revert source. A client meeting an unrecognized kind renders it verbatim.
Null exactly when target is control_plane.
The release the digests came from, when one is still known. NULL is legitimate on a revert, whose digest set may correspond to no release row this instance still has - requested_digests, not this field, is the authority for what an attempt did.
What was asked for, in manifest order - the exact components array sent on agent-api.md release_apply. THE AUTHORITY for what this attempt did.
One component of a platform release, pinned. Same shape as ReleaseManifestComponent and as agent-api.md release_apply.components.
object
The component. Only “node-agent” is ever sent to a registry host; an owned host (amendment 14, #353) may also be sent “recovery-actor”, and the ORDER of the list is the order of replacement. “control-plane” is applied by the updater or recovery actor beside the control plane and never over an agent connection.
Registry repository reference with NO TAG AND NO DIGEST. Consumers compose image@digest (ADR 0001).
Sha256: followed by 64 lowercase hex.
What the target was on before. Empty until first reported. What a revert reads back, and what the manual restore recipe in a failure is copied from.
What a component was on BEFORE an attempt. Recorded from every release_state, in every state and not only failing ones, because the previous digests are what restores a half-failed stack and something has to have written them down before the restore is needed (#113 prototype finding 5). This is what a revert reads.
object
Null when the updater could not determine it. NEVER OMITTED - a client must be able to tell “nothing was there” from “nobody looked”.
One target’s attempt state. The six middle values are EXACTLY agent-api.md release_state.state, relayed unchanged. queued and waiting_sessions are control-plane-only and precede the wire (the command has not been sent); cancelled applies ONLY to an attempt a cancel caught in one of those two states - CANCEL NEVER INTERRUPTS AN ATTEMPT THAT HAS BEEN SENT. Amendment 14 (#353) rewords, without changing any value: recreating means the target’s actor is replacing a component’s container (on an owned machine the old one is stopped and kept until the new one is verified), not a Compose command.
Why an attempt failed. A CLOSED vocabulary of STABLE IDENTIFIERS the UI maps to text, shared verbatim with agent-api.md release_state.reason and with the release_apply ack’s error, so ONE client-side mapping serves the wire, this API and the history. Non-null exactly when the state is failed. “unsupported” is written by the control plane and never sent on the wire: no ack arrived within the 10s ack timeout, so the agent build predates the amendment. A client meeting an unrecognized value renders it verbatim. “signature_missing” and “signature_invalid” are amendment 5’s two APPENDED values (#120): a host that requires a signed release met an unsigned one, and a manifest signature that did not verify (bad signature, untrusted key, digests not named by the signed manifest, a signature that could not be fetched, or verification on with no trusted keys - all fail closed). Signature verification is OFF BY DEFAULT (ADR 0003), so neither occurs unless an operator turns it on. AMENDMENT 14 (#353) APPENDS five values, produced only by an owned machine and each meaning “nothing changed”: recipe_unsupported (the recovery actor lacks the recipe revision an image declares, ADR 0008), owner_conflict (a Quasar-looking container without this installation’s labels is in the way), backup_failed (control plane, migrating release, Quasar-owned database: the pre-update dump could not be taken), backup_unconfirmed (control plane, migrating release, external database, no external_backup_confirmed) and interrupted (the actor, engine or machine restarted before the old container was taken out of service; settled, never retried). Amendment 14 also confirms signature_missing and signature_invalid as members. Full per-value semantics: agent-api.md §release_state.
The N in “waiting on N sessions”: the last observed non-terminal session count while state is waiting_sessions - ON THIS HOST for a host target, and FLEET-WIDE for the control-plane target, whose recreate is instance-wide. On a control-plane target it is set ONLY when the release’s migrates is true (amendment 6, #153): a recreate alone no longer ends a running session, so a non-migrating control-plane attempt never enters waiting_sessions and leaves this null throughout. A CLIENT MUST SHOW IT IN A FORCE CONFIRMATION - force is the operator agreeing to end N live sessions, and a confirmation that does not name N is not informed consent. Advisory and last-observed, never a gate. Null once the attempt has been sent.
Skip the zero-sessions wait and stop what is running. The agent does no session logic on it: it records which decision the control plane made.
The bounded tail (last 8192 bytes, truncated from the front at a line boundary) of the failing step’s output. “” when there is nothing to report. NEVER a credential or an environment value. Amendment 14 (#353): a failed MIGRATING control-plane attempt on an owned machine is never restored automatically, and its output ends with the one-line restore command naming pre_update_dump and the version it returns to.
ADDITIVE (amendment 14, #353). The DUMP REFERENCE: the name of the pre-update dump the recovery actor took before replacing the control plane with a migrating release, exactly as the operator passes it to the restore command. Opaque - display it, never parse it. Null on every host attempt, on a non-migrating or external-database control-plane attempt, and on a registry control plane. A server implementing the amendment always serializes it; optional here so a client still accepts an older server (absent reads as null).
The instance’s release-notification target and its last delivery.
object
Whether a detected release is announced. Mirrors instance_settings.release_webhook_enabled.
The configured https URL, or “” when none is set. Admin-only, as this whole view is.
Whether a signing secret is stored (instance_secrets platform.release_webhook.secret, or its environment fallback). A BOOLEAN AND NEVER THE VALUE. Signing is optional: Slack, Discord and ntfy authenticate by URL.
One recorded delivery attempt (schema.md platform_release_notifications).
object
The platform_releases row this attempt announced.
That release’s version, or null on an edge build (which has none).
delivered is TERMINAL: the release is never announced again. failed is retried on the next detection pass until the attempt cap, after which the release is left un-notified rather than retried forever.
How many detection PASSES this release has cost, not HTTP requests - the retries within one pass are the server’s business.
The receiver’s HTTP status, or null when the request never got one (DNS, TLS, a refused dial, the egress allowlist). Those are different failures.
Bounded operator prose, null on success. IT NEVER CONTAINS THE WEBHOOK URL: that URL is itself the credential on every receiver that authenticates by URL.
Example
{ "channel": "stable", "installed": { "control_plane": { "install_mode": "registry", "database_mode": "owned", "machine_role": "combined" }, "hosts": [ { "status": "online", "install_mode": "registry" } ] }, "available": [ { "channel": "stable", "manifest": { "components": [ { "name": "control-plane" } ] } } ], "targets": [ { "kind": "control_plane", "reason": "no_release", "preflight": { "checks": [ { "id": "updater_socket" } ] } } ], "faults": [ { "kind": "agent_ahead_of_control_plane" } ], "active_apply": { "run": { "state": "pending", "current_target": "control_plane", "skipped": [ { "reason": "no_release" } ], "attempts": [ { "kind": "apply", "target": "control_plane", "state": "queued", "reason": "updater_absent" } ] }, "attempts": [ { "kind": "apply", "target": "control_plane", "state": "queued", "reason": "updater_absent" } ] }, "release_webhook": { "last_delivery": { "status": "delivered" } }}Missing/invalid/expired/revoked token.
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ], "reason": "example" }}Authenticated but insufficient role / not the owner (precedes resource lookup).
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ], "reason": "example" }}