On this page
Registry
Where does the fleet write down what should be true, and how does a write to it stay safe? The registry is the one canonical document; everything else is observation of it.
What it is
registry.json is the fleet's declaration of what SHOULD be true
(architecture): the versioned compute-target and
coordinator registry, plus the service_directory and placement_profiles
blocks. It is "the single source of truth for every box the queue can route
to" (stado-rs/src/targets/target.rs). One document carries the
targets, the services each target must run, the
service directory, placement profiles, build recipes, and the
fleet queue namespace.
Top-level keys a reader does not model round-trip verbatim through
Registry::extra, so a write never deletes them. This is load-bearing, not
cosmetic: a registry write replaces the whole document, and divergent builds
writing the same object would each erase what they cannot name
(targets.rs; cli/registry/write/guards.rs).
Who declares it
Operators and the validated write path. stado registry push PATH uploads an
explicitly selected local file; stado service adopt|retire|deploy and
stado registry host add edit the document programmatically through the same
path. Validation runs before any store call, so a document that would not
validate never reaches the registry
(cli/registry/write/document.rs, push_document_if).
Select the input explicitly
stado registry validate PATH validates the named file. stado registry push PATH
publishes the named file, while stado registry push - reads standard input.
Neither command selects a source-checkout snapshot, a bundled template or a
file from the current directory when PATH is omitted. A missing PATH is a CLI
usage refusal with exit 2, on terminal and piped input alike. --force and
--allow-empty-fleet do not bypass that requirement. No registry is written.
stado registry pull --with-generation
stado registry validate registry.json
stado registry push registry.json --if-generation GENERATION --json
stado registry push - --if-generation GENERATION --json < registry.json
The pull receipt contains the document and its generation from one read. Save
and edit the document, not the surrounding receipt, and retain that generation
for the conditional push. A source file remains unchanged after publication.
An unreadable push file names the failed operation, selected path and actual
filesystem error as cannot read registry push PATH: ERROR; an unreadable
standard input reports cannot read registry push stdin: ERROR. Invalid JSON
names its input and parser error. These failures never select another source.
Import without replacing existing declarations
stado registry import PATH [--json] adopts a complete existing registry-v2 document additively. It is deliberately different from registry push: import never treats omitted source keys as deletions and has no force flag. The importer validates the complete source, including fleet-to-target relationships, before reading canonical storage. Named targets, fleets, coordinators, and placement_profiles use their name as identity; identical names and content are unchanged, missing names are appended, and different content at an existing name conflicts. Other object fields are added recursively; differing scalar values and arrays are conflicts rather than guessed identities. The fully merged candidate is validated again before a conditional write.
Every conflict refuses the whole candidate, exits 75, and retains the prior canonical generation byte-for-byte. Invalid input is rejected, exits 1, and never opens the destination; storage or verification failures also exit 1. A successful write is read back at its returned generation before the receipt is accepted. Repeating a successful import is idempotent and reports unchanged. --json returns stado.registry-import-receipt.v1 with the source SHA-256, previous and accepted generations, exact imported and unchanged targets and fleets, affected sections, and conflict or rejection reasons. Source files remain where the operator selected them and are never modified or copied.
The native app uses the same operation through POST /api/registry/import. That endpoint requires Content-Type: application/json, Content-Length, a body within the deployment's dashboard.request_limits.registry_import_bytes declaration, a registry API bearer explicitly granted registry-import, and the normal trusted dashboard host. It returns the receipt with HTTP 200 for imported/unchanged, 409 for conflict, 400 for rejected, 415 for another media type, and 503 when canonical storage is unavailable. An oversized body is refused before import with HTTP 413 naming its declared and permitted byte counts; Desktop retains that plain-text reason. Policy actions do not imply import permission. See API request boundaries for the required deployment declaration.
Who observes it
Every reader in the fleet. fetch_registry_remote is the fleet-survival
authority — the coordinator's rogue-daemon kill switch and host-health target
resolution read it, with a 30-second in-process cache. It returns a fetch
error rather than an empty registry: an unreachable store does not prove that
a target was removed from the fleet.
A reader is not required to die with the authority. Every canonical read that
parses and passes the registry-v2 contract is copied to
~/.stado/cache/registry-last-good.json with a dated sidecar, and readers
that must keep answering serve that copy — carrying its age and one sentence
for the operator. The snapshot bundled with the binary sits below the cache
and is reachable only through the auto loader, announced whenever it is used
(targets.rs). The bundled template carries no fleet identities and is never
an implicit input to registry push or registry validate.
The copy is scoped to the store that served it. A canonical read from a LOCAL
filesystem store — WC_STORAGE_BACKEND=local — neither records the copy nor
reads it (stado-rs/src/targets/registry/fetch.rs). The copy exists for an authority
on the far side of a network, which can go away while the reader stays up; a
directory on the same disk has no such state, so there is nothing there to
recover from. A stado scratch lease has its own one-target registry and does
not replace the operator's cached fleet. A missing lease document is not
answered with the operator's fleet.
Where it lives in the store
registry.json at the root of the backend STADO_CONFIG selects
(architecture). The read and write sides address the
same object on every backend (cli/registry/mod.rs).
Pushes: compare-and-swap and generations
A push is fenced, refused-by-default, and verified
(cli/registry/write/upload.rs, upload_payload):
- Read the current object and its store generation.
- Refuse a payload that would delete a top-level key the current generation
carries, unless
--force. - Refuse a payload whose
service_directory.generationis lower than the one already published, unless--force. That counter is what consumers compare their cached directory against, and it only means something if it never goes backwards: a rollback reverts every endpoint corrected since. Resolver caches reject generation rollback (architecture). - Refuse removal of every target from a populated fleet unless
--allow-empty-fleetseparately authorizes it.--forcealone does not. - Compare-and-swap against the read generation (or atomically create when the object is absent), then read back and verify both the generation and the exact bytes.
A stale --if-generation exits 75. With --json, its
stado.registry-push-receipt.v1 names state: conflict, expected_generation
and actual_generation. Storage and validation errors remain failures, not
conflicts. A successful receipt names the accepted generation and the generation
it replaced; read the canonical registry again to inspect the persisted result.
Declaration is not observation
The registry declares; it never reports. stado host inventory and the host
beacons are the observation side, and the reconciliation between the two is
deliberately kept as independent axes — declared version vs installed binary,
declared unit vs beacon state, declared endpoint vs consumer probes
(architecture). Software that installs builds onto
hosts without being able to state the declared version, read the actual
version, and name the difference is not automation. A field like a target's
managed_versions is the declaration half; a target that omits it is
reported undeclared, never as agreeing.
Stado Desktop
Registry → Registry documents → Available operations offers
Read the canonical registry and its generation, Validate a supplied registry
document, and Replace the registry from a supplied document. The document
editor starts empty and requires the operator's JSON. Validation sends it as
an explicit input file; replacement uses the explicit - argument and request
stdin through the selected endpoint's POST /api/operator/run. No local CLI is
launched, and no source snapshot supplies missing content.
Replacement exposes the generation read before editing, the separate key-removal and empty-fleet authorizations, and the normal mutation review. Reads and validation need no mutation confirmation. An unconfirmed replacement returns HTTP 403 and writes nothing. Complete operation receipt retains stdout, stderr, process exit and input-write failures, including generation conflicts. Selecting another endpoint clears the prior review and receipt. The existing policy editor and additive import API keep their own contracts.
Commands that act on it
stado registry validate PATH
stado registry push PATH
stado registry pull
stado registry self
stado registry doctor
stado registry host add HOST --ssh DEST --release-platform PLATFORM
stado host beacon list
Full flags in the cli reference.
Real qualification
tests/desktop_api/registry_inputs.rs runs the actual Stado executable and
native HTTP API against isolated local storage. It supplies real terminal stdin
for missing-source checks, publishes a selected file and a supplied stdin
document, reads their saved content and generations, and verifies missing-input,
malformed-input, unconfirmed-write, stale-generation and empty-fleet refusals.
The source file and stored registry are checked after failures. No operator
registry is modified.
cargo test --locked --manifest-path stado-rs/Cargo.toml --test service-journeys journey::registry_inputs:: -- --nocapture
Run from the Stado checkout when execution is permitted. Reports under
.build/desktop-api/ retain the exact source identity and patch digest, binary
digest, commands, exit statuses, actual HTTP responses and registry observations.
The fixture removes its own home and temporary directory after the service exits.
An unexecuted journey is not a passing qualification, and API coverage does not
replace reviewing the Registry screen on the dedicated graphical host.
Not to be confused with
- The service directory. The directory is a block inside the registry with its own generation counter and authority — see directory.
- A beacon. The beacon is what a host observes about itself; the registry is what the fleet declares about the host.
- The object store. The registry is one object in the store, not the store; job state lives under its own prefixes (architecture).
Source: this website