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_keys line 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.json deliberately 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's stado host link <target> --json; route changes use the same API for stado 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