On this page

Runner as a fleet capability

stado runner installs, reads, restarts, removes, and reports GitHub Actions runners by declared profile. The command vocabulary is independent of runner products: a third runner is another row in stado-rs/data/work/runner-profiles.json, not another command.

Invocations

  • stado runner list [--json]
  • stado runner install <TARGET> --profile <PROFILE> [--repository <REPO>] [--json]
  • stado runner status <TARGET> [--profile <PROFILE>] [--json]
  • stado runner restart <TARGET> --profile <PROFILE> [--repository <REPO>]
  • stado runner remove <TARGET> --profile <PROFILE> [--repository <REPO>]
  • stado runner report [--json]
  • stado runner credential [--json]
  • stado runner model-review <TARGET> --repository <REPO> [--json]
  • stado runner diagnostics <TARGET> --profile <PROFILE> [--json]

The listener is a role of the host's Stado unit

A host runs one Stado process, com.wisent.stado, and a runner is not a unit of its own. The installer writes the runner's launcher, <ROOT>/start-runner.sh, and install ends by asserting the host's Stado unit with the option --precheck-runner <ROOT> added to its stado serve line, through the same service ensure pass an operator runs by hand; that pass restarts the unit, and stado serve runs the launcher as its precheck-runner role, under passwordless sudo, so the launcher applies the runner's egress rules and drops to the runner's own account before GitHub's listener starts. The JSON report carries role.option, role.unit, role.taken_off and role.ensure. A host that carries several runners carries one such option per runner root.

restart takes the role off the Stado unit and switches it back on, which restarts that unit twice and the listener with it, then reads GitHub's view of the runner. remove takes the role off before the host forgets the registration. A re-registration or a restart install asks for takes the role off before the installer runs, so no listener holds the old registration while config.sh replaces it; an installer that still finds a listener refuses with runner listener is still running; its role was not taken off the Stado unit.

The listener's exit is the role's failure: the supervisor ends the whole Stado process, and the init system restarts com.wisent.stado and the listener with it. The daemon com.wisent.stado-precheck-runner and the service wisent-stado-precheck-runner.service (and each other profile's unit under its unit_label) are the role's predecessors in the service catalog: install retires them on the host, status refuses with launchd daemon <LABEL> still runs beside the Stado unit; stado runner install retires it while one is loaded, and the autonomy reconciler retires one it finds on a host whose Stado unit runs the role. The host memory policies that restarted the runner's own unit restart com.wisent.stado instead.

On Linux the runner's confinement is its own account and the nftables table the installer writes. The launcher clears the previous job's work tree and runs GitHub's runsvc.sh as that account with HOME, TMPDIR and the .NET extract directory inside the runner root.

diagnostics reads the Stado unit's state as the init system reports it and the last 120 lines of the newest runner log without restarting anything. JSON keeps those lines verbatim in tail, alongside the selected log path, read completeness, and host-read stderr. The human output prints the same log content. A running Stado unit does not establish that the runner listener can start or accept jobs.

On macOS, runtime.signature includes the listener's actual signing identity and entitlements. runtime.codesign reports signature verification separately: a valid signature alone does not prove that CoreCLR can start.

runtime.platform_probes records the CoreCLR library's architecture, signing metadata and signature verification, the runner's .NET runtime configuration, and System Integrity Protection status on macOS. Each observation names the actual command, its exit_status, and its combined output; line breaks in this metadata output are folded to spaces. A failed metadata command remains a failed observation even when the runner log is readable. The field is null on other platforms.

The human command prints both the runtime and memory observations before the log. Stado Desktop keeps the same data in GitHub runners → Complete runner receipt. These reads do not change signatures, security settings or service state, and they do not execute a second listener outside the Stado unit. A valid library signature or a particular SIP setting is an observation, not proof of the startup failure's cause or of a repaired runner.

runtime.last_coreclr_start_failure retains the last CoreCLR startup failure in that log, or null when none was read. Memory readings describe the host at the diagnostic read, not at the logged failure. 0x8007000C is not the E_OUTOFMEMORY value 0x8007000E, so Stado does not label it a proven memory shortage, and it infers neither a runtime memory refusal nor a kill by signal. The observed unit exit status remains in exec_main_status.

--repository is the GitHub repository name inside wisent-ai. It selects repository registration/removal, which uses the repository administrator permission rather than organization runner administration. Without it, Stado explicitly requests organization registration. An organization refusal is not a verdict about repository registration.

Runner installation does not configure Brama model review. A repository whose CI needs BRAMA_MODEL_ROUTER_TOKEN uses runner model-review separately; its Brama refusal does not prevent an unrelated documentation or build job from obtaining a runner. Publisher installation still performs its declared App Store Connect, Sparkle and Developer ID setup.

status without --profile reads every declared profile on one target. report performs the same read for every local registry host. JSON status rows carry profile, installed, runner_scope, a typed listener object (connected plus state), and host_job_slot. The registration scope comes from the host's .stado/registered-runner record; it is unrecorded, never an invented organization default, when that record has no scope.

Installation compares the requested scope, group and labels with the host registration record. A changed registration is applied even when .runner already exists. An executing runner job prevents reconfiguration with runner is executing a job; registration was not changed. The previous local registration is retained during reconfiguration and restored if configuration fails. Changing scope does not claim that an old GitHub registration was deleted.

After installation, Stado reads the GitHub runner list at the requested scope, including subsequent pages. The command succeeds only after GitHub reports the named runner online. JSON adds registration.scope, runner, present, status and reconfigured; listener.connected reflects the successful GitHub observation. Absence, an unreadable list or a runner that remains offline is a failure, not a successful install. An unchanged repeat does not re-register the runner.

GitHub credential

stado-rs/data/fleet/github-identity.json names the Skarbiec route, the required permission, and the exact diagnostic endpoint. Both runner lifecycle operations and runner credential resolve that route through Skarbiec's POST /v1/operator/route/resolve, then read the returned item and field. The installation code does not carry a separate hardcoded credential item.

Resolution requests only the declared resource. Stado asks the Skarbiec at secrets.skarbiec.url (environment override WC_SKARBIEC_URL) as its own consumer stado, with the grant in secrets.skarbiec.token_file; a refusal names the endpoint that answered. The default local skarbiec route resolve command can read another vault and route table, so a local route is not proof that the selected broker declares it. Stado does not change that selection or copy a route to another vault after a refusal.

The shipped diagnostic reads the stado repository's runner list and requires repo. Its JSON names the resolved route, item, field, endpoint, granted permissions and HTTP status without printing the token. The route's existing name github:org-runner-admin is an identifier; it does not grant organization privileges. A successful diagnostic applies to the named endpoint, not to every repository or to organization-wide registration.

stado runner credential --json
stado runner install <TARGET> --profile precheck --repository <REPO> --json
stado runner status <TARGET> --profile precheck --json

Skarbiec's route declaration command is skarbiec route declare --resource <RESOURCE> --item <ITEM> --field <FIELD> --reason <TEXT>. A missing route is reported with its declaration source and this command; Stado does not search other credentials or silently substitute an item.

Declaration

The declaration is stado-rs/data/work/runner-profiles.json, compiled into the binary with include_str!. Each profile declares:

  • name and host identity slug
  • every GitHub Actions label it registers
  • github_runner_group
  • unit_label, the name of the unit earlier releases ran this runner under, which the installer retires
  • an installer kind for darwin-arm64 and linux-amd64
  • the secret names its setup consumes
  • accepts_repository_scope
  • for a publisher that issues Developer ID, the declared developer_id_account_item

The profiles are precheck (stado-precheck), repository-precheck (stado-repo-precheck), product-precheck (stado-product-precheck) and publisher (stado-publisher). Each profile has one registration per host, not one per repository. Selecting another repository for an occupied profile changes its scope; it does not add a parallel registration. The installers retain the host-wide job gate.

That is why there is more than one precheck profile. One Linux host answers for every repository whose gates ask for the stado-precheck label, and each repository that needs its own runner at the same time needs its own profile: precheck and repository-precheck and product-precheck carry that label and differ only in slug and runner root, so three repositories can hold a runner on one host at once, each as its own --precheck-runner option of that host's Stado unit. A fleet-wide runner would be one organization registration instead, which GitHub refuses for the identity Stado resolves through github:org-runner-admin: that token carries repo and read:org and no runner administration, so runner install without --repository answers with GitHub's own refusal rather than registering anything.

Refusals

The capability's declaration and registry refusals are complete sentences:

  • runner profile '<PROFILE>' is not declared; add it to stado-rs/data/work/runner-profiles.json
  • runner profile '<PROFILE>' declares no repository scope; enable accepts_repository_scope in stado-rs/data/work/runner-profiles.json
  • runner profile '<PROFILE>' declares no installer for '<PLATFORM>'; add it to stado-rs/data/work/runner-profiles.json
  • <TARGET> declares no host target; add it to the canonical fleet registry
  • <TARGET> declares no local host provider; set its kind to a local host capability in the canonical fleet registry or select a local host target
  • <TARGET> declares no reachable host destination; add a registry-managed ssh destination to the canonical fleet registry or run the command on that host
  • <TARGET> declares no supported runner platform for "<PLATFORM>"; set release_platform in the canonical fleet registry to "darwin-arm64" or "linux-amd64"
  • <HOST>: no declaration of com.wisent.stado on this host and <HOST>: com.wisent.stado is declared without its program, so no role can be added to it; declare it with stado service ensure stado --host <HOST> first — the runner role needs the host's Stado unit to be declared
  • <HOST>: <PROFILE> runner is not a role of this host's Stado unit; stado runner install declares it — restart of a runner the unit does not run
  • no address for service "brama" on this machine: run \stado service directory publish` so the directory writes ~/.stado/forwards/brama.local` — the model-review route and bearer check dial Brama at the address the service directory published for this machine; no Brama hostname is built in

GitHub authentication failures retain the provider's HTTP response and name the actual Skarbiec route and resolved item/field. Organization failures explain that --repository <NAME> selects a different operation; they do not claim the credential has been tested on that repository.

If registration finishes but cannot be verified, the failure identifies the requested scope and runner name: <TARGET>: <PROFILE> installation exited successfully, and GitHub lists <RUNNER> under <SCOPE> as <STATUS>, not online, or the same sentence for a runner GitHub does not list at all. Use runner diagnostics to read the unit verdict and runner log, not another unverified restart.

Stado Desktop

The host inspector's GitHub runners section reads its profile choices from runner list --json on the selected Stado endpoint, including product-precheck; it does not keep a separate list of profile names. Profile operations remain unavailable until that declaration has been read.

Read profiles repeats that read explicitly after a failed or outdated catalogue response.

The section offers read/install/restart/remove operations, Read diagnostics, Check GitHub credential, and Configure model review for the entered repository. Read diagnostics displays the selected host and profile's log path, complete returned log content, read completeness, and any host-read errors. Complete runner receipt retains the API response, including memory and runtime observations. Switching the selected host or endpoint discards the previous read.

Installation displays the actual GitHub runner, scope, online status and whether registration changed, separately from labels and the host job slot, and the role the host's Stado unit was asserted with. Account operations display the product's response or refusal. The native client sends these requests through Stado's operator API.

Source: this website