On this page

Service

What is a managed service, as opposed to the launchd or systemd unit it names? The declaration lives in the registry; the unit lives on the host; and the whole service layer exists because the two used to be unrelated.

What it is

A managed service is one entry in a target's services[] array: the launchd/systemd unit, program, arguments and host-side unit path Stado is required to keep running (architecture). The absence of such a layer is the incident it closes: on the July control-host outage com.wisent.weles-api existed on the box and was wedged, but nothing in Stado declared it — so no command could list it, restart it, or even assert that it was supposed to be running (stado-rs/src/deploy/service/mod.rs).

A declaration carries the name the CLI addresses it by, the unit identity (launchd label or systemd unit), the unit-file path, and — when the declaration is the source of the unit rather than a pointer at a plist somebody installed by hand — program and args. That pair is what makes the declaration reinstallable from the document alone: service ensure renders the unit from them, so a host that lost its unit file can be made to run the right thing again (deploy/service/model/managed/service.rs, ManagedService). A declaration that names only a path cannot be reinstalled from the document; repair records declaration_incomplete and alerts until the entry carries its program and args — read the truth with stado service show <name> and write it into the entry (operations).

Sources: registry, recovery, product

The managed set has three sources, and the distinction is load-bearing (deploy/service/model/vocabulary.rs):

Source Meaning
registry Declared in the target's services[]. What adopt/retire/deploy edit, and what stado registry doctor diffs against live host state.
recovery The fixed list every stado host recover pass restarts. Genuinely managed, but by that fixed program and not by the document — so it can be neither adopted nor retired, and is never silently converted into a registry service.
product Located by a shipped product declaration naming both the label and the unit file, addressable without a registry record.

The product name and its exact native unit identity select the same catalog entry, including its required environment. Addressing the object API by com.wisent.always-on.stado-object-api therefore keeps the same defaults as addressing it by stado. Its physical recovery writer preserves other installed unit fields, such as resource limits, instead of erasing what the shared service renderer declared.

Who observes it

Two deliberately separate halves (deploy/service/mod.rs). The read side — stado service list — joins declarations against the latest host beacons and issues no ssh, so it stays answerable while a host is wedged; a stale beacon yields unknown, never a confident active or missing. The write side (restart, deploy, ensure, retire, logs, env) rides the approved host channel with a fixed, narrow remote program. The autonomy cycle joins beacon unit state with a fresh stado service verify reachability sweep and repairs from that evidence (operations).

Where it lives in the store

In the registry document, as targets[].services[]. Every mutation goes through the same validated compare-and-swap write path as stado registry push, so a mutation that would produce an invalid document is refused with nothing uploaded (cli).

Ensure

stado service ensure compares the desired unit with the on-disk definition, launchd's retained program and arguments, and the running executable. It leaves a matching running unit alone and installs a missing one. One pass reports created, restarted, already_correct, converged, or reloaded. A declaration that names its own label is rendered at that label, so an existing unit is reinstallable from the document without becoming a second service competing for the same port. Ensure is also the autonomy cycle's repair path for a proven-missing unit, and the one repair permitted on a silent host's own beacon unit, because the channel answering is the evidence that repair is possible (operations).

On macOS a matching loaded definition can be restarted in place. A changed plist or a different program or argument vector retained by launchd requires bootout and bootstrap after executable and plist preflight; kickstart would reuse the old definition. Success requires launchd readback and the running executable to match. An unreadable or malformed retained definition is refused. If activation or readback fails, ensure attempts rollback only when it retained a genuinely different prior unit file. An already-desired plist is not retried as a supposed rollback; the failure explains why no distinct prior definition was available.

After changing a unit, ensure waits up to 30 seconds for an actual authoritative registry read before recording completion. This also covers a unit that serves the registry itself: a new PID can appear before its object API is ready. Only retryable read failures are retried; the host action and conditional registry write are never repeated. Permanent refusals return immediately. If registry or audit recording fails, the error states the completed action, running PID, and original cause, including for --json callers; it does not pretend that nothing happened on the host.

Adopt

stado service adopt brings a unit that already exists on the host under management. Adoption requires proof: Stado records a corrected path or unit record only when the unit is loaded and its live process matches the declared program; unproven ownership is recorded as identity_unresolved rather than guessed, and never duplicated (operations). The registry record is built from what the host actually reported — the resolved unit id, path, and init system — not from what the operator hoped (deploy/service/ops/files/records.rs, record_from_report).

Retire

stado service retire removes a service from management: bootout/disable and forget (cli). It is deliberately not "remove this service" — stado service remove is the operation that also stops the unit and deletes its declared file. Removing the last declaration drops the services key entirely, so a host with nothing declared reads the same as one that never declared anything (deploy/service/model/document/registry.rs). Recovery-sourced units are never written into the registry by any of these paths (operations).

Converge

stado service converge <host> --json is the read-only comparison of declared, installed, attested, and running bytes. --apply closes only the drift it can prove. For Stado, the global product release owns units that execute $HOME/.stado/bin/stado; every registry declaration that executes Stado from an independently installed $HOME/.stado/services/... tree is completed afterwards from the same catalog-verified archive through service update. Private readers are selected from the registry, not from a fixed label list.

The archive stays in the version/platform release tree so a partial failure is resumable. An already-attested global root is not reinstalled or kicked merely to repair one old private reader; an absent or corrupt retained copy is fetched and verified without root activation. service update is idempotent for a private tree already on that archive and refresh-image --if-needed leaves its matching process running. A stale tree is installed and its process image must be proved. Any install, lifecycle, or image-proof failure remains a failed service converge --apply, and the queue agent keeps its existing installed-release handshake rather than being interrupted mid-slot.

Failed private updates retain their child JSON or full stdout and stderr in stado-readers.detail; the enclosing report does not replace that cause with a closing brace or generic status. Stado Desktop calls the authenticated typed API for both report and apply; it does not launch a second CLI process. GET /api/service/converge?target=<host>[&binary=<name>] requires converge-read; the corresponding bodyless POST requires converge-apply. Both call the same implementation as stado service converge.

A completed request returns HTTP 200 with the full report and exit_code, including a failed delivery. Desktop retains that complete JSON and exit code through refresh, including fields it does not recognize. Missing or action-ineligible credentials return 401, unavailable verifier state returns 503, and malformed query parameters or a request body return 400. A failure before a report exists returns 503 with SERVICE_CONVERGE_FAILED and its cause. See Registry API access for the separate client bearer and server verifier grant.

Reader resume requires the target receiver to be built from a release containing the retained-archive command arguments. A numeric version banner alone does not establish that source identity. A receiver built without the contract rejects those arguments and the apply remains failed; normal root delivery of a release containing this fix must happen first. No compatibility shim converts an older source into success.

Commands that act on it

stado service list
stado service show <name>
stado service ensure <name> --host <host> --reason <why>
stado service adopt <unit> --host <host>
stado service retire <unit> --host <host>
stado service restart <name>
stado service logs <name>
stado service converge <host> --json
stado service converge <host> --apply --json

Full flags and the deploy/declare contract in the cli reference.

Not to be confused with

  • The unit itself. The unit is host state; the service is the fleet's declaration of it. service restart weles-api and service restart com.wisent.weles-api address the same declaration by its logical name or its unit id (deploy/service/model/managed/service.rs).
  • A service directory entry. stado://service/<name> routing — who may call a service and where it currently answers — is the directory, a different primitive in the same document.
  • A release. Installing signed product bytes is the release flow; the service declaration is what must keep running regardless of which bytes are current.

Source: this website