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):

  1. Read the current object and its store generation.
  2. Refuse a payload that would delete a top-level key the current generation carries, unless --force.
  3. Refuse a payload whose service_directory.generation is 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).
  4. Refuse removal of every target from a populated fleet unless --allow-empty-fleet separately authorizes it. --force alone does not.
  5. 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