On this page
Runbook
Something in the fleet looks wrong — which command do you run first, and what does its answer mean? Each entry below starts from the symptom, names the first command, says how to read it, and names the next one. Flag-by-flag detail lives in cli.
A host went quiet
Read host diagnosis.
A service reads missing, failed, or unknown
Read service diagnosis.
A public download fails while Funnel configuration looks enabled
release origin is unreachable means the public release route did not receive
an HTTP response from its configured origin. A published release, a reachable
private address, or tailscale funnel status showing an enabled listener does
not prove that the public Funnel path served the request.
Read the failed host's retained Tailscale messages through host exec. Choose
the command for that host's operating system, not the machine running Stado:
# macOS: Tailscale app, network extension, and daemon messages
stado host exec <target> --json -- log show --last 1h --style compact --info --debug --no-pager --process Tailscale --process IPNExtension --process io.tailscale.ipn.macsys.network-extension --process tailscaled
# Linux: the tailscaled systemd journal
stado host exec <target> --json -- journalctl --unit tailscaled --since -1h --no-pager --output short-iso
These fixed commands read the previous hour of already retained native logs. They do not start a live stream, enable logging, restart Tailscale, change Funnel settings, or make a test connection. The JSON receipt preserves the host, actual argv, native stdout and stderr, exit code, and failure sentence. Read the timestamps and the failing process's own message before naming the cause. Empty output is an absence of retained evidence, not a healthy Funnel verdict; an access refusal or unavailable logging program remains a failure.
The exact command must be supported by the Stado client running it. Changing
the time window, adding a process or unit, or requesting a logging mutation
returns '<command>' is not an approved host-exec command before contacting
the host. An older client returns the same refusal and lists its supported
commands.
In Desktop, the same read is under Hosts → Retained Tailscale logs; see Read retained Tailscale logs. Neither a completed read nor a successful publication supplies a public download verdict.
A write to the object API answers 401
The sentence is 401 {"error":"unauthorized or non-immutable release write"}, and it names neither the prefix nor the grant. The object gateway
authorizes a write by matching its key against the configured namespace's
prefix allowlist; a key whose prefix is outside that allowlist can never
authorize, whatever token you present.
First command:
stado config show
Read object_api_namespaces: each namespace carries its verifier item and
its prefix_policies, each with a prefix and the actions it allows. If
the key you are writing does not start with a listed prefix, that is the
whole diagnosis. This is exactly why every autonomy object and every
host-silence record is rooted under state/: no namespace declared
autonomy/ or host_silence/, so the whole layer's writes were refused
with this sentence, and state/ is a canonical prefix that is authorized
wherever the queue prefixes are. The fix is to put the object under an
allowlisted prefix — state/ for fleet state — not to widen the token. A
verifier item that cannot be read answers 503, not 401, so a 401 is always a
scope or bearer answer.
Every object read answers 503 after a Stado delivery
The sentence is 503 {"error":"object authorization unavailable"} on every
stado storage, registry and release read, and it started when a new Stado
reached the control host. The object API checks the host's configuration
with the rules of the binary that serves it; a release whose rules the host's
configuration does not satisfy closes every boundary at once. The registry,
the release records and stado release rollback are behind that same API,
and stado release version converge refuses to downgrade a host that runs
a newer version than it declares, so none of them can undo it.
First command, from any client that still reaches the host:
stado release version show --host <TARGET>
A host-ahead verdict on stado names the version the host declares. Every
delivery keeps the archive it installed under
~/.stado/releases/stado/<version>/<platform>/ on the host, so that version
is still there. Build the current Stado on the host and run its
release local restore there:
stado host run deliver <TARGET> <stado checkout> .stado/work/runs/<id>/stado
stado host run build <TARGET> --manifest-path <HOME>/.stado/work/runs/<id>/stado/stado-rs/Cargo.toml --bin stado
stado host run attach <TARGET> --program <HOME>/.stado/work/runs/<id>/stado/stado-rs/target/release/stado --arg release --arg local --arg restore --arg --version --arg <declared version>
release local restore installs the retained archive through the same checked
install and reader convergence as release local install: the restored
binary validates the host's configuration before anything is replaced, and
every unit running the replaced binary is moved to the installed file. An
unknown version is refused with the list of versions the host retains. When
the broken Stado cannot say which version ran before it, --previous in place
of --version <declared version> reinstalls the retained version whose staged
stado is byte for byte ~/.stado/bin/stado.previous — the binary every
install moves aside when it replaces stado, and whose release reclaim never
removes. It is refused, naming the file, when that rollback target is absent
or no retained release stages it. The
host is recovered when stado storage stat stado://probierz/registry.json
answers present and version show reads in-sync.
A delivered Stado stops on every start
Read startup diagnosis and recovery.
Reads look healthy while writes fail
Storage is single-writer with automatic read failover: mutations commit to
the configured primary and mirror to a read-only disaster-recovery backend, a
failed primary read may be served from the backup, and the backup is never
promoted to writer. The trap is the asymmetry: every write can be failing
while reads keep answering — from stale backup data. This ran in the field:
autonomy writes were refused for days while the local backup backend kept
serving stale reads, which is why stado optimize status still printed a
confident forecast.
So when writes fail, distrust every fresh-looking read and check its own
timestamp through the fleet plane: reported_at on beacons (stado service list prints the age when it is past threshold), inventory freshness in
stado optimize status. A confident answer with an old reported_at is a
stale read, not health. There is one object plane — the configured Stado
backend; the writer has no cloud CLI, provider SDK, direct bucket URL, or
cross-backend fallback. See disaster-recovery.
A job is stuck
First commands:
stado status <job-id>
stado machine logs <job-id> --cursor 0 --limit 1048576
status gives the queue's view; machine logs pages the canonical command
log by byte cursor.
Then check proof of life before concluding anything is dead. The running
document's lease_expires_at is its worker's own promise: the time by which
it said it would renew (its poll period plus the lateness and write time of
its newest renewal, so one renewal a store held for hours does not make the
worker promise hours from then on). While that time is ahead, the job is
alive. A lease_expires_at hours ahead of a job whose work directory shows no
process is a worker that died right after such a renewal. Once it has passed, a
pulse at status/<job_id>/heartbeat written after it means the renewal is
failing and the worker is not, and a blob under the job's checkpoint prefix
written after it means a multi-gigabyte upload is starving the renewal. Only
a job whose promise passed with neither write after it is requeued
(worker lease expired; <n>s past its worker's promise). A job with no
lease_expires_at was claimed by an older Stado and is kept; the reaper
counts it as unpromised.
To classify a failed job fast, grep its stdout against the failure mode quick-grep table. For the job lifecycle itself, see jobs.
ensure refuses
stado service ensure is idempotent and honest: each of its refusals in the
field names its own repair.
- A per-login unit on an always-on host. The declaration puts the unit in
launchd's user domain, but nobody is logged in graphically on an always-on
host, launchd builds no
gui/<uid>, and the system domain is the only one that host can load a unit into. The finding is printed as one sentence ending in the one privileged install command (sudo /usr/bin/install -m 644 -o root -g wheel <plist> /Library/LaunchDaemons/...) to run on the host.stado service listandstado registry doctorreport the same finding before any restart trips over it. - A LaunchAgent loaded in the background domain. When the account does
not hold a graphical session, a LaunchAgent goes to
user/<uid>, and the report says why:<account> does not own /dev/console (it belongs to <owner>), or(it cannot be read (<stat's words>))when the console's owner could not be read, or<account> owns /dev/console but launchd answers no gui/<uid>. An unreadable console is never reported as one belonging to nobody. - A loaded unit running a different program. The refusal reads
<domain>/<unit> is loaded and runs [<declared argv>], not [<argv>]; retire it first. launchd holds the definition it bootstrapped, so rewriting the plist under a live job changes nothing an operator can see;ensurerefuses rather than silently overwrites. Retire the unit (stado service retire <unit> --host <target>), then ensure again. - Registry push refused on directory generation. A registry write
replaces the whole document, so a push that would delete top-level keys, or
whose
service_directory.generationwould go backwards (which would make every stale cached directory start looking current), is refused. The refusal spells the repair: re-pull, re-apply the edit, and push again;--forceonly if the deletion or the older directory is genuinely the intent.
A release did not land on a host
Read release diagnosis.
Source: this website