Workstation

The core resource: a single managed machine (local VM, container, or cloud VM).

A Workstation is the central resource: one managed machine, kept in line with its declared spec. The provider can be a local VM (qemu on Linux, lima on macOS) or a remote cloud VM. You declare intent in spec; the control plane and in-VM agent write status.

apiVersion: workstations.ringleader.dev/v1
kind: Workstation

A workstation is namespaced. Its effective configuration is the merged configuration of every WorkstationConfig that attaches to it (by selector or explicit reference) plus the workstation’s own inline spec. That merge is computed on demand rather than stored in status — read it with rl workstation get-resolved-configuration.

Minimal example

Everything is implicit: a provider is chosen by automatic placement, and configs attach by selector.

apiVersion: workstations.ringleader.dev/v1
kind: Workstation
metadata:
  name: hello-ws
  namespace: dev
  labels:
    tier: dev
spec: {}

Spec fields

You author these. Many are config-layer fields: they can also be set on a WorkstationConfig and merged in.

Placement & provider

FieldTypeDescription
requirements[]stringMachine requirements in precedence order (e.g. ["provider:gcp"]); matched against discovered provider capabilities.
providerstringExplicit provider pin, shorthand for requirements: [provider:X].
providerConfigobjectPer-provider opaque config (e.g. memory/cpus for local VMs, or {gcp: {project, zone, machineType}}). Not merged from WorkstationConfig config layers — but the matching CloudIdentity’s defaultProviderConfig and overrideProviderConfig are folded around it.
machineTypestringProvider machine size class (e.g. e2-micro for GCP).
securityProfilestringNamed security profile (e.g. baseline, privileged).

Lifecycle

FieldTypeDescription
stoppedbooltrue ⇒ desired state Stopped; the machine is halted and rests in the Stopped phase.
restartNoncestringChange this on a running workstation to trigger a Stop→Start restart.
ttlstringA lifetime after which the workstation expires, measured from its creation time — e.g. 90m, 2h, 7d. Omit it for a workstation that never expires.
ttlActionstringWhat expiry does: delete (the default) or stop. Only consulted when ttl is set.

Expiry (ttl)

spec.ttl gives a workstation a deadline the control plane enforces on its own, with no client involved. It exists for the case where nothing is left to clean up after you: a CI job killed outright, or a machine you meant to keep for an afternoon.

apiVersion: workstations.ringleader.dev/v1
kind: Workstation
metadata:
  name: ci-run-4821
  namespace: dev
spec:
  ttl: 2h            # gone two hours after it was created
  ttlAction: delete  # the default
  • The duration is a plain unit string — 45s, 90m, 2h, 720h — plus a whole number of days, 7d. It must be positive; a value that cannot be read, or a zero or negative one, is rejected when you apply it rather than being silently ignored.
  • The clock starts at creation and nothing moves it — not a stop, not a start, not a restart. A stopped cloud VM still bills for its disks, so a deadline that reset on a power cycle would be the wrong promise. Editing spec.ttl is the one thing that moves the deadline, and it takes effect immediately.
  • ttlAction: delete (the default) removes the workstation and destroys the machine and its disk, exactly as rl workstation delete would. ttlAction: stop powers it down and leaves it there, so its disk and state survive and you can start it again.
  • Enforcement is periodic, so a workstation can sit briefly past its deadline before the action lands. Treat the deadline as “at or shortly after this”, not as a precise instant.
  • The deadline is published as status.expiresAt, and rl workstation get grows an EXPIRES column — counting down, or expired — as soon as any listed workstation declares one.

`ttl` is per workstation, never per config

ttl and ttlAction are Workstation fields only. They are rejected on a WorkstationConfig: a config attaches by selector, so a lifetime declared there would expire workstations its author does not own. Set it on each workstation that should expire.

Configuration attachment

FieldTypeDescription
configs[]objectExplicit WorkstationConfig references: {name, priority}.

Machine contents (config-layer fields)

FieldTypeDescription
imagestring/objectOS image: a shorthand string, or {os, distribution, version}.
packages[]Packages to install: each a bare name or {name, version, updatePolicy}.
devtools[]objectCurated install recipes: {name, version} (e.g. nodejs, claude-code, vscode-web).
toolconfigs[]objectPer-tool config channel: {id, name, config} (opaque config) for already-installed tools.
packageRepositories[]objectThird-party repos: {name, type, urls, key, …} (apt/ppa).
aptSources[]Legacy apt repos + keys (normalized to packageRepositories).
scripts[]objectProvisioning scripts: {name, phase, content, …}.
sources[]objectCode/repos synced into the workstation: {name, git|local, path, updatePolicy, …}.
files[]objectDeclarative files materialized on the machine.
services[]objectUser-declared managed services (systemd units / command-mode).
ports[]intDeclared listening ports.

System settings (config-layer fields)

FieldTypeDescription
identityobjectThe login user: {user, uid, gid, group, groups, shell, sudo}. The only place the login user is declared; the agent owns user creation. Default shell /bin/bash; the user gets passwordless sudo by default (sudo: false to opt out).
environmentmapSystem-wide env vars → /etc/environment + profile.d.
dotfilesmapShell-dotfile fragments: path → content (managed blocks).
sysctlsmapKernel sysctls → /etc/sysctl.d/.
limits[]pam_limits entries → /etc/security/limits.d/.
hostnamestringMachine hostname.
timezonestringIANA timezone (e.g. America/New_York).
localestringLocale (e.g. en_US.UTF-8).
securityUpdatesboolEnable unattended security upgrades (Debian: unattended-upgrades).
diagnosticsobjectLedger config: {ledger: {enabled, retain, maxOutputBytes}}.
defaultLocalBindingobjectDevice-local forwarding template; the daemon seeds a LocalBinding from it once when the workstation first reaches Running (honor-delete).

Status fields

The system writes these. They are read-only.

FieldTypeDescription
phasestringPending, Provisioning, PostInstall, Running, Reconfiguring, Starting, Stopping, Stopped, Failed, or Terminating.
messagestringThe one human headline: what is going on with this workstation, in a sentence.
providerstringSelected provider (e.g. gcp, qemu, lima).
providerIDstringProvider-specific machine ID (e.g. the GCE instance name).
osstringThe observed operating system.
imagestringThe base image the machine was actually created from.
executorstringHow workloads run on the machine.
addressstringSSH address reachable by the daemon (absent for a NAT’d VM the daemon can’t dial).
userstringResolved SSH login user.
observedGenerationintThe spec generation the controller last observed.
expiresAtstringWhen a declared spec.ttl falls due, as a timestamp. Absent on a workstation with no ttl.
configobjectConfiguration state: {effectiveHash, appliedHash, appliedAt}. effectiveHash is the configuration the control plane wants; appliedHash is what the machine last reported applying.
conditions[]objectConditions, each with a reason and a message — see Conditions and their reasons.
configStateobjectWhat the in-VM agent reports about its own configuration pass: a single {state} field. It is present only when the agent reports the pass has settled into failure (state: failed) — as opposed to never having reported at all; a working or fully configured workstation carries no configState key. The same fact reads off the Configured condition as the ConfigurationFailed reason, which is what rl describe prints.
diagnosticsobjectLedger rollup (path + last-run counts).
sshHostKeys[]stringThe workstation’s SSH host public keys, for pinning into known_hosts.
deliveryobjectAn internal transport record the daemon reads. Not a user surface — describe does not print it, and its shape is not part of the documented contract.

Status does not carry the merged configuration

There is no status.resolved. Status carries a fingerprint of the effective configuration (status.config.effectiveHash), not the configuration itself — the merge is derived data, re-computed on demand. To read what a workstation actually resolves to, use rl workstation get-resolved-configuration, which prints the whole merged spec including the environment, files, scripts, sources, devtools, toolconfigs, and dotfiles.

Lifecycle phases

Pending → Provisioning → PostInstall → Running
                          (spec change) → Reconfiguring → Running
                          (stopped: true) → Stopping → Stopped
                                            (start) → Starting → Running

Stopping and Starting are real phases, not instant transitions: a power operation is issued, and it rests in them until the machine is confirmed down (or back up). Starting means an existing machine is powering on, as opposed to Provisioning, which means one is being created.

A workstation is fully configured when status.config.appliedHash equals status.config.effectiveHash and its Configured condition is True.

A configuration verdict never outlives the boot that produced it: stopping a workstation clears appliedHash and sets Configured to False, so a stopped workstation never claims a configuration it applied on a previous boot.

Conditions and their reasons

Four conditions are defined — three always published, plus ToolsReady once a tool has failed. Each carries a reason — a short machine token — and a human message. Available is the one to gate on: it is the aggregate that answers “is this workstation usable yet?”, and the CLI, the TUI and the desktop apps all fold it into the single status they show.

ConditionMeaning
AvailableThe aggregate readiness signal: True only when the workstation is Running and fully configured.
ReadyThe machine itself is up and reachable. Says nothing about configuration.
ConfiguredThe in-VM agent has applied the configuration the control plane wants.
ToolsReadyDevtool installation, and usually absent: it is published only once a tool has failed to install. A workstation whose tools all installed cleanly carries no ToolsReady condition at all.

Available reasons

ReasonMeaning
ReadyRunning and fully configured — the workstation is usable.
ConfiguringRunning, but the configuration is still being applied.
ToolsInstallingRunning and configured, but declared tools are still installing.
(a phase name)Not running — the reason is the phase itself (Provisioning, Stopped, Failed, …).

Configured reasons

ReasonMeaning
AppliedThe in-VM agent reported an applied configuration matching the one requested.
ConfiguringWaiting for the in-VM agent to apply the current configuration.
ConfigurationStalledThe current configuration has been waiting past the grace period and the agent has reported nothing. This is absence, so it stays a diagnosis rather than a verdict — the most common cause is a failed boot-time agent bootstrap, but a slow install looks the same from outside, and the workstation is neither failed nor destroyed.
ConfigurationFailedThe agent reported that its own configuration pass keeps failing — a configuration step on the workstation is failing repeatedly. Unlike the stall above this is a report rather than an inference from silence, so it names a real problem: read the agent log and the diagnostic ledger (status.diagnostics), or run rl troubleshoot.
StoppedThe workstation is powered down, so nothing is being applied.
UserConfigFailedThe login user, groups or sudo could not be configured. Unlike the reasons above this is about the login user rather than the applied configuration, so a workstation can carry it while its configuration hashes match exactly.

While ToolsReady is present its reason names the step that failed; it flips to Installed once a workstation that previously failed has every declared tool present.

See also