Workstation access
Start, stop, connect to, and diagnose a workstation from the command line.
These commands manage a workstation’s lifecycle and connect to a running one. Each of
the access commands, shell, tmux, logs, troubleshoot and code, also runs under
the workstation group, so rl workstation shell my-box is the same command as
rl shell my-box. shell, tmux, logs and troubleshoot read the workstation’s
address and login user from its status.
When you omit the name, shell and tmux choose a workstation for you if exactly one
fits. They look for these, in order:
- The only
Readyworkstation. - When none is
Ready, the only one that is running but not yetReady. They warn you when they choose it. - With
--start, the only workstation, even a stopped one.
Otherwise they list the candidates and stop. Name the one you want with -w, or add -A
to look in every namespace, not only the current one.
rl shell chooses a workstation only when you give it no command. With a command and no
-w, it reads the first word as the workstation’s name. To run a command, name the
workstation first or with -w: rl shell -w my-box -- make test.
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.
Each command returns once the request is accepted, and the machine then changes state on
its own. A cloud workstation takes about a minute to start. Add --wait to block until the
change is done. For start and restart that means Ready. For stop it means Stopped,
which is recorded only once the provider confirms the machine is down.
rl workstation start my-box --wait # returns when my-box is Ready
rl workstation stop my-box --wait --timeout 2mrestart takes a workstation down and brings it back up. It is a real power cycle, not a
reload of its configuration. On a stopped workstation, restart starts it. When it targets
several workstations, they all share one restart, and each new run of the command triggers
the next one.
| Flag | Description |
|---|---|
-l, --selector <selector> | Label selector (key=value[,key!=value,key in (a,b),key,!key]) instead of name(s). |
--wait | Block until each workstation the command moved is Ready (start, restart) or Stopped (stop). |
--timeout <duration> | With --wait, how long to wait. Default 20m; 0 means no limit. |
-n, --namespace <ns> | Namespace. |
-A, --all-namespaces | Operate across all namespaces. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
rl workstation cordon / uncordon
Take a workstation pool member out of service, or
put it back. A cordoned member binds no new lease. One that is leased is deleted when its lease
ends, and one that is idle stays as it is, outside the pool’s warm floors and timers, until you
uncordon it. Target members by name, several names, or a label selector with -l.
rl workstation cordon agents-linux-e5f6a7b8
rl workstation cordon -l app=agent-runner
rl workstation uncordon agents-linux-e5f6a7b8workstation/agents-linux-e5f6a7b8 cordoned
workstation/agents-linux-e5f6a7b8 unchanged: already cordonedThe verb writes an annotation on the workstation, so whoever may update the workstation may cordon it: the pool’s owner, who owns every member, and a namespace admin. On a workstation outside a pool the annotation changes nothing.
| Flag | Description |
|---|---|
-l, --selector <selector> | Label selector (key=value[,key!=value,key in (a,b),key,!key]) instead of name(s). |
-n, --namespace <ns> | Namespace. |
-A, --all-namespaces | Operate across all namespaces. Combine it with -l, not with names. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
rl workstation release-finalizer <name>
Close the record of a workstation stuck in Terminating.
Deleting a workstation tears its machine down first, and the control plane will not close the record until it can confirm the machine is gone. When it cannot confirm, because the provider is unreachable, a cloud credential was revoked, or the configuration it needed was already deleted, it holds the record and keeps retrying, so nothing is ever forgotten silently.
This command ends that hold. It closes the record and does not delete the machine. If a machine is still running it keeps running, keeps costing money, and afterwards nothing tracks it, so you are accepting responsibility for finding and deleting it yourself.
rl workstation release-finalizer my-box \
--acknowledge-orphaned-machine my-box-7f3a91It is restricted to administrators of the namespace, and refuses unless you name the machine you may be leaving behind. Run it without the flag and the refusal tells you the machine’s name; delete that machine in your provider’s console first if you can reach it at all. This command is for the case where you cannot.
| Flag | Description |
|---|---|
--acknowledge-orphaned-machine <machine> | Required. The machine that may be left running. Shown in the refusal if omitted. |
-n, --namespace <ns> | Namespace. |
--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 these 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, with its name, its source and its priority. source is explicit for a spec.configs[] reference and selector for a label match. A lower priority applies first, so a higher priority wins. A config that targets the workstation but was dropped is listed too, with refused: true and a reason such as author-lacks-reach or configmap-key-not-found. |
effective | The whole merged spec, including everything status never carried: environment, files, scripts, sources, devtools, toolconfigs, and dotfiles. |
configCurrency | current when the workstation’s Configured condition was computed from its configuration as it is now. stale when the workstation or one of its configs has changed since. |
configCurrencyAt | The workstation’s metadata.resourceVersion that configCurrency was computed against. |
cloudIdentity | The CloudIdentity whose selector matches the workstation, or <none matched>. Absent for a local provider. |
appliedConfigs:
- name: ai-box
priority: 200
source: explicit
- name: base
priority: 100
source: selector
configCurrency: current
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 sole Ready workstation| Flag | Description |
|---|---|
-w, --workstation <name> | Target workstation. Defaults to the sole Ready one; required when several are. |
-n, --namespace <ns> | Namespace. |
-A, --all-namespaces | Look for the workstation in every namespace you can reach. |
--start | Start the workstation first if it is stopped, and wait until it is Ready. See below. |
--start-timeout <duration> | With --start, how long to wait. Default 20m; 0 means no limit. |
--pty | Give the command a terminal. See below. |
--script <file> | Run this local script file on the workstation. See below. |
--copy <dir> | Send this local directory along with --script. |
--exit-code-file <path> | Write the remote command’s exit code to this file. See below. |
--host | Run a command or --script on the workstation’s VM instead of inside its dev container. It needs a command. For a shell on the VM, use the <user>-host login described in Devcontainers. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
Arguments after -- are the remote command’s argv, delivered word for word; with
none, you get an interactive shell.
Starting a stopped workstation
--start brings a stopped workstation up before connecting. It requests a start, waits
until the workstation is Ready, then connects. If the workstation is already starting,
--start waits for it. If it is already running, rl connects immediately. Everything
--start prints goes to stderr, so stdout still carries only the command’s own output.
rl shell --start my-box -- make test # starts my-box, waits, then runs
rl tmux --start my-boxIf a start cannot bring the workstation up, --start stops before connecting and prints
one line saying why. That happens in two cases:
- The workstation is being deleted.
- The workstation is in the
Failedphase and nothing is retrying it. The line then names what will move it, such asrl workstation restart.
A workstation stopped by its maximum run time starts again, for a new run.
Exit codes
The remote command’s exit code passes through unchanged. When Ringleader itself fails,
rl shell exits 1 if it could not find or reach the workstation. It exits 2 for a
usage error, or when it could not choose one workstation for you. An ssh connection failure
exits 255. So 1 and 2 can mean either a remote failure or a Ringleader one.
--exit-code-file tells them apart without reading stderr. The file is written only if the
remote command ran, and it holds that command’s exit code. An absent file means Ringleader
failed. A file already at that path is removed first.
rl shell my-box --exit-code-file /tmp/ec --script ./ci.sh
[ -f /tmp/ec ] || exit 75 # never ran: retry rather than report a result
exit "$(cat /tmp/ec)" # the remote command's own exit codeRemote-command semantics
The workstation receives exactly the argv you wrote, one word for one word. A
metacharacter inside an argument is data, not syntax, so rl shell my-box -- printf '%s|' a d
prints a|d|, and -H "Authorization: Bearer $TOKEN" arrives as a single argument rather
than three. It is the contract docker exec and kubectl exec offer over the same ground,
and it is what makes rl shell safe to drive from a script.
Asking for a remote shell needs no flag, because sh -c is itself just argv:
rl shell my-box -- sh -c 'cd ~/src && make test 2>&1 | tail -20'Pipes, &&, redirections, globs and ~ inside that quoted string are the workstation’s
own shell to interpret, which is the point of spelling it out.
Two things about the remote command surprise people, and neither is specific to Ringleader:
~,$HOMEand globs expand on your machine, not on the workstation. Your shell runs beforerldoes: by the timerlholds an argv,~/srchas already become/home/you/src, and no implementation can tell the two apart afterwards. Sorl shell my-box -- ls ~asks the workstation for your machine’s home path, and quoting it asrl shell my-box -- ls '~'sends a literal~, which fails withls: cannot access '~': No such file or directory. Expand it where you mean it withrl shell my-box -- sh -c 'ls ~', or pass an absolute path. Ansible’scommandmodule documents the same property, and draws the same line with its separateshellmodule.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 and shell integrations such asfzf. Two things reach the command anyway, because neither travels by a profile fragment: environment variables you declare underenvironment:in aWorkstationConfig, which Ringleader also writes to/etc/environmentfor PAM to read on every SSH session, and the forwarded SSH agent (see the note below). 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
Ringleader forwards your local SSH agent into the workstation and makes
SSH_AUTH_SOCK ambient at the sshd level, so every session gets it. A plain
rl shell my-box -- git -C /home/dev/repo pull authenticates with your own keys, and
so does a git run by a script or an on-box agent. No login shell is needed for this,
and none of your private keys ever reach the workstation.
Making it ambient is best-effort: it needs the workstation to grant Ringleader’s
delivery sudo rule, and an sshd whose configuration Includes
/etc/ssh/sshd_config.d/. Both hold on every image Ringleader ships, so this is worth
checking only on a custom image or a login identity without that rule.
rl troubleshoot my-box answers it, in two places:
An
ambient agentline in the controller view, naming which shells lost the agent and which precondition stopped the delivery. It is written only when something is missing, so a workstation that got both files shows no such line, and neither does one your daemon has never connected to, so its absence is not by itself proof that the delivery worked.An
ssh-agent-envprobe under system probes, which is the half that confirms the working case. It readsSSH_AUTH_SOCKand both files over the same non-login, non-interactive session your commands run in, and reports whethersshdreads/etc/ssh/sshd_config.d/at all:ssh-agent-env : SSH_AUTH_SOCK=/home/dev/.ringleader/ssh-auth-sock profile.d fragment: present sshd drop-in: present sshd Include sshd_config.d: yes
rl shell my-box -- printenv SSH_AUTH_SOCK still shows the same variable directly, and is
the quicker check when all you want is a yes or no.
Giving the command a terminal
An interactive shell has a terminal. A remote command runs without one, and some programs need one anyway: an editor, a pager, or a command that leaves you at a shell prompt when it finishes.
--pty gives the remote command a terminal:
rl shell --pty my-box -- bash -lc 'cd ~/src/worktree && exec $SHELL -l'That command moves to a worktree and replaces itself with a login shell, so you land at a prompt already in the right directory.
The cost is that a terminal merges the remote’s stderr into its stdout. Use --pty for a
command you are driving by hand, and leave it off for one whose output you are capturing.
--pty does not combine with --script or --copy. A script travels as a file and runs
as a file, so there is nothing for a terminal to attach to. Asking for both stops with an
error.
Running a local script file
--script takes a script that lives on your machine, copies it to the workstation under
its own name, and runs it there. Its exit code becomes the command’s.
rl shell my-box --script ./setup.shThe script’s bytes are never text a shell parses. They travel as a file, are unpacked as a
file and are executed as a file, so a backtick, $(…), |, * or a newline inside the script
means exactly what it would mean had you typed the file on the workstation. That is the whole
point of the flag, and it is what a heredoc (-- bash -s <<EOF) cannot give you: an unquoted
heredoc is evaluated by your local shell before the bytes ever leave, so a backtick in a
comment is enough to run something at home.
--copy sends a local directory along with the script, and runs the script from inside the
copied directory. Use it for a script that reads its own fixtures or sources a file next to it:
rl shell my-box --copy ./demo --script ./demo/run.shThe script must be inside the directory you copy. Neither flag combines with a trailing command: a run is either a remote argv or a script file, never both.
rl file cp [-r] <src> <dst>
Copy a file or directory between your machine and a workstation. One side is a local
path, the other is workstation:path, in the style of scp:
rl file cp ./local.txt my-box:/remote/path # upload
rl file cp my-box:/remote/path ./local.txt # download
rl file cp -r ./dir my-box:/remote/dir # a whole directory, either way
rl file cp ./notes.md :/tmp/notes.md # empty name: the same box rl shell would pick
rl file cp ./notes.md my-box: # empty path: the login user's home directoryA colon before any slash makes an argument remote, so a local file whose name
contains a colon is written ./name.
The destination is the path that will exist afterwards. The one exception is
cp(1)’s and applies to a single file only: a destination naming an existing
directory, or ending in a separator, receives the file under its own basename. A -r
copy never appends a basename, so running it twice does what running it once did,
where scp would nest dir/dir on the second run.
| Flag | Description |
|---|---|
-r, --recursive | Copy a directory and everything under it. Required for a directory, refused for anything else, in both directions. |
--host | Copy to and from the workstation’s VM rather than the dev container it hosts. |
-n, --namespace <ns> | Namespace. |
-A, --all-namespaces | Look across every namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
What you can rely on:
- A single file lands by a rename, so a transfer that fails leaves the destination exactly as it was.
- A
-rcopy merges into its destination and is not atomic as a tree, though each file within it still is. - Symlinks are never created by a download, and a directory copied up follows only links that stay inside it.
- Any failed transfer exits non-zero and says what failed.
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.
Run this once:
rl ssh-config installIt adds one Include line at the top of ~/.ssh/config, creating the file if it does
not exist, and leaves the rest of the file alone. rl ssh-config
says what it writes and how to check or remove it.
That 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 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 ssh-config
Manage the one Include line in your ~/.ssh/config that
Using ssh directly depends on. The command reads and edits
files on your machine only, so it needs no sign-in and no running daemon. The
Ringleader desktop app runs it for you, from the onboarding step Set up ssh for
workstations and the Settings switch Reach workstations with plain ssh. It is
kept out of rl --help, and rl ssh-config --help describes it.
rl ssh-config # print the Include line for this data directory
rl ssh-config install # put the line at the top of ~/.ssh/config, or move it back there
rl ssh-config status # installed, not installed, or installed in the wrong place
rl ssh-config check # the same report, exiting 1 when something is wrong
rl ssh-config uninstall # remove the line and nothing elseEvery form takes --home <dir> to name a different data directory. status also
takes -o json. A bare rl ssh-config and rl ssh-config print do the same thing.
On Windows the file is %USERPROFILE%\.ssh\config, which the Windows OpenSSH client
reads.
What install writes
rl ssh-config install adds three lines at the very top of ~/.ssh/config, above
every other line in the file:
# BEGIN ringleader-managed - DO NOT EDIT (managed by `rl ssh-config`)
Include /Users/you/.ringleader/ssh/config
# END ringleader-managedThe path is your data directory’s, written in full. That is ~/.ringleader/ssh/config
on a Mac or %USERPROFILE%\.ringleader\ssh\config on Windows, unless RINGLEADER_HOME
or --home names another directory. The file it includes is the configuration the
daemon writes, described in
What Ringleader manages for you.
When ~/.ssh or the config file does not exist, install creates them. An existing
file keeps its permissions, its other lines and its line endings. The command prints
what it did, then the line it wrote, then the notes described under status below:
Added the Ringleader include to /Users/you/.ssh/config.
Include /Users/you/.ringleader/ssh/config
Plain ssh/scp/rsync/git and VS Code Remote-SSH now reach your workstations by name.
- the included config is regenerated by `rl daemon`; with the daemon stopped it freezes, and OpenSSH's ControlPersist can hold a master open against a stale address for ~5 minutes after a box movesRunning it again when the block is already in place changes nothing, and the first
line reads Already installed in /Users/you/.ssh/config (no change). instead.
Reading status
rl ssh-config status names the two files it compares and the state of the block,
then lists each problem with a ! and each note with a -:
Your config: /Users/you/.ssh/config
Managed config: /Users/you/.ringleader/ssh/config
State: installed
- the included config is regenerated by `rl daemon`; with the daemon stopped it freezes, and OpenSSH's ControlPersist can hold a master open against a stale address for ~5 minutes after a box movesThe state is one of these:
installed: the block is at the top of the file and includes this data directory’s config.not installed: there is no block, or no~/.ssh/configat all.installed, but BELOW other directives (ssh may offer the wrong key): another setting in the file comes before the block. Comments and blank lines above it do not count. See A misplaced include.installed, but pointing at another data directory: the block includes a different data directory’s config, which happens whenRINGLEADER_HOMEchanged after it was installed.
-o json prints the same report as JSON. Its state is installed, not-installed
or misplaced, where misplaced covers both wrong-place cases above.
Two notes appear whenever they apply, and neither counts as a problem:
- An alias collision. Every workstation gets a
Hostentry under its bare name, and the block comes first. Sossh devreaches a workstation calleddeveven when your config has its ownHost dev. The note names each such workstation. Reach your own host some other way, or address the workstation asdev.<namespace>.ringleader. - A hand-written
Includeof the same path outside the block. It is harmless, anduninstallleaves it in place.
A third note is always printed: the daemon rewrites the included config, so it freezes
when the daemon stops, and a multiplexed ssh connection can keep an old address for
about five minutes after a workstation moves.
A misplaced include
OpenSSH keeps the first value it reads for each setting, and an Include written after
a Host line applies to that host alone. So a block below your own rules can lose. A
Host * with an IdentityFile above it makes ssh offer your key instead of
Ringleader’s, and the workstation refuses it. An Include at the end of the file may
never be read at all. rl ssh-config install fixes both by moving the block back to the
top, and says so:
Repaired the Ringleader include in /Users/you/.ssh/config (it now sits above every other directive).check
rl ssh-config check prints the same report as status, then exits 1 unless the
state is installed, repeating each problem on standard error. Use it in a script, or
before a command that needs the include:
rl ssh-config check && ssh my-boxAn alias collision and a hand-written duplicate are notes, so neither fails the check.
uninstall
rl ssh-config uninstall removes the three lines and nothing else. rl shell and
rl tmux keep working, because they hand the daemon’s config to ssh directly. When
there is no block to remove it says so and exits 0.
| Flag | Description |
|---|---|
--home <dir> | Data directory. Every form takes it. |
-o, --output <format> | status only: text (default) or json. |
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 runs the system ssh and reads your own ~/.ssh/config, so the Ringleader
include must be installed there with rl ssh-config install. When
it is missing or in the wrong place, rl code prints a warning and opens the editor
anyway.
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. Defaults to the sole Ready one; required when several are. |
--start | Start the workstation first if it is stopped, and wait until it is Ready. See Starting a stopped workstation. |
--start-timeout <duration> | With --start, how long to wait. Default 20m; 0 means no limit. |
-n, --namespace <ns> | Namespace. |
-A, --all-namespaces | Look for the workstation in every namespace you can reach. |
--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>
Diagnose one workstation. The report has these sections, in this order:
- The controller view: phase, conditions, last update, and the ambient SSH agent when its delivery fell short.
- For a workstation with an egress policy, an
egresssection. It shows the rule set the workstation runs, the destinations it permits, and any it could not enforce. It names the edge VM on your machine that serves the workstation, when there is one. When it can, it also tests from inside the workstation which destinations answer. It lists the connections the workstation was refused in the last 24 hours, newest first, up to 10 rows. When there are any, it ends with therl workstation egresscommand that lists the rest. - The port forwarding on your machine: the LocalBindings that forward from the workstation, or a note that none does.
- Live system probes: OS, docker, disk, user, agent and ssh-agent-env.
- 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
rl troubleshoot --diagnostics --log-since=6h # reach 6 hours back in the daemon logThe bundled daemon log is trimmed to a byte budget, so a bundle captured a while after
an incident can carry a window that begins after it. The covered wall-clock range is
printed when the zip is written and recorded inside it. Check it against the time you
are reporting, and use --log-since to reach further back. (--log-since moves the
daemon log; --since filters a workstation’s in-VM timeline.)
| 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. |
--log-since <duration> | With --diagnostics: make the bundled daemon log reach at least this far back in wall clock (e.g. 90m, 6h), across rotated segments. |
--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.rl workstation egress [workstation]
List what a workstation’s Edge recorded: the connections it refused, the connections it let
through, or what it did with each Integration’s credential. Use it to find out why a workstation
cannot reach a destination, to see what it did reach, and to turn that into a strict egress
policy. What an Edge records says what
each record holds and how long Ringleader keeps it.
rl workstation egress dev-box # what was refused for dev-box, last 7 days
rl workstation egress # the same for every workstation you may read
rl workstation egress dev-box --since 24h # the last day
rl workstation egress dev-box --allowed # what dev-box reached
rl workstation egress dev-box --suggest # a strict policy for what dev-box reached
rl workstation egress dev-box --credentials # credential use, per Integration and hostEach call lists one kind of record. Pass at most one of --denied, --allowed and
--credentials; more than one is a usage error.
| Flag | Description |
|---|---|
--denied | List refused connections. This is the default. |
--allowed | List allowed connections, for a workstation that records them. |
--credentials | List credential use per Integration and host. |
--suggest | Print a strict egress policy built from the allowed connections. See Suggest a policy. |
--since <when> | Start of the window: a duration back from now, such as 24h or 168h, or an RFC 3339 time. Default: 7 days ago. |
--until <when> | End of the window, in the same forms as --since. Default: now. |
--json | Print the rows as JSON. |
-n, --namespace <ns> | Namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
Who can read what
A member reads the records of the workstations they own. A namespace administrator reads every workstation’s in the namespace.
Records are counted per UTC day, and the window starts on the day that holds --since. The
records of a cloud workstation are kept on the control plane. The records of a local workstation
stay on the machine it runs on, and only the user of that machine reads them. With no
workstation name, the command reads both places and adds a FROM column that says where each
row came from, and a BOX column that names the workstation.
Reading the list
Each kind of record has its own table. The allowed connections table has one row per day, destination and port, with these columns:
| Column | Meaning |
|---|---|
DAY | The UTC day. |
DESTINATION | The name the Edge read off the connection, or the address when it read no name. |
CONNECTIONS | How many connections the Edge let through. |
BYTES OUT | Bytes the workstation sent. |
BYTES IN | Bytes the workstation received. |
One read returns at most 1,000 rows. When the window holds more, the command says so on standard
error, and you narrow it with --since and --until. A row that reads other destinations
counts what a workstation reached after it passed its limit for the day, and a note under the
table says so.
An empty --allowed list does not prove that nothing was reached. A workstation records allowed
connections only when its policy is audit, or when its Edge sets records.allowed, and its
status.egress.recorded lists allowed when it
does. When the workstation says so, an empty list means no allowed connection fell in the window.
When it does not, the command says that recording may be off.
--json prints the same rows, each with the from it was read from. When a named workstation
has no records because nothing keeps them, notRecorded says why.
Suggest a policy
--suggest turns what a workstation reached into a strict policy. It needs exactly one
workstation name, and it cannot be combined with --denied, --allowed, --credentials or
--json; either mistake is a usage error.
rl workstation egress dev-box --suggest > egress.yamlThe command writes the egress block as YAML on standard output, and its notes on standard
error, so the file holds only the policy.
- It keeps the destinations the workstation already declares, then adds each allowed connection they do not cover. It copies names and addresses exactly as the Edge recorded them.
- It leaves out a hostname on any port other than 80 and 443, and a note says how many. To reach
such a host, declare its address range as a
cidr. - It reads one UTC day at a time. A note names a day that reached the row limit or holds an
other destinationsrow, because the policy may then be incomplete. - It uses the same default window as the other kinds, the last 7 days.
- It refuses when nothing was recorded, and when the workstation records no allowed connections.
A workstation on hcs, vz or qemu that its own network process serves, and whose policy is not
audit, records none.
Find out what a workstation reaches
walks from an audit policy to a strict one.
Exit codes
| Code | Meaning |
|---|---|
0 | At least one store answered, and none failed. |
1 | A read failed, no store answered, or the workstation does not exist. |
2 | A usage error: a flag combination, a time that does not parse, a --since that is not before --until, or no namespace. |
rl workstationpool
Manage workstation pools: sets of workstations
built from one template and leased out one at a time. The group has the generic verbs every
kind has (get, describe, edit, copy, delete, wait) plus the ones below. Aliases:
wsp for workstationpool and wspl for workstationpoollease.
rl workstationpool describe lists the pool’s members and leases by name under its status.
rl workstationpool delete asks you to confirm, because it deletes every member’s machine and
disk. When leased members remain, it prints that they stay until their leases end, with the
command that releases those leases. rl workstationpool wait without --for waits until the
pool’s template is healthy, its namespace has room, and its warm floors are filled.
rl workstationpool create <name>
Create a pool whose template is a copy of an existing workstation’s configuration:
rl workstationpool create agents-linux --from-workstation ci-runner-1 --size-max 8
rl workstationpool create agents-linux --from-workstation ci-runner-1 --size-max 8 --warm-running 2 --recycle
rl workstationpool create agents-linux --from-workstation ci-runner-1 --size-max 8 --dry-run -o yamlThe workstation must be in the pool’s namespace. The command copies its spec and labels, and
leaves out what the pool owns: the power and lifetime fields, the pools.ringleader.dev/
labels, and spec.identity.authorizedKeys, which would open every member to those keys. It
names each dropped field on stderr. A configuration that reached the workstation by selector,
and that would stop reaching a member someone else owns, is added to the template’s configs
at the priority it applied at. The command warns when you are a person, because every device
you own can open a shell on every member, and with --recycle, because a recycled member keeps
whatever its last holder left. Both are advice, and the pool is created either way.
Every lease holder can read the template, and a secret the template consumes reaches every
holder. Check the template with --dry-run -o yaml before you create the pool. A name that is
already taken fails, and nothing is written.
| Flag | Description |
|---|---|
--from-workstation <name> | Required. The workstation whose configuration becomes the template. |
--size-max <n> | Required. The most members the pool holds at once. |
--warm-running <n> | Idle members to keep running and ready. |
--warm-stopped <n> | Idle members to keep stopped and ready to start. |
--recycle | Return a released member to the pool instead of deleting it. |
--dry-run | Print the pool as a manifest instead of creating it. |
-o, --output <format> | Print the manifest instead of creating: yaml (default) or json. |
-n, --namespace <ns> | Namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
rl workstationpool lease <pool>
Lease a workstation from a pool. The pool binds one of its members as soon as one is available, idle, starting or newly built, and that member is yours until the lease ends:
rl workstationpool lease agents-linux --name my-build --label purpose=ci --wait
workstationpoollease/my-build Ready on agents-linux-e5f6a7b8
rl shell agents-linux-e5f6a7b8The lease carries this device’s own public key unless you pass --key, so rl shell --script
and rl file cp work against the member at once. Without --wait the command returns as soon
as the lease is written, and rl workstationpoollease get shows its phase. With --wait it
blocks until the lease is Ready or Failed, then prints the lease and its member. If the wait
times out, the lease still exists and may still bind a member, and the message names the
release command. rl workstationpoollease create <pool> is the same command under the lease’s
own noun.
| Flag | Description |
|---|---|
--name <name> | The lease’s name. Default: generated from the pool’s name. |
--key <public-key> | A public key authorized on the member while the lease holds it, repeatable. Default: this device’s own key. |
--expires-in <duration> | How long the lease runs before the pool treats it as abandoned. Default: the pool’s lease.maxTerm. |
--label <key=value> | A label to add, repeatable. |
--wait | Block until the lease is Ready or Failed. |
--timeout <duration> | With --wait, how long to wait. Default 20m; 0 means no limit. |
--dry-run | Print the lease as a manifest instead of creating it. |
-o, --output <format> | Print the manifest instead of creating: yaml (default) or json. |
-n, --namespace <ns> | Namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
rl workstationpool share / unshare / access
Let a teammate lease from a pool you own, take that back, or report who may lease and through which Grant. They are on the Sharing & access page.
rl workstationpool usage <pool>
Print a pool’s hourly usage history: its demand, waits, endings and idle time, one row per hour, newest first. History is kept for the last 168 hours, and nothing older is served.
rl workstationpool usage agents-linux
rl workstationpool usage agents-linux --since 24h -o jsonThe table has a column for each counter: leases created and bound, the most held at once, the
most waiting, the average wait to Ready, leases that expired before they were bound, leases
released with Done, recycled, whose recycle failed, expired, and whose member was lost,
members built and deleted, and the seconds members spent idle running, idle stopped, leased,
being built and being recycled. Anyone who can read the pool can read its history. A control
plane that keeps no usage store says so instead of printing a table.
| Flag | Description |
|---|---|
--since <duration> | How far back to read. Default: the whole kept history, 168h. |
-o, --output <format> | json or yaml. Default: a table. |
-n, --namespace <ns> | Namespace. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |
rl workstationpoollease
Manage the leases themselves. create is
rl workstationpool lease
under this noun, and the generic verbs apply. rl workstationpoollease get prints each lease’s
holder, pool, phase, member and expiry, and --mine narrows it to yours. rl workstationpoollease wait without --for waits for Ready, and a Failed lease ends the wait. Deleting a lease
releases it: the member is deleted, or recycled if you had already asked for that.
rl workstationpoollease renew
Move a lease’s expiry later, so the pool does not treat it as abandoned. Target leases by name,
several names, or a label selector with -l. Each renewed lease prints its new expiry. A lease
that has been released or has failed is reported and left alone.
rl workstationpoollease renew my-build --expires-in 4h
rl workstationpoollease renew -l purpose=ciworkstationpoollease acme-dev/my-build is renewed until 2026-10-20T22:00:00ZWithout --expires-in, the lease runs for the pool’s lease.maxTerm from now. The expiry may
not be more than one lease.maxTerm ahead, so a renewal with a longer --expires-in is refused.
| Flag | Description |
|---|---|
--expires-in <duration> | How long the renewed lease runs. Default: the pool’s lease.maxTerm. |
-l, --selector <selector> | Label selector 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 workstationpoollease release
End one or more leases. By default the pool deletes the member. With --recycle it asks the pool
to clean the member and keep it for the next lease, which the pool does only when it declares a
recycle block and none of the reasons on the
pool page stops it. A lease whose release
was already requested is reported and left alone.
rl workstationpoollease release my-build
rl workstationpoollease release -l purpose=ci --recycleworkstationpoollease acme-dev/my-build is releasing (its member will be deleted)| Flag | Description |
|---|---|
--recycle | Ask the pool to recycle the member instead of deleting it, if the pool recycles. |
--ignore-not-found | Exit successfully when a named lease is missing. |
-l, --selector <selector> | Label selector instead of name(s). |
-n, --namespace <ns> | Namespace. |
-A, --all-namespaces | Operate across all namespaces. |
--as <subject> | Impersonate a subject. |
--home <dir> | Data directory. |