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: WorkstationA 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
| Field | Type | Description |
|---|---|---|
requirements | []string | Machine requirements in precedence order (e.g. ["provider:gcp"]); matched against discovered provider capabilities. |
provider | string | Explicit provider pin, shorthand for requirements: [provider:X]. |
providerConfig | object | Per-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. |
machineType | string | Provider machine size class (e.g. e2-micro for GCP). |
securityProfile | string | Named security profile (e.g. baseline, privileged). |
Lifecycle
| Field | Type | Description |
|---|---|---|
stopped | bool | true ⇒ desired state Stopped; the machine is halted and rests in the Stopped phase. |
restartNonce | string | Change this on a running workstation to trigger a Stop→Start restart. |
ttl | string | A lifetime after which the workstation expires, measured from its creation time — e.g. 90m, 2h, 7d. Omit it for a workstation that never expires. |
ttlAction | string | What 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.ttlis 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 asrl workstation deletewould.ttlAction: stoppowers 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, andrl workstation getgrows anEXPIREScolumn — counting down, orexpired— 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
| Field | Type | Description |
|---|---|---|
configs | []object | Explicit WorkstationConfig references: {name, priority}. |
Machine contents (config-layer fields)
| Field | Type | Description |
|---|---|---|
image | string/object | OS image: a shorthand string, or {os, distribution, version}. |
packages | [] | Packages to install: each a bare name or {name, version, updatePolicy}. |
devtools | []object | Curated install recipes: {name, version} (e.g. nodejs, claude-code, vscode-web). |
toolconfigs | []object | Per-tool config channel: {id, name, config} (opaque config) for already-installed tools. |
packageRepositories | []object | Third-party repos: {name, type, urls, key, …} (apt/ppa). |
aptSources | [] | Legacy apt repos + keys (normalized to packageRepositories). |
scripts | []object | Provisioning scripts: {name, phase, content, …}. |
sources | []object | Code/repos synced into the workstation: {name, git|local, path, updatePolicy, …}. |
files | []object | Declarative files materialized on the machine. |
services | []object | User-declared managed services (systemd units / command-mode). |
ports | []int | Declared listening ports. |
System settings (config-layer fields)
| Field | Type | Description |
|---|---|---|
identity | object | The 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). |
environment | map | System-wide env vars → /etc/environment + profile.d. |
dotfiles | map | Shell-dotfile fragments: path → content (managed blocks). |
sysctls | map | Kernel sysctls → /etc/sysctl.d/. |
limits | [] | pam_limits entries → /etc/security/limits.d/. |
hostname | string | Machine hostname. |
timezone | string | IANA timezone (e.g. America/New_York). |
locale | string | Locale (e.g. en_US.UTF-8). |
securityUpdates | bool | Enable unattended security upgrades (Debian: unattended-upgrades). |
diagnostics | object | Ledger config: {ledger: {enabled, retain, maxOutputBytes}}. |
defaultLocalBinding | object | Device-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.
| Field | Type | Description |
|---|---|---|
phase | string | Pending, Provisioning, PostInstall, Running, Reconfiguring, Starting, Stopping, Stopped, Failed, or Terminating. |
message | string | The one human headline: what is going on with this workstation, in a sentence. |
provider | string | Selected provider (e.g. gcp, qemu, lima). |
providerID | string | Provider-specific machine ID (e.g. the GCE instance name). |
os | string | The observed operating system. |
image | string | The base image the machine was actually created from. |
executor | string | How workloads run on the machine. |
address | string | SSH address reachable by the daemon (absent for a NAT’d VM the daemon can’t dial). |
user | string | Resolved SSH login user. |
observedGeneration | int | The spec generation the controller last observed. |
expiresAt | string | When a declared spec.ttl falls due, as a timestamp. Absent on a workstation with no ttl. |
config | object | Configuration state: {effectiveHash, appliedHash, appliedAt}. effectiveHash is the configuration the control plane wants; appliedHash is what the machine last reported applying. |
conditions | []object | Conditions, each with a reason and a message — see Conditions and their reasons. |
configState | object | What 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. |
diagnostics | object | Ledger rollup (path + last-run counts). |
sshHostKeys | []string | The workstation’s SSH host public keys, for pinning into known_hosts. |
delivery | object | An 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
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 → RunningStopping 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.
| Condition | Meaning |
|---|---|
Available | The aggregate readiness signal: True only when the workstation is Running and fully configured. |
Ready | The machine itself is up and reachable. Says nothing about configuration. |
Configured | The in-VM agent has applied the configuration the control plane wants. |
ToolsReady | Devtool 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
| Reason | Meaning |
|---|---|
Ready | Running and fully configured — the workstation is usable. |
Configuring | Running, but the configuration is still being applied. |
ToolsInstalling | Running and configured, but declared tools are still installing. |
| (a phase name) | Not running — the reason is the phase itself (Provisioning, Stopped, Failed, …). |
Configured reasons
| Reason | Meaning |
|---|---|
Applied | The in-VM agent reported an applied configuration matching the one requested. |
Configuring | Waiting for the in-VM agent to apply the current configuration. |
ConfigurationStalled | The 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. |
ConfigurationFailed | The 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. |
Stopped | The workstation is powered down, so nothing is being applied. |
UserConfigFailed | The 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
- WorkstationConfig: the reusable layers a workstation merges.
- Configuration tutorial: a worked browser-IDE example.