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
First command:
stado host ping <target>
ping probes two independent signals — ssh reachability and health-beacon
age — and its verdict is the worse of the two (ok, stale, down). The
exit status follows the combined verdict, so a box that answers ssh with a
five-day-old beacon fails this command; that split exists because a host once
answered ssh perfectly for five days while its beacon writer was wedged.
Read it as two different repairs: ssh up with a stale beacon is a host that
is running and not reporting; both signals down is a host that is
unreachable.
Next command:
stado host link <target>
link is the host's own account of why it went quiet: beacon age, the
tailnet path and endpoint it last published, its last sleep and wake, recent
interface changes, the silences recorded against it, and what readers refused
because of them. A silence record opens when the newest beacon crosses the
fleet silence threshold (default 300 seconds, STADO_SILENCE_THRESHOLD_SECONDS)
and its started_at is the last moment the host was heard from, so the
duration is the outage, not the polling interval. Reader refusals are counted
over the last hour by reason — directory_cache_stale,
authority_unreachable, beacon_stale — and each record carries the
refusing component's own sentence verbatim, so the string you grep for is one
that exists in a source file. The exit status follows the verdict; blockers
are named in the report.
For a host that answers while its beacon is stale, link also reads the
managed beacon publisher's own declared log. If beacon_publisher.code is
verifier_unavailable, run:
stado host repair-link <target>
The repair resolves the object API authority from the service directory, reconciles its existing least-privilege verifier grant without rotating the bearer or restarting a service, then waits for the host's normal publisher to write a newer beacon and closes the silence. It refuses every other publisher code; use the returned diagnosis rather than trying a restart.
A service reads missing, failed, or unknown
First command:
stado service list
list answers from the latest health beacons alone — no ssh — so it still
reports on hosts that are currently broken. STATE is the host's own word
about its unit; OBSERVED is when anybody last confirmed the service from
outside. A beacon older than the fleet silence threshold turns every unit on
that host to unknown, with the age and the threshold spelled out in
DETAIL: health beacon is <age>s old, past the <threshold>s silence threshold; unit state is unknown. Read unknown as "the host said nothing
usable", which is deliberately not the same answer as missing.
Next commands:
stado service status <name>
stado service logs <name> --host <target>
status adds best-effort host reads — launchd's last exit status and the
stderr tail — for units whose beacon state is failed; those reads degrade
to a note, never to a failed command. logs tails the unit's log over the
approved channel.
The autonomy loop will also act on this without you. Every stado optimize run and scheduled tick joins the beacon's unit state with a fresh endpoint
sweep: a failed or missing unit is reasserted through the idempotent
service ensure path, a live process running a stale copy of its own
declared binary is kicked in place, and a process executing a binary the unit
never declared stays refused as identity_unresolved. The verdict lands in
state/autonomy/services/latest.json and an immutable
state/autonomy/services/runs/<timestamp>.json; stado optimize status
prints the latest report. Whether the plan is only recorded or actually
executed depends on the autonomy mode — see autonomy and the
missing service reconciliation table.
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.
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 job
writes a per-job heartbeat at status/<job_id>/heartbeat (via the agent's
status watchdog), deliberately decoupled from the agent's capacity broadcast:
a fresh heartbeat means the agent is alive and busy, and the reaper defers.
The second proof is a fresh blob under the job's checkpoint prefix — a
multi-gigabyte checkpoint upload saturates outbound network and starves the
small heartbeat PUT, so a stale heartbeat with fresh checkpoint writes is a
job that is alive and mid-upload, not an orphan. Only when both signals have
aged out is the job genuinely dead and requeued.
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 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
First command:
stado service converge <target>
Three verdicts: in-sync, drifted, and unknown for a binary whose
installed version could not be read — unknown is never folded into either
of the other two, so an uninstalled reporter cannot masquerade as drift.
Reporting exits non-zero on drift alone; --apply delivers the declared
version through stado host release and exits non-zero unless every binary
in scope is confirmed in-sync.
If the host keeps refusing a version it was already given, check quarantine. The release agent quarantines a digest that failed to become ready and never retries it on its own — correct, since a candidate that dies in ninety seconds must not respawn in a loop.
stado release quarantine list <product> --target <target>
stado release quarantine clear <product> --target <target> \
--digest <digest> --reason "<why this digest gets another chance>"
clear starts nothing and kills nothing: it removes one map entry, and the
agent's next tick finds the desired digest no longer quarantined and rolls it
out on its own. --reason is required and recorded in the audit trail beside
the host's rollout state. See primitives/release.
Source: this website