Documentation
¶
Overview ¶
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).
Index ¶
- Constants
- func ClearCredentials(ryshDir string) (bool, error)
- func CredentialsPath(ryshDir string) string
- type Credentials
- 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) 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)
- 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 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 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 (*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 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) 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.
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.