Workstation access
Start, stop, connect to, and diagnose a workstation: start/stop, shell, tmux, logs, and troubleshoot.
These commands manage a workstation’s lifecycle and connect to a running one. The
access commands (shell, tmux, logs, troubleshoot) resolve the target’s
address and login user from its status, and select a default workstation when you
omit the name (the only, or alphabetically first, Running workstation).
rl workstation start / stop / restart
Power one or more workstations imperatively: the command equivalent of setting
spec.stopped (and, for restart, spec.restartNonce) in a manifest. Target them
by name, several names, or a label selector with -l. Alias: ws.
rl workstation start my-box
rl workstation stop my-box other-box
rl ws stop -l tier=dev # every matching workstation
rl workstation restart my-box
rl ws restart -l tier=dev # one logical restart across the whole setA stopped workstation is halted but nothing is lost: its disk, installed tools,
and state all persist; it rests in the Stopped phase until you start it again.
restart takes a workstation down and brings it back up — a real power cycle, not a
reload of its configuration. It works on a stopped workstation too, which simply
starts. When it targets several workstations they all share one restart, so re-running
the same command is what triggers the next one.
| Flag | Description |
|---|---|
-l, --selector <k=v> | Label selector (key=value[,key=value]) instead of name(s). |
-n, --namespace <ns> | Namespace. |
-A, --all-namespaces | Operate across all namespaces. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
rl workstation get-resolved-configuration <name>
Print the merged effective configuration a workstation resolves to: the answer to “what configuration will my workstation actually get?”.
rl workstation get-resolved-configuration my-box
rl ws get-resolved-configuration my-box -o jsonThe configuration is resolved on demand from the workstation, every
WorkstationConfig layered onto it
(explicit spec.configs[] references and selector matches), and any
CloudIdentity governing it — by the
same resolver the control plane uses to configure the workstation. It is not read from
status.
| Flag | Description |
|---|---|
-o, --output <format> | yaml (default) or json. |
-n, --namespace <ns> | Namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
The document has four top-level keys:
| Key | Description |
|---|---|
workstation | <namespace>/<name>, so a pasted document says what it is about. |
effectiveConfigHash | The configuration fingerprint the control plane wants, read from status.config.effectiveHash. Empty means the workstation has not been set up yet. |
appliedConfigs | Each config that shaped the workstation: its name, its source (explicit for a spec.configs[] reference, selector for a label match), and the config-layer priority it was applied at (lower applies first, so a higher priority wins). |
effective | The whole merged spec — including everything status never carried: environment, files, scripts, sources, devtools, toolconfigs, and dotfiles. |
appliedConfigs:
- name: ai-box
priority: 200
source: explicit
- name: base
priority: 100
source: selector
effective:
devtools:
- name: nodejs
packages:
- git
shell: /bin/bash
uid: 1000
user: dev
effectiveConfigHash: 8f2c1a9e4d6b...
workstation: local/my-boxeffective is the whole spec the workstation is set up to match, so a real document is longer
than this: alongside what you declared, it carries the defaults Ringleader resolved
for you (the login user and shell, the diagnostics settings, the node key
authorized on the workstation, the agent version it should run).
Safe to paste into a ticket
${secret:NAME} references are printed verbatim. The command resolves no
secret values and needs no access to any Secret.A workstation homed at a control plane you are logged into is resolved by that control plane, so the answer is the one its control loop uses on the workstation — not a local approximation.
rl shell [workstation] [command...]
Open a shell on a workstation over SSH using the node key.
rl shell my-box # interactive shell
rl shell my-box -- uname -a # run a remote command
rl shell # the default Running workstation| Flag | Description |
|---|---|
-w, --workstation <name> | Target workstation (alias --name). |
-n, --namespace <ns> | Namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
Arguments after -- are run as the remote command verbatim; with none, you get an
interactive shell.
Remote-command semantics
rl shell my-box -- <cmd> hands <cmd> to ssh, which behaves exactly as it does
for ssh my-box <cmd>. Two consequences surprise people, and neither is specific to
Ringleader:
Your local shell expands the arguments first.
rl shell my-box -- ls ~sends your machine’s home directory to the workstation. Quote it (rl shell my-box -- ls '~') to let the remote shell expand it, or use an absolute path.The command runs in a non-login, non-interactive shell.
/etc/profile,/etc/profile.d/*and~/.profileare not sourced, so anything exported from a profile fragment is missing. That covers aPATHentry added by a devtool, the forwarded SSH agent’sSSH_AUTH_SOCK, and shell integrations such asfzf. Environment variables you declare underenvironment:in aWorkstationConfigare the exception: Ringleader also writes them to/etc/environment, which PAM reads for every SSH session, so those are set either way. Wrap the command in a login shell when you need the rest:rl shell my-box -- bash -lc 'node --version'
git over SSH in a remote command
SSH_AUTH_SOCK for login shells. A non-interactive remote command therefore does
not see the agent, and git clone git@github.com:… will fail. Until this is fixed,
run it under a login shell as shown above:
rl shell my-box -- bash -lc 'git -C /home/dev/repo pull'.Using ssh directly
rl is a convenience wrapper, not a walled garden. While the daemon is running it
maintains a real OpenSSH configuration for every workstation you can reach, and
rewrites it in real time as workstations are created, started, stopped, and deleted.
Point your own ~/.ssh/config at it, and every OpenSSH-speaking tool reaches a
workstation with no rl in the loop.
Add this as the first line of ~/.ssh/config (create the file if it does not
exist, with mode 600):
Include ~/.ringleader/ssh/configThat is all. With a running daemon and a Running workstation named my-box in
namespace dev, all of these now work:
ssh my-box # or my-box.dev, or my-box.dev.ringleader
ssh my-box uname -a
scp report.csv my-box:/home/dev/
rsync -av ./src/ my-box:/home/dev/src/
sftp my-box
git clone my-box:/home/dev/repo.gitSo do the tools that read ~/.ssh/config for you: VS Code and Cursor
Remote-SSH, JetBrains Gateway, and Ansible.
Each workstation gets three Host aliases: the bare name (my-box), the
namespace-qualified name (my-box.dev), and a fully-qualified form
(my-box.dev.ringleader). The .ringleader suffix is a deliberately non-resolvable
pseudo-TLD, so the qualified alias can never shadow a real hostname.
Put the Include above everything else
OpenSSH uses the first value it obtains for each parameter, so an Include
placed after your own Host * block is silently overridden by it. In particular, a
Host * block that sets IdentityFile will make ssh offer the wrong key and fail
to authenticate. Put the Include line above every Host and Match block in
~/.ssh/config.
For the same reason, note that the bare alias claims the workstation’s name in your
SSH namespace: a workstation called prod would shadow a Host prod of your own.
Use the <name>.<namespace>.ringleader spelling when in doubt.
What Ringleader manages for you
The Include pulls in ~/.ringleader/ssh/*.conf, one file per workstation. Every
file is owned by the daemon and regenerated on change. Never edit them: your
edits will be overwritten, and deleting a workstation prunes its file.
Each block carries:
- the workstation’s current address, port, and login user, refreshed when they change;
- Ringleader’s node key as the
IdentityFile, withIdentitiesOnly yesso none of your own keys are offered; - a per-workstation
known_hostsfile with the host key pinned once the daemon has confirmed it, so your own~/.ssh/known_hostsis never touched and recreating a workstation under the same name cannot produce a host-key-changed warning; - connection multiplexing (
ControlMaster), so repeated commands reuse one connection instead of re-handshaking. (Not on Windows: Win32-OpenSSH does not support it.)
The configuration is only as fresh as the daemon: if rl daemon is not running, the
files are frozen at their last state, and an address that changed since then will not
connect. Check with rl daemon status.
rl code [workstation]
Open a workstation in a desktop editor over Remote-SSH: VS Code, Cursor, or VS Code
Insiders. Also available as rl workstation code.
rl code my-box
rl code my-box --editor cursor
rl code my-box --path /home/dev/other-repo
rl code my-box --print # show the command, launch nothingThe workstation is addressed by its collision-proof
<name>.<namespace>.ringleader alias, and the folder opened is the first
spec.sources[] path of the resolved
configuration — override it with --path. With no sources, a window connects to the
host with no folder open. The editor is autodetected in the order code, cursor,
code-insiders; pin one with --editor.
Remote-SSH shells out to the system ssh and reads your own ~/.ssh/config, so this
needs the Ringleader include wired in (rl ssh-config install — see
Using ssh directly). A missing or misplaced include is warned
about, not fatal.
The first connect downloads a server
update.code.visualstudio.com. A workstation with restricted outbound access may sit
at “Setting up SSH Host” until that fetch can complete.| Flag | Description |
|---|---|
-w, --workstation <name> | Workstation to open (defaults to the sole Ready one; required when several are). |
--editor <name> | code, cursor, or code-insiders. Default: autodetect in that order. |
--path <dir> | Absolute folder to open on the workstation, overriding the resolved source path. |
--print | Print the command that would run, one argument per line, and launch nothing. |
-n, --namespace <ns> | Namespace. |
-A, --all-namespaces | Resolve across all namespaces. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
rl tmux [workstation]
Attach to a persistent tmux session on the workstation (tmux new-session -A -s ringleader). On a transport drop it reconnects automatically after a backoff,
ideal for long-running work over a flaky connection.
rl tmux my-box| Flag | Description |
|---|---|
-w, --workstation <name> | Target workstation (alias --name). |
-n, --namespace <ns> | Namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
rl logs <workstation>
Stream the in-VM ringleader-agent logs (or a managed service’s logs), like
kubectl logs. It automatically picks the systemd journal or the durable
log file on the workstation.
rl logs my-box -f # follow
rl logs my-box --tail 200
rl logs my-box --since 30m
rl logs my-box --source service:code-server| Flag | Description |
|---|---|
-f, --follow | Stream new lines as they arrive. |
--tail <N> | Show only the last N lines (0 = all). |
--since <duration> | Window to show (e.g. 30m, 1h; journal only). |
--source <source> | agent (default) or service:<name>. |
-n, --namespace <ns> | Namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
rl troubleshoot <workstation>
Gather a diagnostic bundle: the controller-side view (phase, conditions, last update), live system probes (OS, docker, disk, user, agent), and the in-VM agent’s provisioning timeline.
Run with no workstation to dump local status (daemon, origins/auth, version,
host) instead. Either form takes --diagnostics to write a self-contained zip you
can send for support, containing no tokens, secrets, or private keys.
For the whole diagnosis workflow — and how to capture and share a bundle from the desktop apps — see the Troubleshooting section and Generate a diagnostics bundle.
rl troubleshoot # local status to stdout
rl troubleshoot my-box # diagnose one workstation
rl troubleshoot my-box --failed # only failed steps
rl troubleshoot my-box --kind devtool --since 1h
rl troubleshoot my-box --json # machine-readable ledger
rl troubleshoot --diagnostics # local-only zip
rl troubleshoot --all --diagnostics # zip PLUS every workstation
rl troubleshoot my-box --diagnostics=out.zip
rl troubleshoot my-box --report-only # only the in-VM diagnostic ledger| Flag | Description |
|---|---|
--diagnostics[=PATH] | Write a self-contained diagnostics zip (default ./ringleader-diagnostics-<timestamp>.zip). |
--all | With --diagnostics: also include every workstation in the zip (an error without --diagnostics). |
--failed | Show only failed timeline steps. |
--kind <kind> | Filter the timeline by kind (user, package, devtool, tool-config, script, security-update, self-update). |
--target <substring> | Filter the timeline by target (substring match). |
--since <duration> | Steps started within this window (e.g. 30m, 1h). |
--reconcile latest | Show only the steps from the most recent configuration pass. |
--limit <N> | Show only the most recent N steps (0 = remote default). |
--report-only | Print only the in-VM diagnostic ledger (no controller view or host probes). |
--json | Emit the in-VM diagnostic ledger report as JSON. |
-n, --namespace <ns> | Namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
Note
rl troubleshoot <workstation> --report-only pulls just the in-VM diagnostic
ledger over the daemon’s SSH connection (no controller view or host probes) and
renders it with the same formatter the in-VM agent uses.