On this page
Stado Desktop
Stado Desktop shows the fleet's recorded state and runs the same product operations as the CLI. The product APIs and CLI own the results; Desktop does not keep a second registry or calculate its own delivery verdicts.
Native operator connection
Desktop calls Stado's HTTP APIs. It does not search PATH for a Stado command, start that command, or fall back to it when an API request fails. Hosts, Services, Releases, Products, Databases, Inference, Cloudflare routes, Vast, Scratch targets and credential inspection use the same selected source as the console. Existing typed registry, convergence and storage APIs remain separate from the operator command API.
The command API is POST /api/operator/run, with Content-Type: application/json and X-Stado-Action: operator-command. A read request such
as {"args":["database","list","--json"]} carries no mutation confirmation.
A reviewed change adds "confirmation":"RUN_MUTATION". For example the
Databases declaration corresponds to stado database declare NAME --engine postgres --scope read --consumer CONSUMER --json. The server refuses an
unconfirmed write with HTTP 403 before executing it. Any top-level command the
CLI's own clap tree declares is reachable; a family it does not declare is
refused. Whether a request only reads is judged by a model through Oko's
schema-bound call (oko gateway object) from the argv and the command's own
help: the answer is one object whose choice is READ or CHANGES, with its
reason, bound to a strict JSON Schema, so --dry-run, --check or --apply
count as that command's help says;
--help and --version are reads by the parser's own answer. A request whose
judgement could not be obtained is treated as a change, the cause is written to
the service's log, and the confirmation is required. Loopback retains local
trust; remote access uses the operator's authorized Wisent session.
Receipts retain the operation's exit code, stdout, stderr and refusal. A missing exit code is not converted to zero. Truncated output is not accepted as a complete result. Connection and API failures identify the requested endpoint and operation; installing a local CLI is not their fallback. Responses from a previously selected source are not shown as the new source's results. A mutation already accepted by the server is not undone by switching sources.
Service declarations send their JSON in the request's input field and use
$INPUT in the argument array. Desktop does not pass a Mac-local temporary
filename to a remote service. Secret standard input remains the separate
stdin field.
Product desktop and CLI installation still targets this Mac, not the selected
fleet server. Those lifecycle operations use this device's listener, at the
address this device declares: WC_DASHBOARD_BIND and WC_DASHBOARD_PORT,
then dashboard.bind and dashboard.port from the Stado profile, then the
--bind and --port its com.wisent.stado LaunchAgent runs with — and when
that unit runs --port 0, the address its listener announced
([dashboard] listening on http://…) in the unit's error log. Nothing is
assumed: with none of them Desktop says so and names the declaration to add.
Service lifecycle operations retain the selected source and explicit host.
Profile lookup follows STADO_CONFIG, ./stado.config.json,
~/.config/stado/config.json, then ~/.stado/config.json.
Products shows a recipe's declared local signing keychain beside each surface. A source installation of Skarbiec signs from that keychain on the installation host, even while Skarbiec cannot answer; an absent named keychain or identity refusal appears in the mutation result rather than as a successful install. The completed surface displays the receipt's actual keychain and each installed native file's observed signing authority, team and identifier. An exact published release does not sign again. The screen sends the same installation command through the native operator API and keeps its refusal; it has no second credential reader.
The This Mac backend starts com.wisent.stado with --port 0 and takes
its endpoint from that announcement.
Cloud deployment setup exposes Installer API. An existing Stado API
returns its exact release installer through the read-only equivalent of
stado bootstrap --print-install-script. The cloud image verifies and
installs that release; Desktop does not run a local Stado command to obtain
it. A missing API, refusal or incomplete installer stops provisioning with
its error. Local backend hosting still needs the actual Stado service
executable because launchd runs that backend, not because Desktop uses it as
a command transport. Third-party provider tools retain their own contracts.
Its How it works section takes what the control plane runs with, none of
which Desktop or Stado assumes: the control-plane interval in seconds, the
container port, the container's CPU and memory written as the provider takes
them (Cloud Run and Azure Container Apps 1 and 2Gi, AWS App Runner
1 vCPU and 2 GB), and on Google Cloud the requests one container serves at
once. An empty field, or a port, interval or request count that is not a
whole number above zero, refuses the deployment by name before anything is
created. The selected infrastructure target must state its region (Azure:
location) in its metadata; a target without one is refused rather than
placed in a region Desktop picked.
The source-bound API journey is cargo test --manifest-path stado-rs/Cargo.toml --test service-journeys journey:: -- --nocapture. It starts a
real isolated service, creates, changes and removes a database declaration,
checks its persisted configuration and independent CLI reads, and checks
unconfirmed and invalid-input refusals. It retains source identity, executable
digest, requests, statuses and logs under .build/desktop-api/.
Local by default, no session required
The local control plane is the app's default source. The local path requires
no deployment setup, and the active source is visible in Settings — a local
profile reads Local Stado, a remote one names its deployment, provider, and
endpoint. The operations console works without a Wisent session; account
sign-in is required only for remote deployment actions. The menu-bar app
reopens the native console rather than sending you to a browser.
STADO_REGISTRY_API_URL selects the local source when supplied. Otherwise the
dashboard address comes from storage.stado.url in the selected Stado
profile; with neither, Desktop reports the source as unconfigured. Direct loopback access needs no
Wisent sign-in, but registry, inventory, cleanup, convergence and storage
reconciliation calls still require their dedicated registry API credential.
Settings → Registry API access stores the credential endpoint and token
file path, not the secret value. The endpoint must match the active source.
STADO_REGISTRY_API_TOKEN_FILE overrides the saved path; its default is
~/.stado/registry-api-desktop-token. Desktop rereads that file for every
request and reports a missing, empty or unreadable file before sending a
request. A credential assigned to another endpoint is not sent there.
Environment overrides make the saved fields read-only. The server maps the
client's permitted actions through registry_api.clients; a Wisent session
token is not a replacement. See configuration
for the client and verifier settings.
The server's stado-registry-api-verifier grant and the Desktop client bearer
are separate credentials. Stado 0.16.33 can keep a newly minted verifier grant
on its server with credentials token mint --token-file-name; see
Grant.
The client item remains in Skarbiec, while Desktop reads the matching client
bearer from the file selected above.
The Hosts → Vault bearer sheet exposes that same destination as
Keep bearer on this host, and shows the resulting file path in its
receipt without returning bearer bytes.
Import an existing registry
First run includes an optional Import your existing registry screen. The same file picker remains available after onboarding under Settings → Existing registry, so adoption is reusable rather than a one-time wizard. Choose one .json file. It must be a complete Stado registry-v2 document accepted by stado registry validate; Desktop does not accept CSV, provider inventory, or an inferred interchange format. The API enforces the deployment's dashboard.request_limits.registry_import_bytes declaration; Desktop has no separate file-size limit. An oversized import returns HTTP 413, and the screen preserves the API's plain-text explanation and byte counts. See API request boundaries for the declaration and real qualification commands.
Desktop sends the JSON bytes directly to POST /api/registry/import with action registry-import; it never invokes a shell. The route and stado registry import PATH call the same additive importer and canonical compare-and-swap boundary. Configure the selected Stado endpoint and give that registry API client the distinct registry-import action. Policy-read or policy-write access alone cannot import a registry.
The screen shows the exact imported, unchanged, conflict, or rejected receipt: canonical generation, imported and unchanged host and fleet names, affected sections, and every conflict or validation failure. Equal repeated records are unchanged. Differing records refuse the entire import; invalid JSON, schema, targets, or fleet references likewise leave the canonical bytes unchanged. Existing canonical declarations are never replaced, the chosen source file is not copied, and credential contents are never displayed. An accepted first-run receipt records onboarding completion; skipping leaves the product empty and usable, and the Settings action stays available.
What the screens show
What each Desktop screen shows and which command it runs: What the Desktop screens show.
Read retained Tailscale logs
In Operations Console → Hosts, select a declared host, find Retained Tailscale logs, and choose macOS unified log or Linux journal. The registry projection does not currently supply the operating system, so Desktop leaves this choice empty for each newly selected host rather than guessing from its name or the operator's Mac.
Read retained Tailscale logs shows and submits the fixed
stado host exec <target> --json -- ... command documented in the
runbook.
It reads the previous hour through the configured native
POST /api/operator/run endpoint. Opening the screen or refreshing the fleet
does not start a log read. No live stream, test connection, configuration
change, or restart is requested.
The result stays with its requested host and selected source. Desktop shows the reported host, native command, completion time, process status and exit code, original stdout and stderr, and any refusal. Missing endpoint settings, an unknown registry target, an unavailable logging tool, and denied access remain failures. An undecodable receipt preserves the raw Stado output; dashboard truncation is visibly marked instead of being treated as a complete log. No retained log text does not mean that Funnel or a public download is healthy.
The native operator API reads stdout and stderr concurrently through EOF for all allowlisted commands. It does not clip logs or JSON reports at fixed byte counts. A failed read remains an operation failure; the receipt's truncation flags are false only after the streams were fully read. Desktop still rejects an older server's truncated receipt rather than claiming a complete read.
Whether a host-exec request only reads is judged like every console request,
from its argv and the command's help; one judged a change, including provider
sign-in, keeps the confirmation requirement.
An older server may return mutating commands require explicit RUN_MUTATION confirmation; that refusal does not mean the logs were read. Remote access
still requires the configured operator authorization.
Reconcile storage roots
Inspecting and running a storage root reconciliation from Desktop: Desktop host operations.
Prepare Apple code capture
Reading a Mac's Apple readiness and preparing it for Apple code capture: Desktop host operations.
Add a Machine
The Hosts screen carries an Add a Machine path, reachable from the context
bar and from the empty registry state. The window opens on the ways in rather
than on one of them: the list comes from stado fleet methods --json, so a
method this fleet's registry catalog forbids is shown disabled, naming the
field that forbade it, instead of being missing or dead.
- Invite mints a code, shows it once with the single line to send to whoever has the machine, then waits. The wait survives quitting the app; reopening the window says which machine is expected, what it reported when it answers, and offers approve or reject.
- Adopt takes an address and installs the fleet's public key over a session the control plane can already open — and says plainly that no password can be typed into the window, because the process opening that session has no terminal.
- Join and Declare each state what the operator has to do, with pending and approve behind Join.
- The hand-carried key path remains for the machine nobody can reach: name,
key minting with the public half and its
authorized_keysline to carry, the SSH address, verified enrollment, and the channel and agent proofs. The two proofs run for a machine added by any method.
Every step runs one fleet or host command through the
dashboard's POST /api/operator/run argv bridge — an argv array the CLI's own
clap tree must declare, with the mutation confirmation the
console's own operator page sends. No command string is ever assembled, and
there is no second transport. Enrollment progress belongs to the application
rather than to the window, because adding a machine spans a walk to another
computer: closing the window mid-walk does not cost the minted key. An
enrollment that fails says whether it never reached the machine, or reached
it and rolled its own entry back. The command-line equivalent of the whole
flow is add-your-machine.
A host already in the registry is changed or retired from its inspector on
the Hosts screen: Edit or remove registry declaration… opens a sheet with
the SSH destination, target kind and release platform. Only the fields filled
in are sent, and the sheet shows the exact stado registry host edit HOST --json … line before Save changes runs it. Remove host… shows stado registry host remove HOST --json in a confirmation first. The result is the
CLI's receipt or its refusal word for word — including the list of registry
paths that still name the host, which is why a removal was refused. The
machine itself is not touched by either.
Versioned recovery in the enrollment proof
The final agent proof keeps its existing stado repair stado --step host <target> behavior
when the exact-version field is empty. When an operator chooses a version, the
same action runs exactly:
stado repair stado --step host <target> --release <exact-version>
The screen reads the command's release report rather than inferring progress.
It first shows the reported release object API health boundary, then the result
and detail for download, manifest/signature/hash verification, backup, atomic
install, the remote stado resolver --help probe, and final recovery. A
conditional rollback row shows whether a failed probe
restored the previous binary or removed an invalid first install. The command's
verbatim JSON remains beside those rows.
This is a surface for the one procedure, not another recovery path. Use the
Operations recovery procedure
for when to invoke it, its rollback guarantees, and the difference from routine
host release; there is no manual SSH follow-up.
What the app never does
- It never invents state: no snapshot of its own, no optimistic labels, no fallback list of enrollment methods when the control plane has not answered one.
- It never edits the registry document directly.
GET /api/registry.jsondeliberately returns three whitelisted policy fields per target and does not expose routing or credentials. The Hosts screen obtains route names, SSH destinations, and probe results separately through the operator API'sstado host link <target> --json; route changes use the same API forstado registry host path set|remove ... --json. Neither payload contains the channel's private key. - It never assembles shell strings. Mutations use typed product APIs or an argv array passed to the authenticated operator bridge, never a local CLI.
- It never transmits a private key: the channel key's private half stays in the operator's credential store, and only the public line reaches the machine.
- It never registers a duplicate: enrollment refuses a name the registry or the capacity store already knows.
Source: this website