Specification¶
Status
This is the living design document for vlx. It describes v0.1 and records the decisions behind it. Changes to behaviour start as a change to this page.
1. Summary¶
vlx (Varlink explorer) is a terminal UI and command-line tool for
discovering, reading and calling Varlink services —
above all systemd's io.systemd.* APIs. It is to Varlink what d-feet and
busctl are to D-Bus: point it at a host and browse every service, interface,
method, type and error, read the documentation that ships in the IDL, and call
methods with a parameter editor that knows the schema.
vlx is built on sdvarlink for the wire protocol, introspection and IDL parsing, on Cobra and Viper for the CLI and configuration, and on Charm's Bubble Tea, Bubbles and Lip Gloss for the interface.
2. Goals¶
- Zero to useful in one keystroke.
vlxwith no arguments finds the Varlink sockets on the host and opens the explorer. No flags, no config. - Discoverable. Every screen shows the keys that work on it. Anything the
TUI can do, it can also print as the equivalent
vlxandvarlinkctlcommand, so the TUI teaches the CLI. - Safe by default. vlx starts read-only. Methods that change the system need an explicit opt-in and a confirmation; destructive ones need the method name typed back. See §6.
- Schema-aware. Parameters are pre-filled from the IDL and validated locally before anything is sent.
- Scriptable. Every TUI capability has a non-interactive command with stable JSON output and meaningful exit codes.
- Beautiful and accessible. Adaptive light/dark colours, honours
NO_COLOR, works in 80×24, never relies on colour alone to convey meaning.
Non-goals (v0.1)¶
- Implementing Varlink services (vlx is a client;
vlx demois the one exception and serves a fixed, harmless interface). - Remote transports (
ssh:,exec:, TCP). vlx speaks to Unix sockets only. Spawning processes is out of scope by design (see §6.5). - Passing file descriptors to services. Received descriptors are closed and reported.
- Persisting call history (it may contain secrets).
3. Users and scenarios¶
| Who | Scenario | What vlx gives them |
|---|---|---|
| systemd user / sysadmin | "What can I ask systemd-resolved over Varlink?" |
Browse io.systemd.Resolve, read docs, run ResolveHostname read-only. |
| Developer on sdvarlink or a Varlink service | "Does my service answer Describe correctly?" |
Point vlx at the socket, call the method, inspect raw JSON and errors. |
| Script author | "I need the hostname as JSON in CI." | vlx call hostname io.systemd.Hostname.Describe -o json. |
| Learner | "What is Varlink?" | vlx demo serves a sample service anywhere, including macOS. |
4. User experience¶
4.1 Launch¶
vlx # discover sockets, open the explorer
vlx explore hostname # open straight at a service (name, path or unix:path)
vlx demo # start the built-in demo service and explore it
Services are named by socket path. Wherever a SERVICE argument is accepted, vlx also accepts:
- an absolute or relative path (
/run/systemd/io.systemd.Hostname), - the
unix:form thatvarlinkctluses (unix:/run/systemd/io.systemd.Hostname), - a discovered socket's base name (
io.systemd.Hostname), or a case-insensitive unique fragment of it (hostname).
Ambiguous fragments are an error that lists the candidates.
4.2 Explorer layout¶
vlx › Services › Resolve › ResolveHostname READ-ONLY
ResolveHostname READ · io.systemd.Resolve · single reply
Resolves a hostname to one or more IP addresses.
╭─ Parameters ──────────────────────────────────────────────────────────╮
│ ▸ name string › github.com │
│ family ?int 2 │
╰──────────────────────────────────────────────────────────────────────╯
○ more ○ oneway ✓ valid ctrl+j edit as JSON
RESULT ✓ 1 reply · 9ms
{ "addresses": [ … ] }
EARLIER ON THIS PAGE
✓ name=example.com 1 reply 11:07
───────────────────────────────────────────────────────────────────────────
› resolve ResolveHostname name=github.com family=2 mirrors the form
ctrl+s send ⇥ command line ctrl+j JSON ctrl+y copy esc back
The explorer is a drill-down: one page at a time, full width, with a command line along the bottom that is always there.
- The header is a breadcrumb of the pages (
Services › Resolve › ResolveHostname), the safety mode badge (READ-ONLYgreen,WRITES ENABLEDamber) and, when running as root, a redROOTbadge.Escalways goes up one level. - Services (home) lists discovered sockets in groups — System, Network, Users & login, Machines & images, Storage, Security, Devices, Your session, Other — each with a plain-language description from a built-in catalog, a status dot and its method count once read. The last few calls of the session are listed first and reopen with their parameters.
- Preview. When the cursor rests on a service for 250 ms, vlx introspects
it in the background (only
org.varlink.service.GetInfoandGetInterfaceDescription, which are read-only). Results are cached per socket. Sockets the user cannot access are not previewed.preview: false(or--no-preview) restores connect-on-open. - A service page lists its interfaces with their methods, types and
errors, each with the first sentence of its doc comment and, for methods, a
safety tag. Glyphs and letters carry meaning so it never depends on
colour:
ƒmethod,⌁streaming method,τtype,!error, andREAD/WRITE/DANGER. Boilerplate interfaces (org.varlink.service,io.systemd.service,io.systemd) start collapsed. - A method page (§4.5) and detail pages for types, errors, method
documentation and raw IDL (
Ctrl+Dswitches between docs and IDL). - The bottom two lines are the command line and the keys that work right now.
4.3 Keys¶
| Key | Action |
|---|---|
↑/↓, PgUp/PgDn, Home/End |
Move |
Enter, → |
Open the selection |
Space |
Expand or collapse an interface |
Esc, ← |
Up one level |
any letter, /, :, Ctrl+K |
Type on the command line (§4.4) |
Ctrl+D |
Documentation and IDL of the selection |
Ctrl+Y |
Copy a vlx call command, name or socket path (OSC 52) |
Ctrl+R |
Refresh: re-discover sockets, re-introspect the service |
?, F1 |
Full help |
q (on a list), Ctrl+C |
Quit |
4.4 The command line¶
The command line searches and runs. Its grammar is shared with vlx call:
Words split like a POSIX shell's (quotes and backslashes; no expansion).
SERVICE resolves like vlx call's argument, and additionally as an interface
name or its last part (unit → io.systemd.Unit) or, alone, as a method
name that exactly one service has. Values are converted with the IDL:
strings and enum values as typed, everything else as JSON. In the explorer
an enum value may be abbreviated to a unique prefix.
- While the line names nothing exactly, a popup lists matches across every
service, interface, method, type and error. The first keystroke
introspects every accessible service in the background (with preview
enabled). Matching is token-based: every word must match a name, a
qualified name or a service's short name — best at the start of a word or a
CamelCase hump, or as a subsequence of the name — so
resolve hostfindsio.systemd.Resolve.ResolveHostname. Methods rank first; an exact name wins.↑/↓pick,Tabcompletes,Enteropens. - Once the line names a method, the popup lists its parameters and which are
set.
Enteropens the method page and loads the arguments; it also sends the call when arguments were given or nothing is required. Sending goes through the same safety checks and confirmations asCtrl+S. - A service that has not been read yet is introspected first and the line runs when it answers.
↑/↓with no popup recall earlier calls' lines. Errors (unknown parameters with the list of valid ones, impossible enum values) show above the line and change nothing.
4.5 Calling a method¶
A method page shows the method's name, safety class, streaming behaviour and up to three lines of documentation, then:
- A parameter form: one row per top-level parameter showing its name,
IDL type and an input that fits the type —
text for
string, a number field forint/float, a‹ choice ›forbooland enums (←/→, space, or type the first letters), and a one-line JSON field for structs, arrays, maps,objectandany(pre-filled with a skeleton). The focused row shows the parameter's doc comment and how to edit it. Optional (?T) parameters are omitted until given a value; for choices the first option is(unset). ↑/↓ move between rows. - The form and the command line are one call. Every edit of the form
rewrites the line (
resolve ResolveHostname name=github.com); every edit of the line for the same method loads into the form.Tabcycles form → command line → result. - Live validation against the IDL on every keystroke. Problems appear
under the field they belong to, including nested ones
(
where:.x: expected int). Sending is disabled while the parameters are invalid. - Ctrl+J switches to a raw JSON editor holding the same parameters, and back. JSON the form cannot represent (unknown keys, an empty optional string) keeps you in JSON mode with an explanation.
- Flags shown as toggles:
more(enabled and forced on for methods that require it, unavailable for methods that do not support it) andoneway. Ctrl+S(orEnteron the command line) sends. Non-read methods go through the confirmation flow in §6.2.- The result shows each reply as pretty, colourised JSON with a counter,
elapsed time and, for streams, a live tail. Errors show the Varlink error
name, its parameters, and the IDL doc for that error if the interface
declares it.
Escstops a stream (which closes the connection, as the protocol requires). - Earlier on this page lists the method's previous calls with their arguments and outcome. A method's parameters and last result are kept until vlx quits.
y(result focused) copies the replies;Ctrl+Ycopies the equivalentvlx call(inNAME=VALUEform when possible) andvarlinkctl callcommands.
4.6 Visual language¶
| Token | Use |
|---|---|
Violet #A78BFA / #7C3AED |
Brand, focus, selection |
Green #30D475 |
READ, success, connected |
Amber #F5B83D |
WRITE, warnings, writes-enabled badge |
Red #F2545B |
DANGER, errors, root badge |
Cyan #22D3EE |
Types and keys in JSON |
Colours are lipgloss.AdaptiveColor pairs for light and dark terminals.
NO_COLOR or --no-color turns all styling off; every state also has a text
or glyph form.
5. Command-line interface¶
One Cobra command per file under internal/cli/.
| Command | Purpose |
|---|---|
vlx / vlx explore [SERVICE] |
Open the TUI. |
vlx list (ls) |
List discovered sockets; --probe asks each for GetInfo. |
vlx info SERVICE |
Vendor, product, version, URL and interfaces. |
vlx describe SERVICE [INTERFACE] (introspect) |
Print IDL, highlighted on a TTY. |
vlx template SERVICE METHOD |
Print a JSON parameter skeleton for a method. |
vlx call SERVICE METHOD [PARAMS \| NAME=VALUE...] |
Call a method. |
vlx demo |
Serve the demo interface and explore it (--serve to only serve). The socket lives in $XDG_RUNTIME_DIR/vlx/ so other commands find it as demo. |
vlx config path\|show\|init |
Inspect or create the config file. |
vlx version |
Version, commit, build date, Go version. |
vlx completion SHELL |
Shell completion (Cobra built-in). |
vlx call details:
PARAMSis a JSON object,-for stdin, or-f FILE. Omitted means{}. Alternatively, parameters areNAME=VALUEwords (the explorer's command line grammar, §4.4): values are converted using the IDL — strings and enums as typed, everything else as JSON. Unknown and repeated names are usage errors.--morestreams (newline-delimited JSON on stdout, one reply per line), and is turned on automatically for methods that require it;--onewaysends without waiting;--max-replies Ncaps streams (and implies--morewhere supported).--no-validateskips introspection for services that cannot describe themselves; the safety policy still applies.- With
-o json, a Varlink error reply is also printed to stdout as{"error": NAME, "parameters": {...}}. --dry-runvalidates the parameters and prints the message that would be sent, without connecting.--allow-writes/--yes: see §6.2.- Method names may be qualified (
io.systemd.Hostname.Describe) or, when the service implements exactly one interface with that method, bare (Describe).
Global flags: --config, --timeout (default 10s), --allow-writes,
-o/--output text|json, --no-color, --max-frame-size.
Exit codes: 0 success; 1 generic failure; 2 usage error; 3 the
service returned a Varlink error; 4 refused by the safety policy; 5 service
not found or not reachable; 130 interrupted.
Output: human output goes to stdout, diagnostics to stderr. -o json
output is stable and documented. Colour only when stdout is a TTY.
6. Safety model¶
vlx talks directly to privileged system services, often as root. A slip of a key must not reboot a machine, wipe a disk or extend a TPM PCR.
6.1 Classification¶
Every method is classified as one of:
| Class | Meaning | Examples |
|---|---|---|
READ |
No side effects expected. | Get*, List*, Describe, Resolve*, Subscribe*, Dump*, Can*, Ping |
WRITE |
Changes state. Also the default for anything unknown. | SetLogLevel, Unit.SetProperties, Credentials.Decrypt |
DANGER |
Hard to undo, disruptive or security-sensitive. | Manager.Reexecute, Machine.Kill, PCRExtend.Extend, Repart.Run, MachineImage.Remove |
Classification is decided in this order, first match wins:
- User overrides in config (
safety.read,safety.write,safety.danger: lists of glob patterns over qualified method names). - A built-in table covering every
io.systemd.*method in systemd v257–v260 andorg.varlink.service. - Name heuristics: a danger verb (
Kill,Terminate,Remove,Delete,Reset,Reboot,PowerOff,Wipe,Format, …) →DANGER; a read verb (Get,List,Describe,Resolve,Query,Lookup,Dump,Watch,Subscribe,Monitor,Can,Is,Has,Ping, …) →READ. - Otherwise
WRITE— unknown is never assumed safe.
oneway calls are at least WRITE, since nothing can be observed of them.
6.2 Policy¶
- Default mode is read-only.
WRITEandDANGERmethods can be browsed and their parameters prepared and validated, but sending them is refused with a message that explains how to enable writes. --allow-writes(orallow_writes: truein config, orVLX_ALLOW_WRITES=1) enables writes for that session. The header badge turns amber.- With writes enabled, a
WRITEcall shows a confirmation dialog with the method, socket and parameters;DANGERadditionally requires typing the method name. vlx callnon-interactive:WRITEneeds--allow-writes; it then prompts on a TTY and requires--yesotherwise.DANGERneeds--allow-writesand either a typed confirmation on a TTY or--yes. Refusals exit with code4.
6.3 Resource limits¶
| Limit | Default | Why |
|---|---|---|
Call timeout (--timeout) |
10 s | A hung service must not hang vlx. Streams are exempt but cancellable. |
| Introspection timeout | 5 s per socket | Discovery stays responsive with stuck services. |
| Max frame size | 16 MiB | Bounded memory per message (sdvarlink WithMaxFrameSize). |
| Max stream replies (TUI) | 1000 kept | The result drops the oldest and says so. |
| Received file descriptors | closed immediately | Never leak descriptors; their count is reported. |
6.4 Terminal and data hygiene¶
- Escape-sequence injection: every string that comes from a service — IDL doc comments, interface names, JSON values, error names — is stripped of control characters before it reaches the terminal, so a malicious or buggy service cannot rewrite the screen, set the window title or abuse OSC 52.
- Redaction: fields whose names match
redactpatterns (default:*password*,*secret*,*privateKey*,hashedPassword,privileged,*token*) are shown as«redacted»in the TUI.Rreveals them for the current response.vlx calloutput is not redacted (it is for scripts), but vlx never writes responses anywhere other than stdout. - No persistence: vlx keeps no history and writes no files except
vlx config init.
6.5 Attack surface¶
- Unix sockets only. No
exec:/ssh:transports, no shelling out, no plugins. - Discovery is read-only: it lists a fixed set of directories one level deep
(plus configured
search_paths), does not follow symlinked directories and only reports sockets.vlx listnever connects unless--probeis given. In the explorer, preview and the command line introspect services with the read-onlyorg.varlink.servicemethods only;preview: falseturns that off so nothing is sent until a service is opened. - The config file can enable writes, so vlx refuses to load a config file that is group- or world-writable or not owned by the current user or root.
- vlx never needs and never requests elevated privileges. Running as root is
allowed but flagged in the header and in
vlx calldiagnostics.
7. Discovery¶
Default search locations (each listed one level deep, sockets only):
/run/varlink/registry # systemd ≥ 258 registry of public services
/run/systemd # io.systemd.* and friends
/run/systemd/{resolve,userdb,machine,netif,journal,oom,report}
/run/udev
$XDG_RUNTIME_DIR/systemd # per-user services
$XDG_RUNTIME_DIR/vlx # the vlx demo service
plus search_paths from config and --search flags. Results are de-duplicated
by resolved path and sorted by name. A socket is reported with its path, base
name and permissions (so EACCES is predictable before connecting).
8. Configuration¶
Viper, layered (highest wins): flags → environment (VLX_*, - → _) →
config file → defaults. The file is
$XDG_CONFIG_HOME/vlx/config.yaml (~/.config/vlx/config.yaml), overridable
with --config.
# ~/.config/vlx/config.yaml
allow_writes: false
timeout: 10s
max_frame_size: 16777216
max_replies: 1000
preview: true
search_paths: []
redact: ["*password*", "*secret*", "*privateKey*", "hashedPassword", "privileged", "*token*"]
safety:
read: [] # globs, e.g. "com.example.Thing.Peek*"
write: []
danger: []
9. Architecture¶
cmd/vlx/main.go entry point; exit-code mapping
internal/cli/ one Cobra command per file
internal/config/ Viper setup, defaults, file permission checks
internal/discovery/ socket discovery and SERVICE resolution
internal/client/ sdvarlink wrapper: introspect, call, stream
internal/schema/ IDL → JSON skeleton; JSON → IDL validation
internal/safety/ classification and policy
internal/sanitize/ control-character stripping
internal/render/ JSON and IDL highlighting
internal/tui/ Bubble Tea model: pages, command line, styles, keys
internal/cmdline/ SERVICE METHOD NAME=VALUE grammar shared with vlx call
internal/demo/ built-in demo Varlink service
internal/version/ build metadata
Dependencies flow one way: cli → tui → client/schema/safety/render →
sdvarlink. Only client and demo touch sockets. All network work in the
TUI runs in tea.Cmds with contexts, so the UI never blocks.
10. Quality¶
- Unit tests for discovery, resolution, schema skeletons and validation, safety classification (including a test that every built-in systemd method has an explicit class), sanitisation and rendering.
- End-to-end tests run the CLI against the demo service over a real socket — no systemd needed, so they run on macOS too.
- TUI tests drive the Bubble Tea model with messages and assert on state and rendered views.
- CI: tests with
-raceon Linux amd64/arm64 and macOS, golangci-lint, GoReleaser snapshot on every push.
11. Distribution¶
GoReleaser builds static (CGO_ENABLED=0) binaries for linux
amd64/arm64/armv7/riscv64/ppc64le/s390x and darwin amd64/arm64, with
tar.gz archives that include shell completions and man pages, .deb,
.rpm and .apk packages, and SHA-256 checksums. Tags v* publish a GitHub
release; every branch push builds a snapshot artifact. Docs are MkDocs
Material, published to GitHub Pages.
12. Future work¶
ssh:transport behind an explicit flag, mirroringvarlinkctl --host.- Sending file descriptors from files the user picks.
- A request "scratchpad" with saved (opt-in, redacted) calls.
- Bubble Tea v2 migration.
- Recording sessions as
varlinktestgolden files for sdvarlink tests.