On this page

Service

A managed service joins a registry declaration to a launchd or systemd unit. The declaration states what should run; host observations show what actually runs. Stado compares both before reporting or changing service state.

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). A unit absent from that managed set is not a managed-service lifecycle target. Inspect it with service list --undeclared or service unit show; service adopt can record a proven existing unit before normal managed lifecycle operations.

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

The units Stado renders for the host agent, for a deployed service and for the object API that a storage transaction restores carry one declared program search path in their PATH, never the installing shell's: on macOS /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin, on Linux /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin (primitives/tool_path), so a tool such as gpg is found by every one of them on a host or by none of them. A precheck runner's launcher sets its own PATH in its installer.

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 repair stado --step host 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 host Stado process by its unit com.wisent.stado therefore keeps the same defaults as addressing it by stado: that one process serves the object API, the release API and every Stado role, so there is no separate object API unit to address. Its physical recovery writer preserves other installed unit fields, such as resource limits, instead of erasing what the shared service renderer declared.

One shipped entry is worth naming, because it is a repair nothing else can perform. brama-grant-sync runs brama subscription sync on a cadence, so every provider grant the harnesses on that machine hold joins the gateway's pool. It exists for the state a gateway cannot leave on its own: every credential it holds is dead, and its automatic sign-in cannot replace them because the provider asks for an authenticator code the vault does not hold. A machine that is still signed in then repairs the gateway. The entry carries no port and no secret — the gateway is resolved through the service directory and the console bearer is read from the vault at every pass.

stado service ensure brama-grant-sync --host <workstation> \
  --as-launch-agent --reason "<why this machine repairs the pool>"

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

Stado Desktop → Services → Managed services keeps the beacon's failed row when the follow-up stado service status <name> --json cannot read its exit evidence. service status reads the last launchd exit code and last stderr line by default; --lines requests a larger log window. The alarm and inspector show both, or the exact host-read refusal when either is unavailable. The beacon's state, time and detail remain visible separately; a failed status read does not turn the fleet-wide list into an empty fleet. A failed release activation reports the unit's last exit and stderr before restoring the retained executable.

stado service serving stado --host <host> --wait --json waits for the declared unit to own its directory port, or refuses when its process exits or settles on another port. It accepts no --port with --wait: there is no declared readiness to prove for an arbitrary caller-supplied endpoint. The refusal includes the last native exit and stderr evidence. Product installation uses this same read before accepting a resident replacement; on failure it restores the recorded exact backup and checks the predecessor's serving state.

Failed service: evidence and repair

Managed stdout and stderr stay on the fleet host. On macOS the launchd unit's StandardOutPath and StandardErrorPath name their files; when stdout has no declared path, Stado reads $HOME/.stado/logs/<unit>.log. On Linux the system or user journal holds the unit streams. Stado does not forward them to a central support inbox or repository maintainer. Read a bounded window over the approved host channel:

stado service logs --lines <LINES> <NAME> --host <HOST>
stado service status <NAME> --lines <LINES> --json
stado optimize status

For a loaded unit that is not declared as a managed service, use stado service unit logs <LABEL> --host <HOST> --lines <LINES> instead. Neither command sends logs to a maintainer by itself.

stado service status --lines includes that window of a failed unit's stderr; without it the status names the service logs command to read the stream. stado optimize status reads the latest reconciliation outcome, not an assertion that application code was repaired.

The default autonomy mode is report: it records a plan without mutation. In an authorized enforcing mode, the reconciler can restore an eligible declared unit from its program and arguments when host and endpoint evidence justify it. It cannot infer or patch a bug in a repository, and it does not submit private logs to a public issue automatically. A missing declaration, unreachable host or inconclusive evidence is a recorded refusal, not a successful repair (autonomy, operations).

Separately, a catalog-declared answering probe can trigger host-local repair steps and a restart when its API request stands unanswered. Inspect stado service status <NAME> --json and stado service recoveries --service <NAME> --json for the actual attempt, refusal and subsequent answer; a running unit or an attempted restart alone does not prove the service recovered.

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

Creating, repairing or restarting a declared unit, one product to one process per host under its one unit: Ensure.

Adopt

Claiming a unit a host already runs: Adopt.

Retire

Stopping a unit and withdrawing its declaration: Retire.

Converge

Reconciling a service's declared placement with what its hosts run, from the CLI, the API or Desktop: Converge.

A service that accepts and never answers is recovered where it runs

For an operational reading of a standing request, see diagnosing an unanswered service.

A service that accepts every connection and answers none passes a readiness read and fails everyone: its /health can answer while the path the service exists for holds every request, every client's resolver records the waits, and nothing on the host that runs the service acts, because nothing there asks the question the clients are asking. A product's catalog service declares that question as answering: the method and path of one request on the service's own API, the JSON body to send, and the repair step of its repair declaration (stado-rs/data/catalog/repair-catalog.json) to run before a restart. Brama declares POST /v1/chat/completions. Skarbiec declares GET /readyz with the crypto and audit-lock repairs: its readiness decrypts the vault's canary item, so a gpg standing behind a held keyring lock leaves the question unanswered, while a listing such as POST /v1/items/list decrypts nothing and answers as fast as ever. Stado declares GET /api/release/object?uri=stado://releases/answering-probe with the object-api repair: the public release route answers before host trust and Skarbiec and reads the store, so a healthy host answers at once that the object is absent, while an object API whose store holds every read leaves the question unanswered and its declared repair runs.

service:
  answering:
    method: GET
    path: /readyz
    repair:
    - crypto
    - audit-lock

On every beacon pass the host process sends the question to the service's recorded listen port on this host. The request runs to its end in a task of its own and its start is recorded, the way the resolver records every wait; the next pass reads the record. A question unanswered longer than this host's declared directory refresh interval (service_resolver.refresh_seconds, the same measure the resolver applies to a held adapter) is a service that stands: the host runs each declared repair step through the same path stado repair <service> --step <name> --target <host> --apply takes, then the restart through the same path stado service restart takes, forgets the question so the next pass asks the new process afresh, attempts an alert on the configured channels, and appends the recovery to ~/.stado/service-recoveries.jsonl when writable. No manual step: the host that runs the service acts first. A refused restart remains a refusal in the beacon. Alert delivery is attempted on the configured channels, with each channel's outcome retained; the answering beacon does not present the attempt as recovered.

The alert says recovery was attempted, names the repair and restart outcomes, and never claims the register already contains the event: the append happens after the alert and may refuse. The beacon reports that refusal with its file operation and cause.

Once the declared repair steps and restart succeed, the host retains that attempt for the continuous failed-answering episode. A later standing reading reports already_attempted with the last recovery instead of repeating the restart. The unanswered question is retired so the next beacon can ask afresh and observe an answer. A successful API answer ends the episode; a later failure can recover again. A refused repair step or restart does not count as complete recovery: the next beacon retries it; the beacon and alert name each refusal, and a writable register retains it. Replacing the managed unit or changing its declared listen port, method, path or body retires the old in-flight question and answer. The next pass asks the new endpoint rather than treating an old wait or HTTP result as its state. Changing the repair steps starts a new recovery episode if the question still stands, so an earlier attempted repair does not suppress the new declaration. Removing the question or being unable to read its directory endpoint retires its pending request and last answer; the beacon names an unreadable endpoint instead of carrying stale readiness.

stado service status <service> --json and stado service list --json carry the beacon's answering.recovery beside the failed API question; the plain status prints the same answering block. Stado Desktop → Services → Inspector shows the attempt, its repair-step refusals, restart cause, register refusal, and failed alert deliveries on the service row. An absent recovery for a standing question is shown as missing evidence, not as a successful restart. stado service recoveries --service <service> reads the recoveries the host could persist. If a restart ran but the register append failed, answering.recovery keeps the observed restart and recording_refusal names the file operation and cause. The host does not restart the same failed episode again merely because that record could not be persisted; the recovery-history command cannot prove the attempt, so inspect the beacon's refusal.

When the host that runs it does not act

A host that runs an older catalog never asks the question that would find its service standing, and a host whose own Stado stands on the service it would judge never reaches the judgement. The other hosts see it anyway: their resolvers hold the service's requests in phase: answer and refuse new ones. So every host's beacon pass also reads its own resolver's waiting list (~/.stado/resolver-waiting.json): a service on another host that has held this host's requests for the first byte of an answer longer than the declared refresh interval triggers a recovery attempt from this host: the same repair steps and restart run on the host that serves it over its registry SSH channel. The steps are fixed host payloads and the restart is the unit's own init system command, so neither needs that host's Stado, which may be the very process that stands. A wait still opening its channel says the host or its SSH daemon does not answer, not the service, and is not acted on.

One standing is acted on once after its declared repairs and restart succeed. The record keeps stood_since, when the oldest held request was accepted, and the complete answering declaration used for that attempt. A later pass reports already_recovered only when the same oldest request still stands, the managed unit and current declaration match that record, and every declared repair step was applied. A changed declaration or an older record without its snapshot cannot prove the current attempt; the peer acts again. A refused repair step or restart is retried on the next pass. A restart ends the held connections with the process that held them, so a service that stands again is a new standing.

The beacon carries peer_recoveries: one block per service and host with since, waited_seconds, bound_seconds, held (how many requests stand), oldest (the oldest wait's sentence: client, request line, endpoint) and one of recovery, already_recovered, or not_recovered with the reason (the catalog declares no answering question or no such service, the registry declares no unit for it on that host, the register cannot be read). A waiting list that cannot be read is carried as unreadable.

If a peer's register append fails after a restart, its beacon names recording_refusal and retains that outcome for the same oldest held request while the observing host process runs. That is not durable history; if the observer restarts before the register becomes writable, a later pass may attempt the standing again.

The Skarbiec crypto step reads readiness before and after skarbiec recover-daemons within the same declared refresh interval, which the caller hands it as SKARBIEC_READY_WITHIN_SECONDS; a caller that cannot read one is refused naming it. A readiness read without a bound stood as long as the stranded gpg children held the vault, and the restart that ends them was never reached.

The beacon carries each probe under the unit's answering: asked (the first question was just sent), waiting with since, waited_seconds and the newest finished probe, stood with since, waited_seconds and bound_seconds plus the recovery record, or finished with answered (status, the HTTP library's successful verdict, elapsed_ms, at, and the actual response detail when the HTTP status is not successful) or refused (the transport's own cause). A fast HTTP 503 is an answer, not a standing request: its response detail diagnoses the failed readiness operation, but the standing-recovery path does not restart the service for that status. The CLI's service list/status prints the beacon's failed successful verdict and response detail. Desktop's Services fleet table and inspector mark the failed answer as needing attention and show that detail; an older beacon missing successful is not shown as healthy. A port the directory does not record, or a resolver that declares no interval, is said in the block as unreadable, never guessed. A service whose catalog entry declares no question carries no block.

stado service recoveries [--service NAME] [--json] reads this host's register, oldest first: when, which service on which host and its unit, the question that stood and the exact answering declaration used (older rows lack this snapshot), since when and for how long against the bound, the requests this host's resolver held unanswered to the service at the time, the ordered repair-step reports (empty when none is declared; older rows may hold one report or null), what the restart answered and whether it ended well, what each alert channel answered, and for an attempt on another host's service observed_by, the host whose resolver observed it. The plain CLI still says recovered from <host> even for a refused repair or restart, and counts an unreadable resolver-list diagnostic as one held request; inspect restarted, the repair statuses and held_requests in --json before treating that line as a recovery or a request count.

A register that is not there holds no recovery; one that cannot be read, or holds a line that is not a recovery, is refused naming the file and the line. The catalog refuses an answering whose method is not in capitals, whose path does not start with /, whose repair is not a list of nonempty step names, or that carries a field outside method, path, body, repair. Omitted or empty repair steps are valid when only the restart is declared. Stado Desktop's Routes operations carry Read this host's service recovery attempts, including refusals, which runs the same history command on the selected host.

The internal probe exercise tests/service_answering/stands.rs (service-answering-stands) asks the question of a real listener on this machine that accepts and never writes a byte and reads it as standing once the fixture registry's declared refresh interval has passed, asks it of one that answers successfully and another that promptly returns HTTP unavailable with a diagnostic body, classifies the latter as unsuccessful without losing the body, forgets the standing one and sees the next pass ask afresh, and writes a recovery to a register under an isolated home, reads it back whole and sees a line that is not a recovery refused. It does not run a managed host's repair or restart. tests/service_answering/cli.mjs reads an actual fresh host beacon through stado host health, service status and service list, plus the unmanaged service refusal. It requires a declared live host/service and a clean source revision, but does not exercise a declaration change or standing recovery.

The resolver-list journey tests/service_answering/peer.rs (service-answering-peer) publishes a waiting list as a running resolver does under an isolated home. It shows that an undeclared standing service is not_recovered with its cause, while a wait still opening its channel, a wait on this host's service, and a wait younger than the bound are not standings. A list whose publisher no longer runs names nothing, and a malformed list is unreadable naming the file. It does not qualify a peer repair or restart: that requires a managed service on a real other host, its current declaration, and the resulting recovery and service state.

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 release version show --host <host> --json
stado release version converge --host <host> --json

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

service logs and status resolve a declared service; logs can also use a product's catalog unit. retire and remove require a registry declaration. For an exact unit name with no matching declaration, stado service logs <unit> --host <host> --json refuses with not a registry-managed service on <host> and failure code not_found. Repeating service remove after its first successful removal has the same class. That is an absent declaration, not an unattributed transport failure. stado service unit show <unit> --host <host> --json can still inspect the host's loaded unit without a declaration. When the unit is loaded with no process and its last run exited nonzero, unit show prints a failing row (failing in --json): <unit> runs no process: its last run exited <code> and its supervisor has started it <runs> time(s) — a release that cannot serve under a keep-alive supervisor, restarted without end. A host release delivery whose restart cannot prove a fresh process on the activated image appends the same sentence to its refusal, <unit>: restart did not prove a fresh pid on the activated immutable image; … ; <unit> runs no process: …, so the delivery names the crash loop it caused.

The real journey tests/service/failing_unit.mjs (STADO_BIN, STADO_SERVICE_TEST_HOST, STADO_SERVICE_TEST_EXIT_CODE) loads two launchd jobs in this account's GUI domain from plists under .build/: one whose program exits with the stated code, one still running. It reads the first through unit show until launchd reports the run ended and checks the failing verdict in --json and the text row, checks that the running job carries none, boots both out and checks that unit show then refuses the label. It writes report.json with the source revision, the binary's digest and every command with its exit status.

Not to be confused with

  • The unit itself. The unit is host state; the service is the fleet's declaration of it. service restart <name> and service restart <unit-id> address the same declaration when those identities belong to the same managed service.
  • 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