Documentation
¶
Overview ¶
Answering a gated tool from a non-TUI client (E-12b).
A tool call awaiting approval blocks in the orchestrator's waitForApproval until a MsgApprovalResponse arrives on pane.<paneid>.approval.response. The `approval_response` ws command (server.go) is the only thing that publishes one on behalf of the desktop UI, the browser UI and the phone.
client → server: approval_response {pane_id,request_id,decision,reason}
server → client: approval_error {pane_id,request_id,error}
The error direction exists because the phone has no console: an approval that is silently dropped looks exactly like one that is merely slow, while the tool blocks for five minutes and is then rejected.
The agents board's READ path for clients that are not the terminal UI (design 025 §6, design 028, design 027 wave 1).
WHY THIS FILE HAD TO EXIST. agents-board is a PaneType, and a pane of that type never starts a shell (domain.IsShelllessPaneType) — so nothing ever writes into its output buffer and there is nothing on the wire for a renderer to draw. The TUI does not notice, because it BUILDS the board itself: buildPanePanel dispatches the pane type to buildBoardPanel, which reads a board.Store the TUI subscribes to directly. Every other client got the pane's buffer, which for a shell-less pane holds whatever happened to be there — observed live in the desktop app as a board pane showing a stale `##pane list --meta` dump from a command that had errored against it, while four agents were posting to that board and their posts arrived fine.
That is the worst shape a monitoring view can fail in: not blank, not an error, but PLAUSIBLE CONTENT that has nothing to do with the board. The operator's report was "I cannot see that the agent board is working" while the board was in fact working end to end.
THE READER ASKS THE RECORDER. This does not subscribe, and it does not open the board's KV bucket — it puts a board.Query to ABLA over NATS, exactly as `rysh board tail` does. See msg.BoardQuerySubject for the two reasons (design 026 §5.4, and F-23, where a second call site derived the bucket name itself and read an empty board that looked healthy). A web server that grew its own subscription would be a third copy of the board in a process that is not the recorder, and would go stale exactly when the daemon is busiest.
PULL, NOT PUSH, and that is a deliberate limit rather than an oversight. The board has no per-board change notification the server could forward, and adding a subscription here to synthesise one would be the third copy above. The client polls (AgentsBoardView) and the cost is bounded by the client count, not by the post rate.
Package web provides an embedded web server that mirrors the rysh TUI in a browser. It connects to the same NATS bus as the TUI, polls workspace snapshots, and pushes them to WebSocket clients. Commands from the browser are published back to NATS so the actor hierarchy processes them identically.
Parity API surface (web_electron_roadmap Phase 3, tasks W7–W10).
The Electron desktop app gets a handful of conveniences from its main process (`window.electronAPI`): env/platform info, workspace reads, voice transcription and shell tab-completion. In web mode there is no Electron main process — but the daemon this server runs inside owns the same data and the same Go engines, so each capability is served here instead:
W9 GET /api/env → is_web flag + platform + session + capability map
W8 GET /api/workspaces → current workspace + on-disk session registry
W10 GET /api/voice/config → public voice config (never the API key)
W10 POST /api/voice/transcribe→ audio body → transcript (internal/voice)
W7 ws completion_get → completion_result (internal/tui engine),
replied ONLY to the requesting client
All /api/* routes sit behind the same access-token middleware as the rest of the server (auth.go); nothing here widens the surface. Everything is read-only except transcription, which only calls the configured provider.
Control-dashboard surface (WS5, design 005 — tasks DB1–DB4).
This file adds the browser control plane on top of the read-only viewer in server.go:
- DB1 channel operations → MsgHumanoidChannelStart/Stop on humanoid.{name}.inbox (same messages ##humanoid channel start/stop send)
- DB2 pairing operations → MsgChannelPairApprove / MsgChannelAllow / MsgChannelPairList on humanoid.{name}.pairing (same subject the terminal ##humanoid pair commands ride, WS3 / design 003 §4.4), plus a live relay of MsgChannelPairRequest/QR/Status/ListReply into the WS hub
- DB3 humanoid operations → MsgHumanoidActivate/Deactivate on the registry inbox and MsgHumanoidSetGovernance / MsgHumanoidSetReplyMode on humanoid.{name}.inbox
- DB4 control gating → every MUTATING endpoint/action requires control mode; read/list endpoints stay available. Control mode is enabled by the RYSH_WEB_CONTROL environment variable (1/true/yes/on) read in NewServer — chosen as the least-invasive wiring so the two existing WorkspaceActor NewServer call sites need no changes — or programmatically via SetControl (tests, future `rysh web --control`). When control is enabled the HTTP listener is forced onto 127.0.0.1.
Terminal and web drive the SAME typed messages on the SAME subjects, so the two fronts cannot diverge (design 003 §4.4 / 005 G5).
Server-side embedded web panes for browser mode (web_electron_roadmap W12).
In the Electron app a web pane is a native WebContentsView overlaid on the pane. A plain browser tab cannot embed arbitrary third-party pages (X-Frame-Options / CSP) nor attach a debugger — so in web mode the server drives a HEADLESS Chromium (internal/cdp, the same engine behind rysh's browser automation) and streams JPEG frames + url/title to the pane over the existing /ws protocol:
client → server: webpane_open {pane_id,url,profile} · webpane_navigate
{pane_id,url} · webpane_back/forward/reload/close {pane_id}
server → client: webpane_frame {pane_id,url,title,screenshot} (~1 fps)
webpane_error {pane_id,error} (fail visible)
Availability is advertised in /api/env capabilities.web_pane (needs a workspace root for the profile dir and a Chromium binary). When it is false, the renderer keeps its explicit "requires the desktop app" note — the capability degrades visibly, never silently.
Profiles: each rysh web profile maps to its own persistent user-data-dir under <workspace>/.rysh/browser-instances/webpane/<profile> — deliberately NOT the interactive-login profile dir (Chromium locks user-data-dirs, and a headed login browser may hold that lock).
Coordinate input forwarding for server-side web panes (E7 T1 / E-16).
The client renders webpane_frame's JPEG scaled to whatever size the pane happens to be, so a pointer event arrives in DISPLAY space while the headless page only understands its own CSS-pixel SOURCE space. Forwarding a display coordinate unmapped is the entire bug class here: a 1280x900 page shown at 640x450 puts every click at half the intended point.
client → server: webpane_input {pane_id,kind,x,y,display_width,
display_height,button,key,modifiers,delta_x,delta_y}
server → client: webpane_error {pane_id,error} (fail visible, never a
silent no-op — see TestWebPaneUnavailableIsVisible)
x/y are relative to the top-left of the rendered FRAME IMAGE (not the pane), and display_width/display_height are that image's rendered size. The client therefore never scales coordinates itself; it reports what it drew and the server does the mapping against the source dimensions it published on the frame.
Index ¶
- Constants
- func ClearCredentials(ryshDir string) (bool, error)
- func ClearCredentialsFor(ref CredentialsRef) (bool, error)
- func CredentialsPath(ryshDir string) string
- type Credentials
- type CredentialsRef
- type Hub
- type Server
- func (s *Server) ControlEnabled() bool
- func (s *Server) Host() string
- func (s *Server) IsRunning() bool
- func (s *Server) LoginEnabled() bool
- func (s *Server) LoginUsername() string
- func (s *Server) Port() int
- func (s *Server) SetBashCompletion(enabled bool)
- func (s *Server) SetClientCountListener(fn func(n int))
- func (s *Server) SetControl(enabled bool)
- func (s *Server) SetCredentials(c *Credentials)
- func (s *Server) SetFSBrowser(fn func(op, paneID, path string, offset, length int64) (any, string, string))
- func (s *Server) SetGuestOf(fn func() any)
- func (s *Server) SetHost(host string)
- func (s *Server) SetShared(host string, port int)
- func (s *Server) SetVoice(v VoiceSettings)
- func (s *Server) SetWorkspaceInfo(path, name string)
- func (s *Server) SetWorkspaceLister(fn func() []WorkspaceEntry)
- func (s *Server) SharedAddr() (string, int)
- func (s *Server) Start() error
- func (s *Server) StartShared(host string, port int) error
- func (s *Server) Stop() error
- func (s *Server) StopShared() error
- func (s *Server) TrackCredentialsFile(ryshDir string)
- func (s *Server) TrackCredentialsRef(ref CredentialsRef)
- type VoiceSettings
- type WorkspaceEntry
Constants ¶
const AccessTokenTTL = 30 * 24 * time.Hour
AccessTokenTTL is how long a successful login stays valid: one month.
const DefaultHost = "127.0.0.1"
DefaultHost is the bind address used when none is configured: loopback, so a rysh web session is reachable only from the machine running the daemon unless the operator explicitly opts into a wider bind (`--bind 0.0.0.0`, web host, RYSH_WEB_HOST). The login is a guard, not a perimeter — binding the default surface to a LAN-visible interface is a decision the operator must make deliberately.
Variables ¶
This section is empty.
Functions ¶
func ClearCredentials ¶ added in v0.2.3
ClearCredentials removes the credentials file, disabling username/password auth. Reports whether a file was actually there to remove.
func ClearCredentialsFor ¶ added in v0.2.6
func ClearCredentialsFor(ref CredentialsRef) (bool, error)
ClearCredentialsFor removes the ref's own credentials file. The project login is left alone: clearing a session's login means "stop having your own", not "unlock every other session in this project".
func CredentialsPath ¶ added in v0.2.3
CredentialsPath returns the credentials file for a rysh dir.
Types ¶
type Credentials ¶ added in v0.2.3
type Credentials struct {
Username string `json:"username"`
PasswordHash string `json:"password_hash"`
// Secret is the base64url HS256 signing key for issued access tokens.
Secret string `json:"secret"`
UpdatedAt string `json:"updated_at"`
}
Credentials is the on-disk shape of web-auth.json.
func LoadCredentials ¶ added in v0.2.3
func LoadCredentials(ryshDir string) (*Credentials, error)
LoadCredentials reads the credentials for a rysh dir. A missing file is not an error — it means username/password auth is simply not configured, and (nil, nil) is returned. A file that exists but is unusable IS an error: a corrupt credentials file must not silently downgrade to "no auth".
func LoadCredentialsFor ¶ added in v0.2.6
func LoadCredentialsFor(ref CredentialsRef) (*Credentials, error)
LoadCredentialsFor reads the credentials a ref resolves to (its own file, or the project one). Same contract as LoadCredentials.
func SaveCredentials ¶ added in v0.2.3
func SaveCredentials(ryshDir, username, password string) (*Credentials, error)
SaveCredentials hashes password, mints a fresh signing key, and writes the credentials file (0600 in a 0700 dir). Returns what was written.
func SaveCredentialsFor ¶ added in v0.2.6
func SaveCredentialsFor(ref CredentialsRef, username, password string) (*Credentials, error)
SaveCredentialsFor writes credentials to the ref's OWN file — a session that sets a login gets its own from that moment on, and stops following the project's.
func (*Credentials) SigningKey ¶ added in v0.2.3
func (c *Credentials) SigningKey() []byte
SigningKey decodes the HS256 key used to sign and verify access tokens. Returns nil when the stored key is unusable — parseJWT rejects an empty key, so a broken file fails every token rather than accepting every token.
func (*Credentials) Verify ¶ added in v0.2.3
func (c *Credentials) Verify(username, password string) bool
Verify reports whether a login attempt matches. bcrypt runs even when the username is wrong so a bad username and a bad password cost the same time, and neither answer is distinguishable from the outside.
type CredentialsRef ¶ added in v0.2.6
type CredentialsRef struct {
RyshDir string
// Session names the session whose login is meant. Empty means the
// project-wide file — which is what the fallback resolves to anyway.
Session string
}
CredentialsRef says WHICH login a caller means: a named session's, or the project-wide one.
Logins started out per project, on the reasoning that rysh state is project-local and sibling daemons rooted at one project should share a password (that sharing is what F-9 is about — auth.go). But a session is what gets served: two sessions in one project can be two different doors, on two ports, published at two URLs, shown to two different sets of people. Making them share one password meant the second session to set one silently changed the first session's, and logged its browsers out.
So a session's login lives at <ryshDir>/web/<session>-auth.json, beside its web settings. Reads fall back to the project file when a session has none, so every existing install keeps working and a session inherits the project login until it sets its own.
func ProjectCredentials ¶ added in v0.2.6
func ProjectCredentials(ryshDir string) CredentialsRef
ProjectCredentials refers to the project-wide login.
func SessionCredentials ¶ added in v0.2.6
func SessionCredentials(ryshDir, session string) CredentialsRef
SessionCredentials refers to one session's login.
func (CredentialsRef) HasOwn ¶ added in v0.2.6
func (r CredentialsRef) HasOwn() bool
HasOwn reports whether this ref has credentials of its own, as opposed to inheriting the project's. The difference matters: an inherited login is what a session serves only while it has nothing to say about the matter.
func (CredentialsRef) OwnPath ¶ added in v0.2.6
func (r CredentialsRef) OwnPath() string
OwnPath is where this ref's credentials are WRITTEN: the session's own file, or the project file when no session is named.
func (CredentialsRef) Path ¶ added in v0.2.6
func (r CredentialsRef) Path() string
Path is where this ref's credentials are READ from: its own file when that exists, else the project-wide one. The fallback is what lets a session inherit the project login until it sets one of its own.
type Hub ¶
type Hub struct {
// contains filtered or unexported fields
}
Hub maintains the set of active WebSocket clients and broadcasts messages to all of them. Modelled after the Gorilla WebSocket chat example.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the web UI server. It serves static assets over HTTP and maintains WebSocket connections to push real-time workspace snapshots to browsers.
func NewServer ¶
func NewServer(port int, sessionName string, pub *msg.NATSPublisher, nc *nats.Conn, codecs *msg.CodecRegistry) *Server
NewServer creates a new web UI server.
func (*Server) ControlEnabled ¶
ControlEnabled reports whether control mode is enabled.
func (*Server) LoginEnabled ¶ added in v0.2.3
LoginEnabled reports whether a username/password login is configured.
func (*Server) LoginUsername ¶ added in v0.2.3
LoginUsername returns the configured username, or "" when login is off.
func (*Server) SetBashCompletion ¶ added in v0.2.3
SetBashCompletion enables delegation to bash's programmable completion specs for argument-position tab-completion (config [ui] shell_bash_completion). Built-in command/path completion is always on.
func (*Server) SetClientCountListener ¶
SetClientCountListener registers fn to receive the live WebSocket-client count on every transition. Must be called before Start.
func (*Server) SetControl ¶
SetControl enables or disables control mode. Must be called before Start: the listen address is computed at Start time and mutating endpoints consult the flag per-request under the server mutex.
func (*Server) SetCredentials ¶ added in v0.2.3
func (s *Server) SetCredentials(c *Credentials)
SetCredentials installs (or, with nil, removes) the username/password login. Safe to call while the server is running: `##rysh web auth` applies new credentials to a live server, and because saving credentials mints a fresh signing key, every token issued under the old password stops verifying.
func (*Server) SetFSBrowser ¶
func (s *Server) SetFSBrowser(fn func(op, paneID, path string, offset, length int64) (any, string, string))
SetFSBrowser injects the file-browse implementation. Call before Start.
func (*Server) SetGuestOf ¶ added in v0.2.11
SetGuestOf wires /health's guest_of (design 028 §9 limit 7). /health is public, so fn should return the public view (bus.GuestOf.Public): no host path. Call it before Start.
func (*Server) SetHost ¶
SetHost pins the HTTP listener to a specific bind IP — 0.0.0.0 to expose the UI on every interface, a dedicated loopback alias like 127.0.100.1 fronted by a reverse proxy, and so on. Empty selects DefaultHost (loopback). Must be called before Start.
func (*Server) SetShared ¶ added in v0.2.3
SetShared configures the shared door to be opened at Start. Host empty means DefaultHost. Use StartShared to open one on a server already running.
func (*Server) SetVoice ¶ added in v0.2.3
func (s *Server) SetVoice(v VoiceSettings)
SetVoice injects the daemon's voice configuration. Call before Start.
func (*Server) SetWorkspaceInfo ¶ added in v0.2.3
SetWorkspaceInfo records the workspace this session runs in (working directory + display name), served by /api/env and /api/workspaces and used as the root for server-side web-pane browser profiles (W12).
func (*Server) SetWorkspaceLister ¶ added in v0.2.3
func (s *Server) SetWorkspaceLister(fn func() []WorkspaceEntry)
SetWorkspaceLister injects the recent-workspace source (the on-disk session registry, read by the actors package — this package cannot import it). nil ⇒ /api/workspaces returns only the current workspace.
func (*Server) SharedAddr ¶ added in v0.2.3
SharedAddr returns the shared door's host and port, or ("", 0) when closed.
func (*Server) StartShared ¶ added in v0.2.3
StartShared opens the shared door on host:port. The server must be running, and credentials must be configured: the whole point of this listener is that it demands them, so opening one that could not check a password would serve the session to anyone who found the port.
Opening a second shared door closes the first — one shared address at a time, so there is never an old port still serving after a rebind.
func (*Server) StopShared ¶ added in v0.2.3
StopShared closes the shared door, leaving the private one (and every client on it) untouched. A no-op when none is open.
func (*Server) TrackCredentialsFile ¶ added in v0.2.4
TrackCredentialsFile tells the server which workspace rysh dir holds its web-auth.json, making that FILE the source of truth rather than whatever snapshot was installed at start.
This exists because credentials are per-workspace state shared by every daemon rooted there, while `creds` is per-process. A second daemon running `##rysh web auth` rewrites the file and mints a fresh signing key — correctly, that is how changing the password logs old browsers out — and this process, still verifying against the superseded key, then rejects every client with a 401 that no user can diagnose or fix (F-9). Restarting the listener "fixed" it precisely because a restart is the only thing that re-read the file.
Not a watcher: a stat per auth decision costs a syscall, needs no goroutine and no lifecycle, and cannot leak a watch when the server stops.
func (*Server) TrackCredentialsRef ¶ added in v0.2.6
func (s *Server) TrackCredentialsRef(ref CredentialsRef)
TrackCredentialsRef is TrackCredentialsFile for a session-scoped login: the server follows that session's own file, falling back to the project's while the session has none. The ref (not a resolved path) is what is held, so a session that sets its own login mid-run is picked up on the next auth decision rather than at the next restart.
type VoiceSettings ¶ added in v0.2.3
type VoiceSettings struct {
Enabled bool
Provider string // "deepgram" (default) | "whisper"
APIKey string
Hotkey string // e.g. "ctrl+r"
Language string // optional BCP-47 hint
}
VoiceSettings is the web server's view of the daemon's voice configuration (voice + [voice_control] in rysh.config.yaml). Injected by the workspace actor via SetVoice before Start. The APIKey never leaves the server.
type WorkspaceEntry ¶ added in v0.2.3
type WorkspaceEntry struct {
Path string `json:"path"`
Name string `json:"name"`
LastOpened string `json:"last_opened,omitempty"`
LastSession string `json:"last_session,omitempty"`
}
WorkspaceEntry is one workspace visible to the web UI: the current one, or a recent one recorded in the on-disk session registry.