Documentation
¶
Overview ¶
Package ui implements a local web dashboard for monitoring and controlling cloop.
Index ¶
- Constants
- Variables
- func NewReproduceRunner(exe string) taskreplay.SandboxRunner
- func TokenStatus(t *apitoken.Token, now time.Time) string
- type ChatMessage
- type Server
- func (s *Server) Handler() http.Handler
- func (s *Server) OpenSessionStore() (oidcauth.SessionStore, error)
- func (s *Server) ReconcileQuotas()
- func (s *Server) RecordQuotaSpend(identity string, tokens, usd float64)
- func (s *Server) Run(ctx context.Context) error
- func (s *Server) SessionAuditSink() func(oidcauth.SessionAudit)
- func (s *Server) SessionLimitFor(identity string, groups, roles []string) int
- func (s *Server) SessionStoreSealsRefreshTokens() bool
- func (s *Server) SetQuotaPolicy(resolver *quota.Resolver)
- func (s *Server) Shutdown(ctx context.Context) error
- func (s *Server) Start() error
- func (s *Server) StartLeaseJanitor(ctx context.Context)
- func (s *Server) StopLeaseJanitor()
- func (s *Server) TLSEnabled() bool
Constants ¶
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.
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
SessionStoreSealsRefreshTokens reports whether the durable store has an encryption key, and therefore whether IdP-side revocation is armed.
func (*Server) SetQuotaPolicy ¶
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 ¶
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 ¶
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 ¶
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 ¶
TLSEnabled reports whether this process is configured to terminate TLS.
Source Files
¶
- audit_api.go
- authz.go
- autobackup.go
- executor.go
- executor_agents.go
- executor_supervisor.go
- executors_api.go
- executors_inventory.go
- gitproxy.go
- glasses_api.go
- icons.go
- install_script.go
- ledger_api.go
- livelog.go
- oidc.go
- provider_calls.go
- provision.go
- quotas_api.go
- reproduce_api.go
- reproduce_runner.go
- retention.go
- retention_api.go
- routes.go
- sandbox.go
- secrets.go
- secrets_api.go
- secrets_revoke.go
- secrets_sweep.go
- server.go
- sessions_api.go
- stale_recovery.go
- state_diff.go
- static.go
- subprocess.go
- tls.go
- tokens_api.go
- validate.go
- workspace.go