ui

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: MIT Imports: 84 Imported by: 0

Documentation

Overview

Package ui implements a local web dashboard for monitoring and controlling cloop.

Index

Constants

View Source
const AgentUpgradeCommand = "cloop executor agent install --upgrade"

AgentUpgradeCommand is the procedure that actually upgrades a device.

It is a constant because it used to be a lie in three places. The Executors panel told operators to run `cloop executor agent install --upgrade`, and cmd/executor_install_cmd.go defined no such flag — so the one command the UI offered as a remedy failed with an unknown-flag error. The flag now exists (see install.Upgrade), and naming it from one place means the string and the flag cannot drift apart again without the drift test noticing.

View Source
const ReproduceResultMarker = "CLOOP_REPRODUCE_RESULT "

ReproduceResultMarker prefixes the machine-readable line the in-sandbox half prints so this side can recover the test outcome.

A marker on stdout rather than a file in the workspace: the file would be committed by the write-back and show up as a spurious difference in the diff-of-diffs — the reproduction would change its own result. Stdout is already streamed home by every driver, including the remote one.

Variables

View Source
var ErrNoIsolatingExecutor = errors.New(
	"reproduce needs an isolating executor (container, VM, or remote agent) and this project is bound to one that " +
		"shares the host filesystem — a reproduction there would run against the working tree instead of a clean " +
		"checkout, so it is refused; configure a container or Kubernetes executor, or enrol a remote agent")

ErrNoIsolatingExecutor is returned when no executor can host a reproduction.

Its message names the remediation because this is the error an operator hits on a hub that has never been configured for isolation, and "no isolating executor" alone does not tell them what to do about it.

Functions

func NewReproduceRunner

func NewReproduceRunner(exe string) taskreplay.SandboxRunner

NewReproduceRunner returns a taskreplay.SandboxRunner backed by the hub's executor fleet. exe is the cloop binary to invoke inside the sandbox.

func TokenStatus

func TokenStatus(t *apitoken.Token, now time.Time) string

TokenStatus renders a token's lifecycle state. Exported so `cloop hub token list` prints the same word the panel does, from the same rules.

Types

type ChatMessage

type ChatMessage struct {
	Role      string    `json:"role"` // "user" or "assistant"
	Content   string    `json:"content"`
	Timestamp time.Time `json:"timestamp"`
	Action    string    `json:"action,omitempty"` // resolved cloop command, if any
}

ChatMessage is a single turn in the chat conversation history.

type Server

type Server struct {
	WorkDir  string
	Port     int
	Token    string   // optional auth token; empty = no auth
	Projects []string // extra project directories for multi-project dashboard

	// RPS and Burst control the per-IP token-bucket rate limiter.
	// Zero values use 20 req/s and burst 50.
	RPS   float64
	Burst int

	// MaxWebSocketConns caps the total number of concurrent WebSocket
	// connections accepted across every remote IP. Zero substitutes
	// config.WebSocketConnsDefault (256). Each accepted upgrade spawns
	// at least three goroutines (handler, drain, ping ticker) plus a
	// hubClient registry entry; without a cap a flood of upgrades can
	// exhaust scheduler resources and the per-project broadcast budget.
	MaxWebSocketConns int

	// MaxWebSocketConnsPerIP caps the number of concurrent WebSocket
	// connections any single remote IP may hold. Zero substitutes
	// config.WebSocketConnsPerIPDefault (8). Reaching the cap causes
	// new upgrades from that IP to be rejected with HTTP 429 and a
	// Retry-After header before nhooyr.Accept hijacks the response.
	MaxWebSocketConnsPerIP int

	// MaxRequestBodyBytes caps any incoming POST/PUT/PATCH request body
	// the server will accept. Zero substitutes maxJSONBodyBytes (10 MiB).
	// Oversize requests are rejected with HTTP 413 (Request Entity Too
	// Large). Set via config.UIConfig.MaxRequestBodyBytes; bounded by
	// config.MaxRequestBodyBytesLower / Upper. Task 20102.
	MaxRequestBodyBytes int64

	// AllowedWSOrigins lists extra Origin hosts (host or host:port) that may
	// open a WebSocket, in addition to the always-allowed loopback origins
	// and same-origin requests. Needed only when the dashboard sits behind a
	// reverse proxy that rewrites Host so same-origin detection can't see the
	// public hostname. Populated from config.UIConfig.AllowedWSOrigins.
	AllowedWSOrigins []string

	// AllowedOrigins is the deployment-wide origin allowlist: it applies to
	// the dashboard socket AND to the executor-agent endpoint. Populated from
	// config.UIConfig.AllowedOrigins.
	//
	// It is separate from AllowedWSOrigins because the two have different
	// blast radii. An entry here can open an agent connection; an entry in
	// AllowedWSOrigins cannot. Merging them would silently promote every
	// dashboard-scoped origin to the agent endpoint.
	AllowedOrigins []string

	// ExternalURL is what this deployment calls itself, e.g.
	// https://cloop.example.com. Its host is an accepted WebSocket Origin for
	// both the dashboard and the executor-agent endpoint. Populated from
	// config.UIConfig.ExternalURL.
	ExternalURL string

	// TLSCertFile / TLSKeyFile enable native HTTPS. Both must be set or
	// neither; Run refuses to start on a half-configuration rather than
	// silently falling back to plaintext. TLSMinVersion is "1.2" (default)
	// or "1.3". Populated from config.UIConfig.TLS or --tls-cert/--tls-key.
	TLSCertFile   string
	TLSKeyFile    string
	TLSMinVersion string

	// SelfExe is the cloop binary this server hands to an executor when a
	// handler needs to run a subcommand (`init`, `run`, `reset`, `do`,
	// `listen`, `suggest`). Empty means os.Executable(), which is correct in
	// production and wrong under `go test`: there os.Executable() is the
	// compiled test binary, so a handler that spawns it re-runs the package's
	// own test suite as a child process with CLI-looking argv. That child
	// passes and is never reported, so the only visible symptom was pkg/ui
	// taking ten minutes — long enough to trip the default per-package test
	// timeout and fail CI. Tests point this at a stub.
	SelfExe string

	// Lease fences this process as the sole control plane for its state.db
	// (Task 20214). Set by `cloop ui` before the server is constructed, so
	// nothing in New — which already reconciles executors and sweeps orphaned
	// sessions — can run against a control plane another hub owns.
	//
	// nil means unfenced, which is what every in-process test wants: each one
	// gets its own temp directory, so there is nothing to fence against, and
	// requiring a lease would make the lifecycle harder to drive without
	// testing anything. Run starts the heartbeat, watches for the lease being
	// lost, and Shutdown releases it; every one of those is nil-safe.
	Lease *hublease.Lease

	// ReadyCheck overrides the readiness check used by /readyz. nil
	// means use defaultReadyCheck (stat state.db, open it, run SELECT 1
	// bounded by ctx). Tests use this field to simulate degraded states
	// like "closed db handle" or "state store not initialized".
	ReadyCheck func(ctx context.Context) error

	// Log is the structured logger used for lifecycle / error messages
	// emitted by the UI server itself (panics in HTTP middleware, client
	// JavaScript errors POSTed to /api/client-error, watcher failures).
	// Nil means the package picks a sensible default at first use (text
	// output to stdout, project bound).
	Log logger.Logger

	// OIDC is the optional OpenID Connect authenticator (Task 20152). Nil
	// (the default) means OIDC is disabled and the dashboard behaves
	// exactly as before: token auth if Token is set, otherwise open. Set
	// from ui.oidc.* in .cloop/config.yaml by cmd/ui_cmd.go; when active it
	// gates every route behind an IdP session and scopes the multi-project
	// registry per user (see pkg/ui/oidc.go).
	OIDC *oidcauth.Authenticator

	// Authz resolves OIDC claims to roles and permissions (Task 20164).
	// Nil means "no role mappings configured": every session identity then
	// falls back to the resolver's deny-by-default, which is why
	// authzActive() also requires OIDC to be enabled. With OIDC disabled
	// this field is ignored entirely and every request is granted
	// everything, preserving single-tenant local behavior.
	Authz *authz.Resolver
	// contains filtered or unexported fields
}

Server is the cloop web dashboard HTTP server.

func New

func New(workdir string, port int, token string) *Server

New creates a new UI server for the given working directory and port. token is optional; if non-empty every API request must supply it via "Authorization: Bearer <token>" header or "?token=<token>" query param.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the HTTP handler for the server with all routes registered and security/auth middleware applied. It does NOT start background goroutines (watchState, watchProjects); call Start() for the full lifecycle. This method exists primarily to support httptest-based unit tests.

func (*Server) OpenSessionStore

func (s *Server) OpenSessionStore() (oidcauth.SessionStore, error)

OpenSessionStore returns a durable session store backed by the hub's own control-plane database, opening it on first call.

The error return is a *warning*, not a failure: a caller that gets one receives a nil store and is expected to fall back to process-local sessions. Refusing to start a dashboard because its session table is unavailable would trade a degraded property (sessions do not survive a restart) for a total outage, and that is not a trade an authentication layer should make on the operator's behalf.

The database is the hub's, never a managed project's — the same rule as API tokens and executor bindings, and for a sharper reason here: a tenant able to write to the file holding the sessions table could insert a row for any subject and authenticate as them.

func (*Server) ReconcileQuotas

func (s *Server) ReconcileQuotas()

ReconcileQuotas rebuilds every gauge counter from live state.

Called once at startup, and this is the load-bearing half of the design. A persisted counter is a claim about the world that nothing corrects: a hub killed while three runs were in flight comes back believing that tenant still holds three slots, and no code path decrements a counter for a process that no longer exists. Left alone, a crash permanently narrows the tenant it happened to and the only symptom is "my runs are refused".

So the counters are treated as a cache and rebuilt from the things that actually exist: the project registry, the enrolment records, the session table, and the projects whose persisted state says a run is in progress. Daily spend is deliberately not rebuilt — see pkg/quota.

Startup only. This *replaces* the gauge set rather than adjusting it, so calling it on a live hub would erase in-flight admissions — a run started two seconds ago has no representation in any of the sources read below until its project's state file says "running". cmd/ui_cmd.go therefore calls it once, synchronously, before the listener binds.

func (*Server) RecordQuotaSpend

func (s *Server) RecordQuotaSpend(identity string, tokens, usd float64)

RecordQuotaSpend books token and dollar consumption against an identity. Exported so the run-accounting path can call it once a run reports cost.

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run is the lifecycle entrypoint without signal handling: it blocks until ctx is cancelled or the underlying http.Server fails. On ctx cancellation it triggers a bounded graceful shutdown (10s) and returns nil. On listener failure it returns the underlying error. Calling Run more than once on the same Server is not supported.

func (*Server) SessionAuditSink

func (s *Server) SessionAuditSink() func(oidcauth.SessionAudit)

SessionAuditSink returns the callback pkg/oidcauth uses to record session lifecycle events in the hash-chained trail.

Best-effort, matching auditTokenEvent: a wedged journal must not turn signing in — or being signed out — into an error path. Nothing passed through here can carry credential material: SessionAudit has no field a cookie or a refresh token could be written to, and the session id it does carry is a digest.

func (*Server) SessionLimitFor

func (s *Server) SessionLimitFor(identity string, groups, roles []string) int

SessionLimitFor reports how many sessions identity may hold, for the eviction hook in pkg/oidcauth. Zero means unlimited.

Sessions are the one resource enforced by evicting the oldest rather than refusing the newest, because refusing a login is a lockout: the user ends up with no session, and the self-service remedy (POST /api/session/logout-all) needs one. Eviction preserves the invariant the cap exists for without ever locking anyone out of their own account.

func (*Server) SessionStoreSealsRefreshTokens

func (s *Server) SessionStoreSealsRefreshTokens() bool

SessionStoreSealsRefreshTokens reports whether the durable store has an encryption key, and therefore whether IdP-side revocation is armed.

func (*Server) SetQuotaPolicy

func (s *Server) SetQuotaPolicy(resolver *quota.Resolver)

SetQuotaPolicy installs the resolved quota policy. Called from cmd/ui_cmd.go after config is read; a hub that never calls it enforces nothing.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown initiates a graceful shutdown of a server started via Run. It is safe to call from any goroutine; if the server has not started or has already been shut down it is a no-op. The supplied ctx bounds how long Shutdown will wait for in-flight requests to drain.

func (*Server) Start

func (s *Server) Start() error

Start begins listening on the configured port and broadcasting state updates. It installs SIGINT/SIGTERM handlers so the daemon shuts down gracefully when supervised by systemd or interrupted from a TTY: in-flight requests drain, watcher goroutines stop polling, and Start returns nil. Production callers use this entrypoint; tests that need fine-grained lifecycle control should call Run with their own context instead.

func (*Server) StartLeaseJanitor

func (s *Server) StartLeaseJanitor(ctx context.Context)

StartLeaseJanitor begins sweeping expired leases off live agents.

This is what makes a lease TTL bind the whole system rather than just the hub. Lease.Expired and Lease.TTL existed from the start, but nothing ever consulted them once the material had been handed out: an executor given a fifteen-minute credential simply kept it for the three hours its task ran. Sweeping live sessions is the difference.

Idempotent. Calling it twice replaces the previous janitor rather than running two, which matters because the Server is constructed per test and a leaked ticker per construction would show up as a goroutine leak.

func (*Server) StopLeaseJanitor

func (s *Server) StopLeaseJanitor()

StopLeaseJanitor shuts the sweeper down and waits for it to finish.

func (*Server) TLSEnabled

func (s *Server) TLSEnabled() bool

TLSEnabled reports whether this process is configured to terminate TLS.

Jump to

Keyboard shortcuts

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