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.

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 http://127.0.0.1:8765 as the fallback. 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 host vault-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 no larger than 2 MiB. 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.

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

The Hosts, Services, and Releases screens show the product's own reports. Their commands and refusal handling are described below and in the CLI reference.

  • Hosts keeps two paths separate. Host-control routes reads stado host link <target> --json, lists the preferred SSH destination and every ordered fallback with its live probe answer, and marks the route Stado selected for a real operation. Beacon network path is the direct/relay path the host reported when it published its beacon; it does not claim to be a host-control route. Manage host-control routes reviews the exact stado registry host path set|remove ... --json invocation before changing the registry, reprobes every route afterwards, and never offers to remove the preferred primary route. When that same command proves a reachable host's beacon publisher is being refused because the dashboard verifier is unavailable, Hosts shows Repair beacon publication. It runs stado host repair-link <target> --json, keeps the command's receipt on screen, and refreshes only that host after the CLI has observed a newer beacon and closed the silence. Other publisher failures have no repair button; the app does not guess.
  • Services separates what is declared from what is running: declared units, the fleet as the health beacons report it, and processes no unit owns. running_binary is a column rather than a detail, and orphan processes are a list of their own — both facts were learned the expensive way. Beacon-reported states are printed with the beacon's own timestamp, so an active from a five-day-old beacon reads as five days old; the console never re-asks a host to fill the gap.
  • Services → Web hosting reads stado web status [product] --json through the selected Stado API. It shows the selected edge, the product's verdict, DNS and port observations, and the complete edge_error. The sheet retains the command, source endpoint, exit code and complete returned output even when the command fails; Desktop does not calculate a second verdict.
  • Releases shows one row per product target with the verdict stado release doctor reached, desired and observed versions side by side, and blockers in the CLI's own words — it reads verdict, failed, and findings out of status --json and re-derives none of them. A host's quarantined digests are listed with the one the registry desires first, then newest first, so the digest actually blocking a rollout is not buried under refusals that are already history.
  • Inference shows which model each router alias reaches and the declared deployments behind it. It reads stado inference list --json and, per deployment, stado inference status <name> --json: an alias that lands on a declared deployment is shown with that deployment's exact repository@revision, an alias that leaves the fleet with the remote model name the router uses, and each deployment with its host, engine, GPU mode, context length, endpoint and the beacon's last state and GPU memory figure. A host whose beacon could not be read says so on its row in the command's own words; the screen never shows a state it did not read. It changes nothing: replanning a deployment stays with stado inference plan and apply.
  • Cloudflare routes runs the same stado cloudflare route list|status|route|remove commands the Cloudflare reference describes, shows the exact argv before a change runs, and keeps each receipt on screen.
  • Dashboard state decoding presents the published backend snapshot — each worker shows its admission decision, running jobs, live CPU and RAM, per-device accelerator availability, and VRAM. Job, failure, and onboarding labels reflect what the backend recorded, not optimistic placeholders.

The console reads a product report before judging its result code. host gates, service converge, web status, and release status can report failure together with a complete JSON result. Desktop keeps that result on its screen rather than replacing it with a generic command error.

Services report mode calls GET /api/service/converge?target=<host>. The apply sheet selects the host and all declared binaries or one binary, then sends POST /api/service/converge?target=<host>[&binary=<name>] with no body. The sheet also shows the equivalent stado service converge <host> [binary] --apply --json command. Both interfaces call the same product implementation; Desktop does not launch the CLI for convergence or calculate its verdict.

A completed request returns HTTP 200 with {"exit_code": <code>, "report": {...}}, including when delivery failed. Desktop retains the complete JSON document and the product's exit code through the following refresh. It shows every release, stado-readers.detail, refusal, undeliverable item and final binary row without discarding fields it does not recognize. Missing or unauthorized client actions return 401; unavailable verifier state returns 503 with its diagnostic. Malformed query parameters or a request body return 400. A failure before a report exists returns 503 with SERVICE_CONVERGE_FAILED and its cause.

Apply has no shorter Desktop watchdog than the product's bounded host stages. Changing the selected source does not cancel delivery already accepted by the server, but a response from the old source is not shown as a result for the new one. Read-only requests keep their ordinary 120-second deadline.

Desktop never infers reader-resume support from a numeric version banner. Partial-state reader resume requires a target receiver built from a release containing the retained-archive command arguments. A receiver built without that contract rejects the apply; Desktop keeps the product's failed receipt rather than converting the older source into success.

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.

For these two reads the native API retains up to 16 MiB of command stdout, so an ordinary multi-megabyte log still reaches Desktop as a complete JSON receipt. Stderr retains its 1 MiB limit; other commands retain the existing 1 MiB limit for each stream. A larger response is still marked truncated and is not presented as a complete read.

These two exact reads do not require RUN_MUTATION. Other host-exec commands keep their existing confirmation requirement, including provider sign-in. 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

In Hosts → Reconcile storage roots…, choose Run, Resume, Status, Rollback, or Finalize for one transaction ID. The sheet initially selects Status; opening it or editing a field sends no request. The app retains the ID for the host, shows the equivalent CLI command, and requires a review of every mutating phase.

The sheet captures the selected dashboard address and host when it opens. Its confirmation and every retained result show that address. Changing the source elsewhere neither redirects this sheet nor cancels a request already sent; a result from another address is not shown as this source's receipt.

Status calls GET /api/host/storage-root-reconcile?target=<host>&transaction=<id>&phase=status. The other four phases use POST on the same path with their selected phase. Neither method accepts a request body. Desktop calls the configured API, not a local CLI subprocess or a second reconciliation implementation.

The server requires storage-reconcile-read for GET and storage-reconcile-apply for POST in the client's registry_api.clients action list. Policy and convergence permissions do not grant these actions. The endpoint-bound credential comes from Settings → Registry API access.

A product result returns HTTP 200 with exit_code, refusal, and the complete report, including when that report represents a refused operation. Unauthorized clients receive 401; unavailable authorization or an operation that failed before producing a report receives 503. The latter names STORAGE_RECONCILIATION_FAILED and its cause. A malformed, duplicate, empty or unknown query field, an invalid method/phase combination, or a body receives 400.

The sheet retains the actual HTTP status, product exit code, complete raw API response and every decoded report field. A decoding error or later refusal does not discard those bytes or an earlier result. Status reads have a 120-second deadline; mutations use the product's own bounded stages.

An accepted Run, Resume, Rollback, or Finalize response only confirms that the resident operation accepted the request. Select Status explicitly to read its durable result; the app never advances a transaction automatically. Resume reuses the same ID. Rollback is limited to the pre-data-commit path; after that boundary the recorded activation must be completed instead. After activation, ordinary lifecycle cleanup must finish before explicitly selecting Finalize, and Finalize's accepted response still needs a later Status receipt to prove completion. The checkpoint's actual serving root decides which conflicting bytes remain authoritative, as described in Operations.

Prepare Apple code capture

The Apple-only workflow below requires Stado 0.16.38 or later.

In Hosts > Apple code capture, Read Apple readiness sends the read-only command below through the configured native API and displays its fresh receipt. It needs no mutation confirmation and changes no host settings:

stado workload status gui-automation --target <target> --json

Its JSON receipt contains target, ssh_target, an ordered items array of [name, value] pairs, and error. apple-challenge-ready: yes is observed readiness: the signed version 2 helper passed its prompt-free --preflight in the registry-bound user's exact Aqua session. A TCC row by itself is not ready.

Apple readiness is independent of gui-ready. The Apple helper can be ready while CuaDriver reports accessibility: denied and gui-ready: no. accessibility-record: granted describes the stored CuaDriver entry, whereas accessibility reports the serving driver's prompt-free permission response. Apple-only preparation does not change either CuaDriver state.

Prepare Apple code capture shows and submits exactly:

stado workload run gui-automation --target <target> --plan apple-only.json --json

The plan it submits is exactly:

{"schema":"wisent.gui-automation-plan.v1","operation":"grant-accessibility","apple_only":true}

apple_only installs or reuses only the signed Apple challenge helper and reconciles only that helper's Accessibility grant. It leaves CuaDriver, its Accessibility grants and runtime, autologin, and remote-management settings unchanged. The command does not open a browser, request Apple authorization, or sign in to Apple. A plan without apple_only retains the separate full GUI-automation behavior.

The host it prepares carries no signing certificate of its own. Stado installs the pinned shared signer from the fleet's object namespace, verifies its digest, and signs the compiled helper with the Apple certificate, key and issuer it reads for that command only; see Native macOS code signatures.

Privileged preparation is noninteractive. Stado reads the host account's registered password and supplies it to remote sudo -S over stdin; if no password is available it uses noninteractive sudo -n and reports refusal instead of displaying a password or consent prompt. After writing the grant, Stado invokes the helper's prompt-free --preflight in the declared Aqua session. Only that successful observation adds apple-challenge-ready: yes.

Credential reads are bounded independently of host execution. Stado allows 15 seconds to connect to Skarbiec and 120 seconds for the complete HTTP request. The existing owner-vault password fallback has a 90-second deadline and its child is terminated if that wait is cancelled. A failure before host execution names the account field, the broker error, and the owner-vault error; it is not an Accessibility refusal and does not prove that preparation reached the host.

Each readiness or preparation command acquires the registered host key and chooses a declared connection once, before its host operations begin. It keeps that key and connection only for that command, so every individual read does not repeat credential acquisition or probe an unavailable preferred path. The command releases its key when it finishes or is cancelled. A failed host operation is reported, not repeated on another connection.

After an interrupted preparation, the Probierz journey apple-challenge-readiness reads the existing state through both the CLI and native API without repeating preparation. It also checks the API's refusal of an unconfirmed preparation request; it never submits a confirmed preparation. Select it with PROBIERZ_JOURNEY=apple-challenge-readiness and name the host with STADO_APPLE_PREPARATION_HOST. The separate apple-challenge-preparation journey performs the CLI and native API changes. Both journeys retain the source revision, compiled executables, compilation result, and per-command JSON receipts with stdout and stderr files. A missing final command result or an incomplete run is not evidence that preparation succeeded or that it is safe to repeat it.

Desktop's configured FleetControlStore sends the displayed argv array and mutation confirmation to the native POST /api/operator/run API at the configured Stado endpoint. Desktop neither assembles a shell command nor spawns the CLI. The loopback endpoint retains local trust; a remote endpoint requires operator authorization. The server executes the allowlisted argv and returns stdout, stderr, and exit status.

Desktop decodes the command's JSON stdout before judging that status, so the receipt keeps target, ssh_target, every completed items entry, and error even after a later step fails. The failure receipt and refusal remain visible; a refused database write or failed helper preflight is never presented as ready.

Preparation does not capture a code or issue a certificate. Certificate issuance is a separate, explicitly authorized trajectory that may open Apple sign-in or system-consent UI and require 2FA; preparation neither starts nor proves that trajectory. See the Weles Apple account workflow.

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 allowlisted fleet or host command through the dashboard's POST /api/operator/run argv bridge — an argv array checked against a closed family allowlist, 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.

Versioned recovery in the enrollment proof

The final agent proof keeps its existing stado host recover <target> behavior when the exact-version field is empty. When an operator chooses a version, the same action runs exactly:

stado host recover <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 from the locally executed stado host link <target> --json; route changes run 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 CLI or authenticated operator bridge.
  • 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