Help us shape the future of Go! Take the 2026 Go Developer Survey

web

package
v0.2.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 15, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

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

View Source
const AccessTokenTTL = 30 * 24 * time.Hour

AccessTokenTTL is how long a successful login stays valid: one month.

View Source
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

func ClearCredentials(ryshDir string) (bool, error)

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

func CredentialsPath(ryshDir string) string

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

func (s *Server) ControlEnabled() bool

ControlEnabled reports whether control mode is enabled.

func (*Server) Host

func (s *Server) Host() string

Host returns the configured bind IP ("" = default).

func (*Server) IsRunning

func (s *Server) IsRunning() bool

IsRunning reports whether the server is currently running.

func (*Server) LoginEnabled added in v0.2.3

func (s *Server) LoginEnabled() bool

LoginEnabled reports whether a username/password login is configured.

func (*Server) LoginUsername added in v0.2.3

func (s *Server) LoginUsername() string

LoginUsername returns the configured username, or "" when login is off.

func (*Server) Port

func (s *Server) Port() int

Port returns the configured port number.

func (*Server) SetBashCompletion added in v0.2.3

func (s *Server) SetBashCompletion(enabled bool)

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

func (s *Server) SetClientCountListener(fn func(n int))

SetClientCountListener registers fn to receive the live WebSocket-client count on every transition. Must be called before Start.

func (*Server) SetControl

func (s *Server) SetControl(enabled bool)

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

func (s *Server) SetHost(host string)

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

func (s *Server) SetShared(host string, port int)

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

func (s *Server) SetWorkspaceInfo(path, name string)

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

func (s *Server) SharedAddr() (string, int)

SharedAddr returns the shared door's host and port, or ("", 0) when closed.

func (*Server) Start

func (s *Server) Start() error

Start launches the web server in background goroutines.

func (*Server) StartShared added in v0.2.3

func (s *Server) StartShared(host string, port int) error

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) Stop

func (s *Server) Stop() error

Stop gracefully shuts down the web server.

func (*Server) StopShared added in v0.2.3

func (s *Server) StopShared() error

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

func (s *Server) TrackCredentialsFile(ryshDir string)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL