Rio
adr/0030-always-a-channel-client.mdRaw Markdown · GitHub ↗

ADR-0030: The frontend is always a client over a channel

  • Status: Accepted; amended by ADR-0096
  • Date: 2026-06-30
  • Deciders: jka
  • Decision: D30

Context

After ADR-0029 the GUI had two transports: embedding the core, and a TCP socket. Two paths for one job is redundancy, and the socket path had a security problem: the agent was about to gain write and command-execution powers, and a loopback socket without authentication is reachable by every user on a shared host. SSH protects the network hop, not the loopback endpoints at either end.

Decision

A frontend never embeds the core. It is always a client to a core at the far end of a channel, and one client code path serves every channel kind:

  • Default: a pipe to a spawned child core (tclsh rio-core/server.tcl --stdio). Requests go to its standard input; replies and events come from its standard output. No listening socket exists, so process ownership is the access control. Closing the channel ends the child.
  • --connect host:port: a TCP socket to a listening core, for a persistent daemon serving one or more frontends. It binds loopback by default.

The agent runs in the core. Provider selection, keys and policy are operations (agent.provider.set, agent.key.set, agent.status, …), providers register in a named registry in rio::agent, and agent events are ordinary broadcast traffic. API keys live in the core's secret store wherever the core runs.

Operational rules that followed:

  • The first operation on any channel is session.hello, bounded at 8 seconds, so a dead tunnel that accepts connections but never answers produces an error instead of a blank window.
  • On attach, the GUI reads the core's agent state and mirrors it; it writes state only on an explicit user action, so it never resets a daemon another client configured.
  • File ▸ Connect to Remote Core… rewires the running window in place (open the new connection first, save-check open tabs, then swap), or opens a second window.
  • With a remote core, the Open, Save As and Open Folder dialogs browse the core's filesystem over fs.list.

Alternatives considered

  • Unix domain sockets. Core Tcl (8.6 through 9.0) supports only TCP; AF_UNIX needs a compiled extension.
  • Loopback TCP with a capability token. Workable, but adds an authentication protocol and token distribution.
  • A pipe to a child needs no socket, no authentication and no cryptography, and uses only the standard library.

Consequences

  • Local and remote use are the same code, and the default configuration has no network surface.
  • Every frontend-to-core call serialises, including locally.
  • Each window has its own core unless it connects to a daemon.
  • A headless core that runs the agent needs tcltls; the server deploy script installs it.
  • The planned --ssh convenience wrapper (phase P4) was refused in ADR-0096. The remote path is --connect to whatever endpoint the operator has made reachable.